Files
deepseek-harness/snapshots/session/headless.snapshot.ts
T

112 lines
4.3 KiB
TypeScript

/** Recorded-session replay through the shipped headless `dsh` profile. */
import { readFile, readdir } from 'node:fs/promises'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { normalizeSessionSnapshot, parseSnapshotManifest, type NormalizeContext } from '@deepseek-ai/dsh-session-snapshot'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
const repoRoot = fileURLToPath(new URL('../../', import.meta.url))
const snapshotsRoot = fileURLToPath(new URL('./', import.meta.url))
const dshBin = join(repoRoot, 'apps/cli/src/bin.ts')
const tsconfigPath = join(repoRoot, 'tsconfig.json')
const defaultCompositionDir = join(snapshotsRoot, 'text-turn')
const basePatch = join(defaultCompositionDir, 'cordis.yml')
const replayPatch = join(defaultCompositionDir, 'cordis.snapshot.yml')
interface JsonObject {
[key: string]: unknown
}
function contextOf(log: string): NormalizeContext {
const header = JSON.parse(log.split('\n').find(line => line.trim() !== '') ?? '{}') as JsonObject
return {
sessionIds: typeof header.id === 'string' ? [header.id] : [],
cwd: typeof header.cwd === 'string' ? header.cwd : '\0missing-cwd\0',
}
}
async function persistedSession(cwd: string): Promise<string> {
const root = join(cwd, '.dsh', 'sessions')
const files = (await readdir(root, { recursive: true }))
.filter(file => file.endsWith('session.jsonl'))
expect(files).toHaveLength(1)
return readFile(join(root, files[0] as string), 'utf8')
}
function records(log: string): JsonObject[] {
return log.split(/\r?\n/)
.filter(line => line.trim() !== '')
.map(line => JSON.parse(line) as JsonObject)
}
function taskFromSession(log: string): string {
for (const record of records(log)) {
if (record.type !== 'user/message') continue
const data = record.data as JsonObject | undefined
const source = data?.source as JsonObject | undefined
if (source?.kind !== 'user' || !Array.isArray(data?.content)) continue
const blocks = data.content as JsonObject[]
if (blocks.length === 1 && blocks[0]?.type === 'text' && typeof blocks[0].text === 'string') {
return blocks[0].text
}
}
throw new Error('headless snapshot session has no single-text user task')
}
function finalTextFromSession(log: string): string {
const messages = records(log).flatMap((record) => {
if (record.type !== 'assistant/message') return []
const data = record.data as JsonObject | undefined
const message = data?.message as JsonObject | undefined
return message === undefined ? [] : [message]
})
const content = messages.at(-1)?.content
if (!Array.isArray(content)) throw new Error('headless snapshot session has no final assistant message')
return (content as JsonObject[])
.flatMap(block => block.type === 'text' && typeof block.text === 'string' ? [block.text] : [])
.join('')
}
describe('headless recorded-session snapshots', () => {
it.each(['text-turn', 'tool-call-turn'])('replays %s through dsh --profile headless', async (name) => {
const scenarioDir = join(snapshotsRoot, name)
const manifestPath = join(scenarioDir, 'snapshot.yml')
expect(parseSnapshotManifest(await readFile(manifestPath, 'utf8'), manifestPath)).toMatchObject({
version: 1,
profile: 'headless',
composition: 'default',
recording: 'live',
})
const fixture = await readFile(join(scenarioDir, 'session.jsonl'), 'utf8')
const task = taskFromSession(fixture)
let actual = ''
const result = await runLoaderSmoke({
label: 'tool-call-turn headless snapshot',
tempDirPrefix: 'dsh-headless-session-snapshot-',
binScript: dshBin,
configPath: basePatch,
binArgs: [
'--profile', 'headless',
'--patch', basePatch,
'--patch', replayPatch,
task,
],
tsconfigPath,
env: {
DSH_SNAPSHOT: 'replay',
DSH_SNAPSHOT_FILE: join(scenarioDir, 'session.jsonl'),
DSH_TELEMETRY_DISABLED: '1',
},
inspect: async (cwd) => { actual = await persistedSession(cwd) },
})
expect(result.stdout).toBe(`${finalTextFromSession(fixture)}\n`)
expect(result.stderr).toBe('')
expect(normalizeSessionSnapshot(actual, contextOf(actual)))
.toBe(normalizeSessionSnapshot(fixture, contextOf(fixture)))
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
})