Merge remote-tracking branch 'origin/master' into codex/fix-deepseek-keepalive
# Conflicts: # .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml # examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml # packages/llm/llm-deepseek/README.i18n.yaml # packages/util/timeout/README.i18n.yaml
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { readFile, readdir, writeFile } from 'node:fs/promises'
|
||||
import { copyFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
|
||||
import { createServer } from 'node:http'
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http'
|
||||
import { delimiter, dirname, join } from 'node:path'
|
||||
@@ -14,6 +14,10 @@ import {
|
||||
type NormalizeContext,
|
||||
} from '@deepseek-ai/dsh-acp-snapshot'
|
||||
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
|
||||
import {
|
||||
decompressZstdFrame,
|
||||
scanZstdFrames,
|
||||
} from '@deepseek-ai/dsh-session-persistence-jsonl/src/zstd.ts'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const snapshotsDir = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
|
||||
@@ -29,16 +33,29 @@ const goalScenarioDir = join(snapshotsDir, 'goal-tools')
|
||||
const goalConfigPath = fileURLToPath(new URL('../goal.cordis.snapshot.yml', import.meta.url))
|
||||
const retryScenarioDir = join(snapshotsDir, 'provider-retry')
|
||||
const retryConfigPath = fileURLToPath(new URL('../retry.cordis.snapshot.yml', import.meta.url))
|
||||
const compactionScenarioDir = join(snapshotsDir, 'compaction-recovery')
|
||||
const compactionSessionFixture = join(compactionScenarioDir, 'session.jsonl')
|
||||
const compactionStreamExpected = join(compactionScenarioDir, 'stream-json.expected.jsonl')
|
||||
const compactionConfigPath = fileURLToPath(new URL('../compaction.cordis.snapshot.yml', import.meta.url))
|
||||
const credentialsScenarioDir = join(snapshotsDir, 'missing-credential')
|
||||
const credentialsConfigPath = fileURLToPath(new URL('../credentials.cordis.snapshot.yml', import.meta.url))
|
||||
// Same keyless composition as the missing-credential scenario: the endpoint is
|
||||
// never dialed either way, because a supplied-but-unusable key fails credential
|
||||
// resolution exactly where an absent one does.
|
||||
const invalidCredentialScenarioDir = join(snapshotsDir, 'invalid-credential')
|
||||
const ralphScenarioDir = join(snapshotsDir, 'ralph-loop')
|
||||
const ralphConfigPath = fileURLToPath(new URL('../ralph.cordis.snapshot.yml', import.meta.url))
|
||||
const startupFailureConfigPath = fileURLToPath(new URL('./fixtures/startup-activation-error/cordis.yml', import.meta.url))
|
||||
const startupFailureExpected = join(snapshotsDir, 'startup-activation-error', 'stderr.expected.txt')
|
||||
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
|
||||
const dshBinScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
const reasoningConfigPath = fileURLToPath(new URL('./fixtures/cli.cordis.yml', import.meta.url))
|
||||
const deepseekDefaultsConfigPath = fileURLToPath(new URL('./fixtures/deepseek-defaults.cordis.yml', import.meta.url))
|
||||
const dshRunOverlayPath = fileURLToPath(new URL('./fixtures/dsh-run.cordis.yml', import.meta.url))
|
||||
const dshRunSessionExpected = join(snapshotsDir, 'dsh-run', 'session.expected.jsonl')
|
||||
const dshRunFailureExpected = join(snapshotsDir, 'dsh-run', 'stderr.expected.txt')
|
||||
const cliMockLlmPluginPath = fileURLToPath(new URL('./fixtures/cli-mock-llm.ts', import.meta.url))
|
||||
const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
|
||||
|
||||
interface JsonObject {
|
||||
@@ -168,23 +185,100 @@ async function scenarioPrompt(dir: string, label: string): Promise<string> {
|
||||
return prompt
|
||||
}
|
||||
|
||||
async function persistedLogs(cwd: string): Promise<PersistedLog[]> {
|
||||
const root = join(cwd, '.sessions')
|
||||
const files = (await readdir(root, { recursive: true })).filter(file => file.endsWith('.jsonl'))
|
||||
async function readPersistedLog(file: string): Promise<string> {
|
||||
const content = await readFile(file)
|
||||
if (!file.endsWith('.zstd')) return content.toString('utf8')
|
||||
const scan = scanZstdFrames(content)
|
||||
if (scan.tornStart !== undefined) throw new Error(`persisted snapshot log has a torn Zstandard frame: ${file}`)
|
||||
const decoded: Buffer[] = []
|
||||
for (const frame of scan.frames) {
|
||||
decoded.push(await decompressZstdFrame(content.subarray(frame.start, frame.end)))
|
||||
}
|
||||
return Buffer.concat(decoded).toString('utf8')
|
||||
}
|
||||
|
||||
async function persistedLogs(cwd: string, root: string = join(cwd, '.sessions')): Promise<PersistedLog[]> {
|
||||
const files = (await readdir(root, { recursive: true }))
|
||||
.filter(file => file.endsWith('.jsonl') || file.endsWith('.jsonl.zstd'))
|
||||
return Promise.all(files.map(async (file) => {
|
||||
const content = await readFile(join(root, file), 'utf8')
|
||||
const content = await readPersistedLog(join(root, file))
|
||||
return { content, header: parseJsonl(content)[0] ?? {} }
|
||||
}))
|
||||
}
|
||||
|
||||
/** Install the keyless product-CLI adapter into the temporary headless profile. */
|
||||
async function prepareCliMockFixture(cwd: string): Promise<void> {
|
||||
const fixtureDir = join(cwd, '.dsh', 'profiles', 'headless', 'snapshot-fixtures')
|
||||
await mkdir(fixtureDir, { recursive: true })
|
||||
await Promise.all([
|
||||
copyFile(cliMockLlmPluginPath, join(fixtureDir, 'cli-mock-llm.ts')),
|
||||
writeFile(join(fixtureDir, 'package.json'), '{"type":"module"}\n'),
|
||||
])
|
||||
}
|
||||
|
||||
describe('headless stream-json snapshots', () => {
|
||||
it('runs one task through the product dsh run command', async () => {
|
||||
const task = 'Prove the product dsh run path with one real tool round trip.'
|
||||
const result = await runLoaderSmoke({
|
||||
label: 'product dsh run snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-dsh-run-',
|
||||
binScript: dshBinScript,
|
||||
configPath: dshRunOverlayPath,
|
||||
binArgs: ['run', '--patch', dshRunOverlayPath, task],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_PERMISSION_MODE: 'danger-full-access',
|
||||
DSH_TELEMETRY_DISABLED: '1',
|
||||
NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
|
||||
},
|
||||
prepare: prepareCliMockFixture,
|
||||
inspect: async (cwd) => {
|
||||
const logs = await persistedLogs(cwd, join(cwd, '.dsh', 'sessions'))
|
||||
expect(logs).toHaveLength(1)
|
||||
const actual = logs[0]
|
||||
if (actual === undefined) throw new Error('dsh run did not persist its session')
|
||||
const context = contextFromLogs([actual.content])
|
||||
const session = scrubRequestHeaders(normalizeSessionLog(actual.content, context))
|
||||
if (refreshing) await writeFile(dshRunSessionExpected, session)
|
||||
expect(session).toBe(await readFile(dshRunSessionExpected, 'utf8'))
|
||||
expect(session).toContain(task)
|
||||
expect(session).toContain('CLI tool round trip complete: CLI_TOOL_ROUND_TRIP')
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.stdout).toBe('CLI tool round trip complete: CLI_TOOL_ROUND_TRIP\n')
|
||||
expect(result.stderr).toBe('')
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('prints a terminal model failure through the product dsh run command', async () => {
|
||||
const result = await runLoaderSmoke({
|
||||
label: 'product dsh run model failure snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-dsh-run-failure-',
|
||||
binScript: dshBinScript,
|
||||
configPath: dshRunOverlayPath,
|
||||
binArgs: ['run', '--patch', dshRunOverlayPath, 'Trigger the keyless model failure.'],
|
||||
tsconfigPath,
|
||||
expectedExitCode: 1,
|
||||
env: {
|
||||
DSH_CLI_MOCK_FAILURE: '1',
|
||||
DSH_TELEMETRY_DISABLED: '1',
|
||||
NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
|
||||
},
|
||||
prepare: prepareCliMockFixture,
|
||||
})
|
||||
|
||||
expect(result.stdout).toBe('\n')
|
||||
await expect(result.stderr).toMatchFileSnapshot(dshRunFailureExpected)
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('prints the original Loader activation error through the assembled one-shot app', async () => {
|
||||
const result = await runLoaderSmoke({
|
||||
label: 'headless startup activation error snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-startup-error-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: startupFailureConfigPath,
|
||||
binArgs: ['--config', startupFailureConfigPath, '--output-format', 'stream-json', 'unreachable task'],
|
||||
binArgs: [startupFailureConfigPath, 'unreachable task'],
|
||||
tsconfigPath,
|
||||
expectedExitCode: 1,
|
||||
})
|
||||
@@ -200,8 +294,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'provider retry headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-provider-retry-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: retryConfigPath,
|
||||
binArgs: ['--config', retryConfigPath, '--output-format', 'stream-json', prompt],
|
||||
binArgs: [retryConfigPath, prompt],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT: 'replay',
|
||||
@@ -232,15 +327,86 @@ describe('headless stream-json snapshots', () => {
|
||||
expect(normalized).toBe(await readFile(streamExpected, 'utf8'))
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('surfaces actionable missing-credential guidance through the one-shot app', async () => {
|
||||
it('recovers from context overflow through an assembled compaction', async () => {
|
||||
const prompt = await scenarioPrompt(compactionScenarioDir, 'compaction-recovery')
|
||||
let expectedSession = await readFile(compactionSessionFixture, 'utf8')
|
||||
let runCwd = ''
|
||||
const result = await runLoaderSmoke({
|
||||
label: 'compaction recovery headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-compaction-recovery-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: compactionConfigPath,
|
||||
binArgs: [compactionConfigPath, prompt],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT: 'replay',
|
||||
DSH_SNAPSHOT_FILE: compactionSessionFixture,
|
||||
NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
|
||||
},
|
||||
prepare: (cwd) => { runCwd = cwd },
|
||||
inspect: async (cwd) => {
|
||||
const logs = await persistedLogs(cwd)
|
||||
expect(logs).toHaveLength(1)
|
||||
const actual = logs[0]
|
||||
if (actual === undefined) throw new Error('compaction snapshot did not persist its session')
|
||||
const records = parseJsonl(actual.content)
|
||||
const types = records.map(record => record.type)
|
||||
expect(types.filter(type => type === 'compact/start')).toHaveLength(1)
|
||||
expect(types.filter(type => type === 'compact/summary')).toHaveLength(1)
|
||||
expect(types.filter(type => type === 'compact/end')).toHaveLength(1)
|
||||
const start = types.indexOf('compact/start')
|
||||
const summary = types.indexOf('compact/summary')
|
||||
const replacement = records.findIndex((record) => {
|
||||
if (record.type !== 'user/message') return false
|
||||
const surfaceOp = record.surfaceOp as JsonObject | undefined
|
||||
return surfaceOp?.op === 'replace'
|
||||
})
|
||||
const end = types.indexOf('compact/end')
|
||||
expect(start).toBeLessThan(summary)
|
||||
expect(summary).toBeLessThan(replacement)
|
||||
expect(replacement).toBeLessThan(end)
|
||||
const summaryRecord = records[summary]
|
||||
const summaryData = summaryRecord?.data as JsonObject | undefined
|
||||
expect(summaryData?.shadowedSeqs).toEqual(expect.arrayContaining([expect.any(Number)]))
|
||||
const final = [...records].reverse().find(record => record.type === 'assistant/message')
|
||||
expect(JSON.stringify(final)).toContain('COMPACTION RECOVERED')
|
||||
|
||||
const actualContext = contextFromLogs([actual.content])
|
||||
if (refreshing) {
|
||||
const harvested: HarvestedLog = {
|
||||
id: String(actual.header.id),
|
||||
createdAt: Number(actual.header.createdAt),
|
||||
content: actual.content,
|
||||
}
|
||||
const replacements = refreshFixtureReplacements([harvested], [expectedSession])
|
||||
expectedSession = tokenizeSessionFixtureCwd(
|
||||
stabilizeRefreshLog(actual.content, expectedSession, replacements, actualContext),
|
||||
)
|
||||
await writeFile(compactionSessionFixture, expectedSession)
|
||||
}
|
||||
const expectedContext = contextFromLogs([expectedSession])
|
||||
expect(scrubRequestHeaders(normalizeSessionLog(actual.content, actualContext)))
|
||||
.toBe(scrubRequestHeaders(normalizeSessionLog(expectedSession, expectedContext)))
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.stderr).toBe('')
|
||||
const normalized = normalizeHeadlessStream(result.stdout, runCwd)
|
||||
if (refreshing) await writeFile(compactionStreamExpected, normalized)
|
||||
expect(normalized).toBe(await readFile(compactionStreamExpected, 'utf8'))
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('logs actionable missing-credential guidance through the one-shot app', async () => {
|
||||
const streamExpected = join(credentialsScenarioDir, 'stream-json.expected.jsonl')
|
||||
let runCwd = ''
|
||||
const result = await runLoaderSmoke({
|
||||
label: 'missing-credential headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-missing-credential-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: credentialsConfigPath,
|
||||
binArgs: ['--config', credentialsConfigPath, '--output-format', 'stream-json', 'say pong'],
|
||||
binArgs: [credentialsConfigPath, 'say pong'],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
// First-run posture: no key in the environment, none under ./.dsh.
|
||||
@@ -248,22 +414,62 @@ describe('headless stream-json snapshots', () => {
|
||||
DEEPSEEK_BASE_URL: '',
|
||||
NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
|
||||
},
|
||||
// The designed failure surface: the one-shot app reports the failed turn.
|
||||
expectedExitCode: 1,
|
||||
prepare: (cwd) => { runCwd = cwd },
|
||||
})
|
||||
|
||||
// The guidance leads with the credential store — the path that keeps the
|
||||
// secret out of configuration files — and offers a literal key last.
|
||||
expect(result.stderr).toBe(
|
||||
'dsh-cli-demo: turn 1 failed at step 1: llm-deepseek: no API key for provider route "deepseek-official";'
|
||||
+ ' store DEEPSEEK_API_KEY through the credentials service (the web Models page writes it),'
|
||||
+ ' export DEEPSEEK_API_KEY in the launching environment, or — as a last resort — set a literal'
|
||||
+ ' "apiKey" in the llm-deepseek settings section\n',
|
||||
)
|
||||
// The failure reaches the caller through the stream, not stderr; the
|
||||
// recorded transcript below pins the guidance text itself, which names
|
||||
// both places a credential can come from and nothing else.
|
||||
expect(result.stderr).toBe('')
|
||||
const normalized = normalizeHeadlessStream(result.stdout, runCwd)
|
||||
if (refreshing) await writeFile(streamExpected, normalized)
|
||||
expect(normalized).toBe(await readFile(streamExpected, 'utf8'))
|
||||
// The durable failure leads with the credential store — the path that
|
||||
// keeps the secret out of configuration files — then names the launching
|
||||
// environment, and stops there: configuration carries the reference, so
|
||||
// there is no literal-key escape hatch left to offer.
|
||||
expect(normalized).toContain(
|
||||
'store DEEPSEEK_API_KEY through the credentials service (the web Models page writes it),',
|
||||
)
|
||||
expect(normalized).toContain('or export DEEPSEEK_API_KEY in the launching environment')
|
||||
expect(normalized).not.toContain('as a last resort')
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('logs actionable invalid-credential guidance through the one-shot app', async () => {
|
||||
const streamExpected = join(invalidCredentialScenarioDir, 'stream-json.expected.jsonl')
|
||||
let runCwd = ''
|
||||
const result = await runLoaderSmoke({
|
||||
label: 'invalid-credential headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-invalid-credential-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: credentialsConfigPath,
|
||||
binArgs: [credentialsConfigPath, 'say pong'],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
// A key that exists but no HTTP header can carry — the paste this
|
||||
// change exists for. Before it, `fetch` refused to build the header
|
||||
// and the turn ended on a retried ByteString TypeError.
|
||||
DEEPSEEK_API_KEY: 'sk-\u{1F600}pasted-from-a-chat-window',
|
||||
DEEPSEEK_BASE_URL: '',
|
||||
NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
|
||||
},
|
||||
prepare: (cwd) => { runCwd = cwd },
|
||||
})
|
||||
|
||||
expect(result.stderr).toBe('')
|
||||
const normalized = normalizeHeadlessStream(result.stdout, runCwd)
|
||||
if (refreshing) await writeFile(streamExpected, normalized)
|
||||
expect(normalized).toBe(await readFile(streamExpected, 'utf8'))
|
||||
// The durable failure names the reference to correct and the writer that
|
||||
// usually owns it, and stays true in a composition that mounts no Models
|
||||
// page at all.
|
||||
expect(normalized).toContain('the API key resolved from DEEPSEEK_API_KEY contains characters')
|
||||
expect(normalized).toContain('the web Models page writes it')
|
||||
// Neither the key nor the transport-level symptom it used to produce may
|
||||
// reach the user: the code point of one character is still the key.
|
||||
expect(normalized).not.toContain('pasted-from-a-chat-window')
|
||||
expect(normalized).not.toContain('ByteString')
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('logs the model default and a dynamic next-step reasoning effort', async () => {
|
||||
@@ -271,8 +477,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'reasoning effort headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-reasoning-effort-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: reasoningConfigPath,
|
||||
binArgs: ['--config', reasoningConfigPath, '--output-format', 'stream-json', 'prove dynamic reasoning effort'],
|
||||
binArgs: [reasoningConfigPath, 'prove dynamic reasoning effort'],
|
||||
tsconfigPath,
|
||||
})
|
||||
|
||||
@@ -313,16 +520,17 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'DeepSeek adapter defaults headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-deepseek-defaults-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: deepseekDefaultsConfigPath,
|
||||
binArgs: [
|
||||
'--config',
|
||||
deepseekDefaultsConfigPath,
|
||||
'--output-format',
|
||||
'stream-json',
|
||||
'return the deterministic response',
|
||||
],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
// Configuration carries only the reference; the key rides the
|
||||
// launching environment, which is the whole credential plane here.
|
||||
DEEPSEEK_API_KEY: 'snapshot-key',
|
||||
DSH_SNAPSHOT_BASE_URL: server.url,
|
||||
NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
|
||||
},
|
||||
@@ -370,8 +578,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'advanced headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-advanced-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: advancedConfigPath,
|
||||
binArgs: ['--config', advancedConfigPath, '--output-format', 'stream-json', prompt],
|
||||
binArgs: [advancedConfigPath, prompt],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT: 'replay',
|
||||
@@ -441,8 +650,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'goal tools headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-goal-tools-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: goalConfigPath,
|
||||
binArgs: ['--config', goalConfigPath, '--output-format', 'stream-json', prompt],
|
||||
binArgs: [goalConfigPath, prompt],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT: 'replay',
|
||||
@@ -470,19 +680,11 @@ describe('headless stream-json snapshots', () => {
|
||||
const probeContent = probeMessage?.content as JsonObject[] | undefined
|
||||
expect(probeContent?.[0]?.isError).toBe(true)
|
||||
expect((probeData?.error as JsonObject | undefined)?.code).toBe('GOAL_NOT_FOUND')
|
||||
const goalChanges = records.filter((record) => {
|
||||
if (record.type !== 'user/message') return false
|
||||
const data = record.data as JsonObject | undefined
|
||||
const source = data?.source as JsonObject | undefined
|
||||
const change = source?.change as JsonObject | undefined
|
||||
return source?.kind === 'goal' && change?.kind === 'goal/change'
|
||||
})
|
||||
const goalChanges = records.filter(record => record.type === 'goal/change')
|
||||
expect(goalChanges).toHaveLength(1)
|
||||
const data = goalChanges[0]?.data as JsonObject | undefined
|
||||
const source = data?.source as JsonObject | undefined
|
||||
const change = source?.change as JsonObject | undefined
|
||||
const goal = change?.goal as JsonObject | undefined
|
||||
expect(change?.operation).toBe('create')
|
||||
const goal = data?.goal as JsonObject | undefined
|
||||
expect(data?.operation).toBe('create')
|
||||
expect(goal).toMatchObject({
|
||||
objective: 'Finish the headless goal-tool snapshot proof',
|
||||
phase: 'active',
|
||||
@@ -505,8 +707,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'Ralph loop headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-ralph-loop-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: ralphConfigPath,
|
||||
binArgs: ['--config', ralphConfigPath, '--output-format', 'stream-json', prompt],
|
||||
binArgs: [ralphConfigPath, prompt],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT: 'replay',
|
||||
@@ -586,8 +789,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'headless persistent PTY snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-pty-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: ptyConfigPath,
|
||||
binArgs: ['--config', ptyConfigPath, '--output-format', 'stream-json', prompt],
|
||||
binArgs: [ptyConfigPath, prompt],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT: 'replay',
|
||||
|
||||
Reference in New Issue
Block a user