fix(workspace-context): reconcile resumed baselines
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Remember the workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":2,"time":0,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nOld workspace instruction.\n</system-reminder>"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"ba65bdb41810f4d0129129dcbd6cadcd643c069d"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":2,"time":0,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nOld workspace instruction.\n</system-reminder>"}],"source":{"kind":"workspace-instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"ba65bdb41810f4d0129129dcbd6cadcd643c069d"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"turn/end","seq":3,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
{"type":"session/end-seed","seq":4,"time":0,"data":{}}
|
||||
{"type":"turn/start","seq":5,"time":0,"data":{"turn":2,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Remember the workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":2,"time":0,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: CLAUDE.md\n\nOld CLAUDE rule.\n\nInstructions from: AGENTS.md\n\nOld AGENTS rule.\n</system-reminder>"}],"source":{"kind":"workspace-instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"CLAUDE.md\",\"AGENTS.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000CLAUDE.md","path":"CLAUDE.md","digest":"b525eb8a6d3660b732dad4b0aff1b7c63ab32890"},{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"3113bd093ae91976207dcef7390bdc0b2bfcfa10"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"turn/end","seq":3,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
{"type":"session/end-seed","seq":4,"time":0,"data":{}}
|
||||
{"type":"turn/start","seq":5,"time":0,"data":{"turn":2,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":6,"time":0,"data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":7,"time":0,"data":{"title":"Remember the workspace instruction.","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"<system-reminder>\nThis complete workspace instruction baseline replaces all earlier workspace instruction baselines. The following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nCurrent AGENTS rule.\n\n\nInstructions from: CLAUDE.md\n\nCurrent CLAUDE rule.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"7f53d2327837129750aef117f9754a001c46cf68"},{"action":"set","scope":".\u0000CLAUDE.md","path":"CLAUDE.md","digest":"5b1e9e3fd759eee6b43ceff899e47fb10c64701a"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":9,"time":0,"data":{"turn":2,"step":1}}
|
||||
{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}"},"reason":"initial"}}
|
||||
{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"RESUME_DONE"}}}
|
||||
{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RESUME_DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":16,"time":0,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"RESUME_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"}},"sourceEventSeqs":[12,13,14,15],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":17,"time":0,"data":{"turn":2,"step":1}}
|
||||
{"type":"turn/end","seq":18,"time":0,"data":{"turn":2,"reason":{"kind":"completed"}}}
|
||||
@@ -19,12 +19,14 @@ import SessionStore, {
|
||||
} from '@deepseek-ai/dsh-session'
|
||||
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import { renderWorkspaceContext } from '@deepseek-ai/dsh-workspace-context'
|
||||
import { resolveConfig, workspaceBaselineIdentity } from '@deepseek-ai/dsh-workspace-context/src/config.ts'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const fixtureDir = join(dirname(fileURLToPath(import.meta.url)), 'workspace-context-resume-snapshots/offline-edit')
|
||||
const replayFixture = join(fixtureDir, 'replay.jsonl')
|
||||
const replayOverride = join(fixtureDir, 'replay.override.json')
|
||||
const sessionExpected = join(fixtureDir, 'session.expected.jsonl')
|
||||
const precedenceExpected = join(dirname(fixtureDir), 'precedence-change/session.expected.jsonl')
|
||||
const configPath = fileURLToPath(new URL('../workspace-context-resume.cordis.snapshot.yml', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
@@ -33,7 +35,16 @@ const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
|
||||
const oldInstruction = 'Old workspace instruction.'
|
||||
const newInstruction = 'New workspace instruction after offline edit.'
|
||||
|
||||
async function seedVisibleBaseline(root: string, cwd: string): Promise<string> {
|
||||
interface SeedBaselineOptions {
|
||||
files?: Array<{ name: string; content: string }>
|
||||
instructionFileCandidates?: string[]
|
||||
}
|
||||
|
||||
async function seedVisibleBaseline(
|
||||
root: string,
|
||||
cwd: string,
|
||||
options: SeedBaselineOptions = {},
|
||||
): Promise<string> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionPersistenceJsonl, { root, compression: 'none' })
|
||||
@@ -44,11 +55,19 @@ async function seedVisibleBaseline(root: string, cwd: string): Promise<string> {
|
||||
cwd,
|
||||
delegationDepth: 0,
|
||||
}
|
||||
const baseline = renderWorkspaceContext([{
|
||||
absolutePath: join(cwd, 'AGENTS.md'),
|
||||
displayPath: 'AGENTS.md',
|
||||
content: oldInstruction,
|
||||
}], { maxBytes: 65536 })
|
||||
const files = options.files ?? [{ name: 'AGENTS.md', content: oldInstruction }]
|
||||
const baseline = renderWorkspaceContext(files.map(file => ({
|
||||
absolutePath: join(cwd, file.name),
|
||||
displayPath: file.name,
|
||||
content: file.content,
|
||||
})), { maxBytes: 65536 })
|
||||
const config = resolveConfig({
|
||||
dshHome: join(cwd, '.dsh'),
|
||||
maxBytes: 65536,
|
||||
...options.instructionFileCandidates === undefined
|
||||
? {}
|
||||
: { instructionFileCandidates: options.instructionFileCandidates },
|
||||
})
|
||||
const events: SessionEvent[] = [
|
||||
{ type: 'turn/start', seq: 0, time: 10, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } },
|
||||
{
|
||||
@@ -67,12 +86,13 @@ async function seedVisibleBaseline(root: string, cwd: string): Promise<string> {
|
||||
source: {
|
||||
kind: 'workspace-instructions',
|
||||
baseline: true,
|
||||
changes: [{
|
||||
baselineIdentity: workspaceBaselineIdentity(config, cwd, cwd),
|
||||
changes: files.map(file => ({
|
||||
action: 'set',
|
||||
scope: '.\0AGENTS.md',
|
||||
path: 'AGENTS.md',
|
||||
digest: createHash('sha1').update(oldInstruction).digest('hex'),
|
||||
}],
|
||||
scope: `.\0${file.name}`,
|
||||
path: file.name,
|
||||
digest: createHash('sha1').update(file.content).digest('hex'),
|
||||
})),
|
||||
},
|
||||
}),
|
||||
surfaceOp: 'append',
|
||||
@@ -148,4 +168,69 @@ describe('workspace-context resume snapshot', () => {
|
||||
reason: { kind: 'completed' },
|
||||
})
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('recomposes a compatible current-order baseline when precedence changed offline', async () => {
|
||||
let cwd = ''
|
||||
let sessionPath = ''
|
||||
const result = await runLoaderSmoke({
|
||||
label: 'workspace-context precedence-change resume snapshot',
|
||||
tempDirPrefix: 'dsh-workspace-context-precedence-',
|
||||
binScript,
|
||||
configPath,
|
||||
binArgs: ['--config', configPath, '--output-format', 'stream-json', 'Acknowledge the current workspace instruction.'],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT_FILE: replayFixture,
|
||||
DSH_SNAPSHOT_OVERRIDE: replayOverride,
|
||||
},
|
||||
prepare: async (runCwd) => {
|
||||
cwd = runCwd
|
||||
await mkdir(join(runCwd, '.git'), { recursive: true })
|
||||
await writeFile(join(runCwd, 'AGENTS.md'), 'Current AGENTS rule.\n')
|
||||
await writeFile(join(runCwd, 'CLAUDE.md'), 'Current CLAUDE rule.\n')
|
||||
sessionPath = await seedVisibleBaseline(join(runCwd, '.sessions'), runCwd, {
|
||||
files: [
|
||||
{ name: 'CLAUDE.md', content: 'Old CLAUDE rule.' },
|
||||
{ name: 'AGENTS.md', content: 'Old AGENTS rule.' },
|
||||
],
|
||||
instructionFileCandidates: ['CLAUDE.md', 'AGENTS.md'],
|
||||
})
|
||||
},
|
||||
inspect: async () => {
|
||||
const normalization: NormalizeContext = { sessionIds: [sessionId], cwd }
|
||||
const session = scrubRequestHeaders(normalizeSessionLog(await readFile(sessionPath, 'utf8'), normalization))
|
||||
if (refreshing) {
|
||||
await mkdir(dirname(precedenceExpected), { recursive: true })
|
||||
await writeFile(precedenceExpected, session)
|
||||
}
|
||||
expect(session).toBe(await readFile(precedenceExpected, 'utf8'))
|
||||
|
||||
const records = session.trimEnd().split('\n').map(line => JSON.parse(line) as {
|
||||
type?: string
|
||||
data?: {
|
||||
source?: { kind?: string; baseline?: boolean }
|
||||
content?: Array<{ type?: string; text?: string }>
|
||||
}
|
||||
})
|
||||
const baselines = records.filter(record => record.type === 'user/message'
|
||||
&& record.data?.source?.kind === 'workspace-instructions'
|
||||
&& record.data.source.baseline === true)
|
||||
expect(baselines).toHaveLength(2)
|
||||
const replacement = JSON.stringify(baselines.at(-1)?.data?.content)
|
||||
expect(replacement).toContain('replaces all earlier workspace instruction baselines')
|
||||
expect(replacement.indexOf('Instructions from: AGENTS.md'))
|
||||
.toBeLessThan(replacement.indexOf('Instructions from: CLAUDE.md'))
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.stderr).toBe('')
|
||||
expect(result.stdout.trimEnd().split('\n').map(line => JSON.parse(line) as Record<string, unknown>).at(-1))
|
||||
.toMatchObject({
|
||||
type: 'result',
|
||||
success: true,
|
||||
sessionId,
|
||||
result: 'RESUME_DONE',
|
||||
reason: { kind: 'completed' },
|
||||
})
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user