| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 |
- /**
- * Assembled-app regressions for Session-format lifecycle behavior: released v0
- * migrates before resume without changing its source, while a future format or
- * unknown required event fails loud through the real Loader composition.
- * @module session-format-guard-snapshot
- */
- import { join, dirname } from 'node:path'
- import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises'
- import { fileURLToPath } from 'node:url'
- import { Context } from '@deepseek-ai/cordis'
- import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
- import {
- SESSION_FORMAT_VERSION,
- SessionId,
- SessionSeq,
- type SessionEvent,
- type SessionHeader,
- } from '@deepseek-ai/dsh-session'
- import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
- import {
- generationLogFilename,
- generationLogPath,
- logPath,
- } from '../../../../../../packages/session/session-persistence-jsonl/src/format.ts'
- import { describe, expect, it } from 'vitest'
- const fixtureDir = join(dirname(fileURLToPath(import.meta.url)), 'expected/workspace-context-resume/offline-edit')
- const replayFixture = join(fixtureDir, 'replay.jsonl')
- const configPath = fileURLToPath(new URL('../workspace-context-resume-snapshot.patch.yml', import.meta.url))
- const binScript = fileURLToPath(new URL('../../../../../../packages/test-support/loader-smoke/tests/fixtures/headless-driver.ts', import.meta.url))
- const tsconfigPath = fileURLToPath(new URL('../../../../../../tsconfig.json', import.meta.url))
- // The resumed-agent fixture in the shared config resumes exactly this id.
- const sessionId = SessionId('workspace-context-resume')
- const fixtureEnv = {
- DSH_SNAPSHOT_FILE: replayFixture,
- DSH_LOADER_SMOKE_REQUIRED_ENTRY_ID: 'resumed-agent',
- }
- /** Persist one session with the given header version and events, returning its log path. */
- async function seedSession(root: string, cwd: string, version: number, events: SessionEvent[]): Promise<string> {
- if (version !== SESSION_FORMAT_VERSION) {
- const path = generationLogPath(root, cwd, sessionId, version, 'none')
- await mkdir(dirname(path), { recursive: true })
- await writeFile(path, [
- JSON.stringify({ type: 'session', version, id: sessionId, createdAt: 1, cwd, delegationDepth: 0 }),
- ...events.map(event => JSON.stringify(event)),
- '',
- ].join('\n'))
- return path
- }
- const ctx = new Context()
- await ctx.plugin(JsonlSessionPersistence, { root, compression: 'none' })
- const meta: SessionHeader = {
- version: SESSION_FORMAT_VERSION,
- id: sessionId,
- createdAt: 1,
- cwd,
- isSeeded: false,
- }
- try {
- const handle = await ctx.sessionPersistence.create(meta)
- await handle.append(events)
- await handle.close()
- return logPath(root, meta.cwd, meta.id, 'none')
- } finally {
- await ctx.fiber.dispose()
- }
- }
- function closedTurn(): SessionEvent[] {
- return [
- { type: 'turn/start', seq: SessionSeq(0), time: 1, data: { turn: 1 } },
- { type: 'turn/end', seq: SessionSeq(1), time: 2, data: { turn: 1, reason: { kind: 'completed' } } },
- ]
- }
- describe('session format guard through the assembled app', () => {
- it('migrates v0 before resume, preserves its source, and appends only to the current generation', async () => {
- let sourcePath = ''
- let source = Buffer.alloc(0)
- let sourceIdentity: { readonly dev: bigint; readonly ino: bigint } | undefined
- await runLoaderSmoke({
- label: 'v0 migration before resume',
- tempDirPrefix: 'dsh-format-migrate-v0-',
- binScript,
- libBinScript: binScript,
- configPath,
- binArgs: [configPath, 'Continue the migrated session.'],
- tsconfigPath,
- env: fixtureEnv,
- prepare: async (runCwd) => {
- sourcePath = await seedSession(join(runCwd, '.sessions'), runCwd, 0, closedTurn())
- source = await readFile(sourcePath)
- const identity = await stat(sourcePath, { bigint: true })
- sourceIdentity = { dev: identity.dev, ino: identity.ino }
- },
- inspect: async () => {
- const currentPath = join(
- dirname(sourcePath),
- generationLogFilename(SESSION_FORMAT_VERSION, 'none'),
- )
- const current = await readFile(currentPath, 'utf8')
- const sourceAfter = await stat(sourcePath, { bigint: true })
- const currentIdentity = await stat(currentPath, { bigint: true })
- expect(await readFile(sourcePath)).toEqual(source)
- expect({ dev: sourceAfter.dev, ino: sourceAfter.ino }).toEqual(sourceIdentity)
- expect({ dev: currentIdentity.dev, ino: currentIdentity.ino }).not.toEqual(sourceIdentity)
- expect(JSON.parse(current.split('\n')[0] as string)).toMatchObject({
- version: SESSION_FORMAT_VERSION,
- })
- expect(current.trimEnd().split('\n').length).toBeGreaterThan(closedTurn().length + 1)
- // `session.lock` is the write handle's kernel lock file, published
- // with the first materializing write and kept across release.
- expect((await readdir(dirname(sourcePath))).sort())
- .toEqual(['session.jsonl', 'session.lock', generationLogFilename(SESSION_FORMAT_VERSION, 'none')])
- },
- })
- }, LOADER_SMOKE_TEST_TIMEOUT_MS)
- it('refuses to resume a newer-format log, naming the upgrade direction and the raw log path', async () => {
- let sessionPath = ''
- const result = await runLoaderSmoke({
- label: 'newer-format resume refusal',
- tempDirPrefix: 'dsh-format-guard-version-',
- binScript,
- libBinScript: binScript,
- configPath,
- binArgs: [configPath, 'Try to resume.'],
- tsconfigPath,
- env: fixtureEnv,
- expectedExitCode: 1,
- prepare: async (runCwd) => {
- sessionPath = await seedSession(join(runCwd, '.sessions'), runCwd, SESSION_FORMAT_VERSION + 99, closedTurn())
- },
- })
- expect(result.stderr).toContain(
- `session "${sessionId}" uses log format v${SESSION_FORMAT_VERSION + 99}, but this harness reads only v${SESSION_FORMAT_VERSION}: the log was written by a newer harness — upgrade the harness to open it`,
- )
- // macOS reports the temp dir via the /private symlink parent; assert the
- // stable path suffix instead of the realpath-dependent prefix.
- expect(result.stderr).toContain('(raw log: ')
- expect(result.stderr).toContain(sessionPath.slice(sessionPath.indexOf('/.sessions/')))
- }, LOADER_SMOKE_TEST_TIMEOUT_MS)
- it('refuses unaudited V2 queued content before resume without publishing a successor', async () => {
- let sourcePath = ''
- let source = Buffer.alloc(0)
- let sourceIdentity: { readonly dev: bigint; readonly ino: bigint } | undefined
- const result = await runLoaderSmoke({
- label: 'unaudited V2 content resume refusal',
- tempDirPrefix: 'dsh-format-guard-content-',
- binScript,
- libBinScript: binScript,
- configPath,
- binArgs: [configPath, 'Try to resume.'],
- tsconfigPath,
- env: fixtureEnv,
- expectedExitCode: 1,
- prepare: async (runCwd) => {
- sourcePath = generationLogPath(join(runCwd, '.sessions'), runCwd, sessionId, 2, 'none')
- await mkdir(dirname(sourcePath), { recursive: true })
- const rows = [
- { type: 'session', version: 2, id: sessionId, createdAt: 1, cwd: runCwd, isSeeded: false, delegationDepth: 0 },
- { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
- { type: 'step/start', seq: 1, time: 2, data: { turn: 1, step: 1 } },
- { type: 'team/message/queued', seq: 2, time: 3, data: {
- version: 1, teamId: 'team', message: {
- id: 'queued', senderId: 'sender', senderName: 'Sender', targetId: 'target', delivery: 'quiet',
- content: [{ type: 'future-message-block', localSeq: 1 }],
- },
- } },
- ]
- source = Buffer.from(rows.map(row => JSON.stringify(row)).join('\n') + '\n')
- await writeFile(sourcePath, source)
- const identity = await stat(sourcePath, { bigint: true })
- sourceIdentity = { dev: identity.dev, ino: identity.ino }
- },
- inspect: async () => {
- expect(await readFile(sourcePath)).toEqual(source)
- const identity = await stat(sourcePath, { bigint: true })
- expect({ dev: identity.dev, ino: identity.ino }).toEqual(sourceIdentity)
- expect((await readdir(dirname(sourcePath))).filter(name => name !== 'session.lock'))
- .toEqual(['session.v2.jsonl'])
- },
- })
- expect(result.stderr).toContain(
- 'format v2 team/message/queued at seq 2 data.message.content[0]: cannot safely transform unclassified message content kind "future-message-block"',
- )
- expect(result.stderr).toContain('source v2 artifact remains unchanged')
- expect(result.stderr).toContain(sourcePath.slice(sourcePath.indexOf('/.sessions/')))
- }, LOADER_SMOKE_TEST_TIMEOUT_MS)
- it('refuses to resume a log with an unknown required event type', async () => {
- let sessionPath = ''
- const result = await runLoaderSmoke({
- label: 'unknown-event resume refusal',
- tempDirPrefix: 'dsh-format-guard-event-',
- binScript,
- libBinScript: binScript,
- configPath,
- binArgs: [configPath, 'Try to resume.'],
- tsconfigPath,
- env: fixtureEnv,
- expectedExitCode: 1,
- prepare: async (runCwd) => {
- sessionPath = await seedSession(join(runCwd, '.sessions'), runCwd, SESSION_FORMAT_VERSION, [
- ...closedTurn(),
- { type: 'future/event', seq: SessionSeq(2), time: 3, data: { payload: 1 } } as unknown as SessionEvent,
- ])
- },
- })
- expect(result.stderr).toContain(
- `session "${sessionId}" contains event type "future/event" (seq 2) unknown to this harness and not marked ignorable; refusing to interpret the log — it was likely written by a newer harness`,
- )
- // macOS reports the temp dir via the /private symlink parent; assert the
- // stable path suffix instead of the realpath-dependent prefix.
- expect(result.stderr).toContain('(raw log: ')
- expect(result.stderr).toContain(sessionPath.slice(sessionPath.indexOf('/.sessions/')))
- }, LOADER_SMOKE_TEST_TIMEOUT_MS)
- })
|