Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
176 lines
7.4 KiB
TypeScript
176 lines
7.4 KiB
TypeScript
import { readFile, writeFile } from 'node:fs/promises'
|
|
import { dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import {
|
|
normalizeSessionLog,
|
|
normalizeStdout,
|
|
runScenario,
|
|
scrubRequestHeaders,
|
|
type AgentUnderTest,
|
|
type InputScript,
|
|
type NormalizeContext,
|
|
} from '@deepseek-ai/dsh-acp-snapshot'
|
|
import { foldGoal } from '@deepseek-ai/dsh-goal'
|
|
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
// This lifecycle proof has goal-specific timestamp normalization and semantic
|
|
// assertions, so it owns a separate snapshot root from the generic suite.
|
|
const scenarioDir = join(dirname(fileURLToPath(import.meta.url)), 'goal-snapshots/goal-round-driver')
|
|
const fixtureFile = join(scenarioDir, 'session.jsonl')
|
|
const overrideFile = join(scenarioDir, 'replay.override.json')
|
|
const stdoutExpected = join(scenarioDir, 'stdout.expected.jsonl')
|
|
const sessionExpected = join(scenarioDir, 'session.expected.jsonl')
|
|
const wrapupDir = join(dirname(fileURLToPath(import.meta.url)), 'goal-snapshots/goal-wrapup')
|
|
const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
|
|
|
|
const agent: AgentUnderTest = {
|
|
binScript: fileURLToPath(new URL('../../../packages/examples/acp-demo/src/bin.ts', import.meta.url)),
|
|
configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)),
|
|
tsconfigPath: fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)),
|
|
}
|
|
|
|
interface JsonObject {
|
|
[key: string]: unknown
|
|
}
|
|
|
|
/** Parse non-empty records from one JSONL artifact. */
|
|
function parseJsonl(content: string): JsonObject[] {
|
|
return content.split('\n').filter(line => line.trim().length > 0)
|
|
.map(line => JSON.parse(line) as JsonObject)
|
|
}
|
|
|
|
/** Zero durable goal timestamps inside metadata records and rendered XML JSON. */
|
|
function normalizeGoalTimestamps(value: unknown): unknown {
|
|
if (typeof value === 'string') {
|
|
return value.replace(/("(?:createdAt|updatedAt|clearedAt)":)\d+/g, '$10')
|
|
}
|
|
if (Array.isArray(value)) return value.map(normalizeGoalTimestamps)
|
|
if (value !== null && typeof value === 'object') {
|
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
key,
|
|
['createdAt', 'updatedAt', 'clearedAt'].includes(key) && typeof item === 'number'
|
|
? 0
|
|
: normalizeGoalTimestamps(item),
|
|
]))
|
|
}
|
|
return value
|
|
}
|
|
|
|
/** Normalize one persisted goal log after the shared snapshot scrubbers. */
|
|
function normalizeGoalLog(content: string, context: NormalizeContext): string {
|
|
return parseJsonl(scrubRequestHeaders(normalizeSessionLog(content, context)))
|
|
.map(record => JSON.stringify(normalizeGoalTimestamps(record)))
|
|
.join('\n') + '\n'
|
|
}
|
|
|
|
describe('same-session goal snapshot through the ACP automation driver', () => {
|
|
it('runs exact automatic rounds in the shipped application and persists cancellation', async () => {
|
|
const input = JSON.parse(await readFile(join(scenarioDir, 'input.json'), 'utf8')) as InputScript
|
|
const result = await runScenario(input, {
|
|
agent,
|
|
mode: 'replay',
|
|
fixtureFile,
|
|
overrideFile,
|
|
configPath: agent.configPath,
|
|
})
|
|
|
|
expect(result.stderr).toBe('')
|
|
expect(result.sessionLogs).toHaveLength(1)
|
|
const log = result.sessionLogs[0]
|
|
if (log === undefined) throw new Error('goal snapshot did not persist its session')
|
|
const records = parseJsonl(log.content)
|
|
const events = records.slice(1) as unknown as SessionEvent[]
|
|
const calls = events.filter(event => event.type === 'tool/call').map(event => event.data.name)
|
|
expect(calls).toEqual(['create_goal', 'get_goal'])
|
|
const rounds = events.flatMap(event => event.type === 'user/message' && event.data.source.kind === 'goal'
|
|
&& event.data.source.round > 0
|
|
? [event.data.source.round]
|
|
: [])
|
|
expect(rounds).toEqual([1, 2])
|
|
expect(foldGoal(events)).toMatchObject({
|
|
goal: {
|
|
objective: 'Finish the ACP goal-round-driver snapshot proof',
|
|
phase: 'paused',
|
|
revision: 2,
|
|
maxGoalRounds: 2,
|
|
},
|
|
roundsStarted: 2,
|
|
})
|
|
|
|
const context: NormalizeContext = {
|
|
sessionIds: [result.sessionId, log.id].filter((id): id is string => id !== undefined),
|
|
cwd: result.cwd,
|
|
}
|
|
const stdout = normalizeStdout(result.rawStdout, context)
|
|
const session = normalizeGoalLog(log.content, context)
|
|
if (refreshing) {
|
|
await Promise.all([
|
|
writeFile(stdoutExpected, stdout),
|
|
writeFile(sessionExpected, session),
|
|
])
|
|
}
|
|
expect(stdout).toBe(await readFile(stdoutExpected, 'utf8'))
|
|
expect(session).toBe(await readFile(sessionExpected, 'utf8'))
|
|
})
|
|
|
|
it('injects the wrap-up instruction after an autonomous completion and delivers a closing message', async () => {
|
|
const input = JSON.parse(await readFile(join(wrapupDir, 'input.json'), 'utf8')) as InputScript
|
|
const result = await runScenario(input, {
|
|
agent,
|
|
mode: 'replay',
|
|
fixtureFile: join(wrapupDir, 'session.jsonl'),
|
|
overrideFile: join(wrapupDir, 'replay.override.json'),
|
|
configPath: agent.configPath,
|
|
})
|
|
|
|
expect(result.stderr).toBe('')
|
|
expect(result.sessionLogs).toHaveLength(1)
|
|
const log = result.sessionLogs[0]
|
|
if (log === undefined) throw new Error('goal wrap-up snapshot did not persist its session')
|
|
const records = parseJsonl(log.content)
|
|
const events = records.slice(1) as unknown as SessionEvent[]
|
|
const calls = events.filter(event => event.type === 'tool/call').map(event => event.data.name)
|
|
expect(calls).toEqual(['create_goal', 'update_goal'])
|
|
expect(foldGoal(events)).toMatchObject({
|
|
goal: {
|
|
objective: 'Finish the ACP goal wrap-up snapshot proof',
|
|
phase: 'complete',
|
|
revision: 2,
|
|
},
|
|
roundsStarted: 1,
|
|
})
|
|
// The wrap-up instruction is one plugin-sourced context injected after the
|
|
// terminal tool result, and the model still answers inside the same turn.
|
|
const wrapups = events.filter(event => event.type === 'user/message'
|
|
&& event.data.source.kind === 'plugin' && event.data.source.plugin === 'tool-goal')
|
|
expect(wrapups).toHaveLength(1)
|
|
const wrapupText = wrapups.map(event => event.type === 'user/message' ? event.data.content : [])[0]
|
|
expect(JSON.stringify(wrapupText)).toContain('<goal_complete>')
|
|
const closing = events.filter(event => event.type === 'assistant/message')
|
|
.flatMap(event => event.data.message.content)
|
|
.filter(block => block.type === 'text' && block.text.startsWith('GOAL WRAP-UP'))
|
|
expect(closing).toHaveLength(1)
|
|
const roundTurnEnds = events.filter(event => event.type === 'turn/end' && event.data.turn === 2)
|
|
expect(roundTurnEnds).toHaveLength(1)
|
|
expect(roundTurnEnds[0]?.data).toMatchObject({ turn: 2, reason: { kind: 'completed' } })
|
|
|
|
const context: NormalizeContext = {
|
|
sessionIds: [result.sessionId, log.id].filter((id): id is string => id !== undefined),
|
|
cwd: result.cwd,
|
|
}
|
|
const stdout = normalizeStdout(result.rawStdout, context)
|
|
const session = normalizeGoalLog(log.content, context)
|
|
const wrapupStdoutExpected = join(wrapupDir, 'stdout.expected.jsonl')
|
|
const wrapupSessionExpected = join(wrapupDir, 'session.expected.jsonl')
|
|
if (refreshing) {
|
|
await Promise.all([
|
|
writeFile(wrapupStdoutExpected, stdout),
|
|
writeFile(wrapupSessionExpected, session),
|
|
])
|
|
}
|
|
expect(stdout).toBe(await readFile(wrapupStdoutExpected, 'utf8'))
|
|
expect(session).toBe(await readFile(wrapupSessionExpected, 'utf8'))
|
|
})
|
|
})
|