Merge remote-tracking branch 'origin/master' into jsonl-packed-chunk-rows
Master removed the stdio demo (#702c8cc30) — accept the deletion; this branch's packChunks passthrough survives in acp-demo (auto-merged), and cli-demo/tui-demo arrived from master without one (the follow-up snapshot PR decides which demos expose the switch). Generated catalogs regenerated over merged sources; the hand-written session.md durability paragraph re-weaves this branch's lossless-encoding wording with master's invariant- companion sentence.
This commit is contained in:
@@ -152,6 +152,7 @@ async function handlePrompt(id: number | string): Promise<void> {
|
||||
mode: process.env.DSH_SNAPSHOT,
|
||||
override: process.env.DSH_SNAPSHOT_OVERRIDE ?? null,
|
||||
childFiles: process.env.DSH_SNAPSHOT_CHILD_FILES ?? null,
|
||||
spillRoot: process.env.DSH_SNAPSHOT_SPILL_ROOT ?? null,
|
||||
})}`)
|
||||
}
|
||||
if (behavior.echoWorkspace === true) {
|
||||
@@ -299,7 +300,10 @@ function flushLogsAndExit(): void {
|
||||
`setTimeout(() => process.stdout.write(${JSON.stringify(`${frame}\n`)}), 50)`,
|
||||
`setTimeout(() => process.stderr.write(${JSON.stringify('late inherited stderr\n')}), 75)`,
|
||||
].join(';')
|
||||
spawn(process.execPath, ['-e', code], { stdio: ['ignore', 1, 2] }).unref()
|
||||
spawn(process.execPath, ['-e', code], {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'inherit', 'inherit'],
|
||||
}).unref()
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { delimiter, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterAll, describe, expect, it, vi } from 'vitest'
|
||||
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
|
||||
import { runScenario, type AgentUnderTest, type InputStep } from '../src/harness.ts'
|
||||
import { runScenario, snapshotSpillRoot, type AgentUnderTest, type InputStep } from '../src/harness.ts'
|
||||
import { launchAcpTestAgent } from '../src/launcher.ts'
|
||||
|
||||
const fsControl = vi.hoisted(() => ({ cleanupFailure: undefined as Error | undefined }))
|
||||
@@ -60,6 +60,24 @@ async function scenario(behavior: object): Promise<{ dir: string; fixtureFile: s
|
||||
|
||||
const boot: InputStep[] = [{ op: 'initialize' }, { op: 'newSession' }]
|
||||
|
||||
it('keeps scenario-owned snapshot spill root length stable across platforms', () => {
|
||||
const fixtureFile = '/fixtures/scenario/session.jsonl'
|
||||
const posix = snapshotSpillRoot(fixtureFile, 'linux')
|
||||
const windows = snapshotSpillRoot(fixtureFile, 'win32')
|
||||
expect(posix).toMatch(/^\/tmp\/dsh-acp-snap-[0-9a-f]{9}$/)
|
||||
expect(windows).toMatch(/^\/t\/dsh-acp-snap-[0-9a-f]{9}$/)
|
||||
expect(windows.length + 2).toBe(posix.length)
|
||||
})
|
||||
|
||||
function environmentEcho(rawStdout: string): Record<string, unknown> {
|
||||
const frames = rawStdout.trim().split('\n')
|
||||
.map(line => JSON.parse(line) as { params?: { update?: { content?: { text?: unknown } } } })
|
||||
const text = frames.map(frame => frame.params?.update?.content?.text)
|
||||
.find(value => typeof value === 'string' && value.startsWith('env:'))
|
||||
if (typeof text !== 'string') throw new Error('fake ACP agent did not echo its environment')
|
||||
return JSON.parse(text.slice('env:'.length)) as Record<string, unknown>
|
||||
}
|
||||
|
||||
describe('runScenario', () => {
|
||||
it('surfaces an asynchronous child spawn failure through startup and close', async () => {
|
||||
const { dir } = await scenario({})
|
||||
@@ -134,6 +152,9 @@ describe('runScenario', () => {
|
||||
update.sessionUpdate === 'agent_message_chunk'
|
||||
&& update.content.type === 'text'
|
||||
&& update.content.text === 'late inherited stdout')
|
||||
// Arm rejection handling before close may exhaust the stream; the later assertion still
|
||||
// observes the original promise and turns a missing inherited frame into the test failure.
|
||||
void lateUpdate.catch(() => undefined)
|
||||
|
||||
await launched.close()
|
||||
|
||||
@@ -171,6 +192,97 @@ describe('runScenario', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves the child error when the requested signal sets an exit marker', async () => {
|
||||
const { dir } = await scenario({})
|
||||
const launched = launchAcpTestAgent({ agent: AGENT, cwd: dir })
|
||||
await launched.spawned
|
||||
|
||||
const childFailure = Object.assign(new Error('signal failed as the child exited'), { code: 'EPERM' })
|
||||
const originalKill = launched.child.kill.bind(launched.child)
|
||||
const kill = vi.spyOn(launched.child, 'kill').mockImplementation((signal) => {
|
||||
expect(signal).toBe('SIGTERM')
|
||||
originalKill('SIGKILL')
|
||||
Object.defineProperty(launched.child, 'signalCode', { configurable: true, enumerable: true, writable: true, value: 'SIGTERM' })
|
||||
return true
|
||||
})
|
||||
try {
|
||||
launched.child.emit('error', childFailure)
|
||||
await expect(launched.close('SIGTERM')).rejects.toBe(childFailure)
|
||||
expect(kill).toHaveBeenCalledOnce()
|
||||
} finally {
|
||||
kill.mockRestore()
|
||||
if (launched.child.exitCode === null && launched.child.signalCode === null) originalKill('SIGKILL')
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves the child error when the requested signal publishes its exit marker later', async () => {
|
||||
const { dir } = await scenario({})
|
||||
const launched = launchAcpTestAgent({ agent: AGENT, cwd: dir })
|
||||
await launched.spawned
|
||||
|
||||
const childFailure = Object.assign(new Error('signal failed before the delayed exit marker'), { code: 'EPERM' })
|
||||
const originalKill = launched.child.kill.bind(launched.child)
|
||||
const kill = vi.spyOn(launched.child, 'kill').mockImplementation((signal) => {
|
||||
expect(signal).toBe('SIGTERM')
|
||||
setTimeout(() => { originalKill('SIGKILL') }, 10)
|
||||
return true
|
||||
})
|
||||
try {
|
||||
launched.child.emit('error', childFailure)
|
||||
await expect(launched.close('SIGTERM')).rejects.toBe(childFailure)
|
||||
expect(kill).toHaveBeenCalledOnce()
|
||||
} finally {
|
||||
kill.mockRestore()
|
||||
if (launched.child.exitCode === null && launched.child.signalCode === null) originalKill('SIGKILL')
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves the child error when fallback refusal races with an exit marker', async () => {
|
||||
const { dir } = await scenario({})
|
||||
const launched = launchAcpTestAgent({ agent: AGENT, cwd: dir })
|
||||
await launched.spawned
|
||||
|
||||
const childFailure = Object.assign(new Error('signal failed while the child exited'), { code: 'EPERM' })
|
||||
const originalKill = launched.child.kill.bind(launched.child)
|
||||
const kill = vi.spyOn(launched.child, 'kill').mockImplementation((signal) => {
|
||||
if (signal === 'SIGTERM') return true
|
||||
originalKill('SIGKILL')
|
||||
Object.defineProperty(launched.child, 'signalCode', { configurable: true, enumerable: true, writable: true, value: 'SIGKILL' })
|
||||
return false
|
||||
})
|
||||
try {
|
||||
launched.child.emit('error', childFailure)
|
||||
await expect(launched.close('SIGTERM')).rejects.toBe(childFailure)
|
||||
expect(kill).toHaveBeenNthCalledWith(1, 'SIGTERM')
|
||||
expect(kill).toHaveBeenNthCalledWith(2, 'SIGKILL')
|
||||
} finally {
|
||||
kill.mockRestore()
|
||||
if (launched.child.exitCode === null && launched.child.signalCode === null) originalKill('SIGKILL')
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves the child error after accepted fallback termination drains', async () => {
|
||||
const { dir } = await scenario({})
|
||||
const launched = launchAcpTestAgent({ agent: AGENT, cwd: dir })
|
||||
await launched.spawned
|
||||
|
||||
const childFailure = Object.assign(new Error('requested signal failed before fallback'), { code: 'EPERM' })
|
||||
const originalKill = launched.child.kill.bind(launched.child)
|
||||
const kill = vi.spyOn(launched.child, 'kill').mockImplementation((signal) => {
|
||||
if (signal === 'SIGTERM') return true
|
||||
return originalKill('SIGKILL')
|
||||
})
|
||||
try {
|
||||
launched.child.emit('error', childFailure)
|
||||
await expect(launched.close('SIGTERM')).rejects.toBe(childFailure)
|
||||
expect(kill).toHaveBeenNthCalledWith(1, 'SIGTERM')
|
||||
expect(kill).toHaveBeenNthCalledWith(2, 'SIGKILL')
|
||||
} finally {
|
||||
kill.mockRestore()
|
||||
if (launched.child.exitCode === null && launched.child.signalCode === null) originalKill('SIGKILL')
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects promptly when fallback termination emits an error', async () => {
|
||||
const { dir } = await scenario({})
|
||||
const launched = launchAcpTestAgent({ agent: AGENT, cwd: dir })
|
||||
@@ -285,7 +397,11 @@ describe('runScenario', () => {
|
||||
expect(result.sessionLogs[0]?.createdAt).toBe(42)
|
||||
expect(result.sessionLogs[0]?.content).toContain('turn/start')
|
||||
// The harvested log embeds the run's REAL temp cwd (template-substituted).
|
||||
expect(result.sessionLogs[0]?.content).toContain(result.cwd)
|
||||
// The cwd is JSON-encoded in the log line, so compare the parsed field
|
||||
// rather than substring-matching a raw path (which breaks when the path
|
||||
// separator is escaped inside JSON text on Windows).
|
||||
const sessionLine = result.sessionLogs[0]?.content.split('\n').find(l => l.includes('"type":"session"')) ?? '{}'
|
||||
expect((JSON.parse(sessionLine) as { cwd?: string }).cwd).toBe(result.cwd)
|
||||
})
|
||||
|
||||
it('forwards override/child fixture paths into the child env and captures stderr', { timeout: 20_000 }, async () => {
|
||||
@@ -306,7 +422,33 @@ describe('runScenario', () => {
|
||||
expect(result.stderr).toContain('fake bin booted')
|
||||
expect(result.rawStdout).toContain('replay.override.json')
|
||||
// Child paths ride one env var, joined with the platform delimiter.
|
||||
expect(result.rawStdout).toContain(JSON.stringify(childFiles.join(delimiter)).slice(1, -1))
|
||||
// Parse the fake bin's env-probe chunk rather than substring-matching a
|
||||
// JSON-encoded path (the escaping breaks raw-substring compares on Windows).
|
||||
const envChunk = result.rawStdout.split('\n')
|
||||
.map(l => l.trim())
|
||||
.filter(l => l.length > 0)
|
||||
.map(l => JSON.parse(l) as { params?: { update?: { content?: { text?: string } } } })
|
||||
.find(f => f.params?.update?.content?.text?.startsWith('env:'))
|
||||
const env = JSON.parse((envChunk?.params?.update?.content?.text ?? 'env:{}').slice('env:'.length)) as {
|
||||
childFiles: string | null
|
||||
}
|
||||
expect(env.childFiles).toBe(childFiles.join(delimiter))
|
||||
})
|
||||
|
||||
it('gives concurrent scenarios distinct equal-length spill roots', { timeout: 20_000 }, async () => {
|
||||
const [first, second] = await Promise.all([scenario({ echoEnv: true }), scenario({ echoEnv: true })])
|
||||
const results = await Promise.all([first, second].map(({ fixtureFile }) => runScenario(
|
||||
{ steps: [...boot, { op: 'prompt', text: 'env?' }] },
|
||||
{ agent: AGENT, mode: 'replay', fixtureFile },
|
||||
)))
|
||||
const roots = results.map(result => environmentEcho(result.rawStdout).spillRoot)
|
||||
expect(roots.every(root => typeof root === 'string')).toBe(true)
|
||||
expect(new Set(roots).size).toBe(2)
|
||||
expect((roots[0] as string).length).toBe((roots[1] as string).length)
|
||||
expect(roots).toEqual([
|
||||
snapshotSpillRoot(first.fixtureFile),
|
||||
snapshotSpillRoot(second.fixtureFile),
|
||||
])
|
||||
})
|
||||
|
||||
it('seeds the workspace dir into the temp cwd before the run', { timeout: 20_000 }, async () => {
|
||||
@@ -334,6 +476,21 @@ describe('runScenario', () => {
|
||||
expect(result.rawStdout.indexOf('thinking about it')).toBeLessThan(result.rawStdout.indexOf('cancelled'))
|
||||
})
|
||||
|
||||
it('promptAndWaitForAgentMessage keeps the app live through a matching later update', { timeout: 20_000 }, async () => {
|
||||
const { fixtureFile } = await scenario({ prompt: 'respond' })
|
||||
const result = await runScenario(
|
||||
{
|
||||
steps: [...boot, {
|
||||
op: 'promptAndWaitForAgentMessage',
|
||||
text: 'go',
|
||||
waitForText: 'thinking about it',
|
||||
}],
|
||||
},
|
||||
{ agent: AGENT, mode: 'replay', fixtureFile },
|
||||
)
|
||||
expect(result.rawStdout).toContain('thinking about it')
|
||||
})
|
||||
|
||||
it('promptAndCancel can bracket cancellation with tool-call updates', { timeout: 20_000 }, async () => {
|
||||
const { fixtureFile } = await scenario({
|
||||
prompt: 'hang-until-cancel',
|
||||
@@ -443,6 +600,7 @@ describe('runScenario', () => {
|
||||
|
||||
it.each([
|
||||
[{ op: 'prompt', text: 'x' }, /prompt before newSession/],
|
||||
[{ op: 'promptAndWaitForAgentMessage', text: 'x', waitForText: 'later' }, /promptAndWaitForAgentMessage before newSession/],
|
||||
[{ op: 'promptExpectError', text: 'x' }, /promptExpectError before newSession/],
|
||||
[{ op: 'promptAndCancel', text: 'x' }, /promptAndCancel before newSession/],
|
||||
[{ op: 'cancel' }, /cancel before newSession/],
|
||||
|
||||
@@ -44,6 +44,56 @@ describe('normalizeStdout', () => {
|
||||
expect(out).not.toContain(ctx.sessionIds[0] as string)
|
||||
})
|
||||
|
||||
it('canonicalizes only cwd-rooted path separators', () => {
|
||||
const windowsCtx: NormalizeContext = {
|
||||
sessionIds: [],
|
||||
cwd: String.raw`C:\Users\runner\AppData\Local\Temp\acp-snapshot`,
|
||||
}
|
||||
const raw = JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'session/update',
|
||||
params: {
|
||||
path: `${windowsCtx.cwd}\\nested\\proof.txt`,
|
||||
regex: String.raw`\d+\w+`,
|
||||
command: String.raw`printf "\\n"`,
|
||||
},
|
||||
})
|
||||
const frame = JSON.parse(normalizeStdout(raw, windowsCtx)) as {
|
||||
params: { path: string; regex: string; command: string }
|
||||
}
|
||||
expect(frame.params).toEqual({
|
||||
path: '{{cwd}}/nested/proof.txt',
|
||||
regex: String.raw`\d+\w+`,
|
||||
command: String.raw`printf "\\n"`,
|
||||
})
|
||||
})
|
||||
|
||||
it('canonicalizes generated relative path fields and text markers without rewriting other text', () => {
|
||||
const raw = JSON.stringify({
|
||||
path: String.raw`nested\AGENTS.md`,
|
||||
content: String.raw`<path>.\nested\task.txt</path>
|
||||
Additional instructions from: nested\AGENTS.md`,
|
||||
regex: String.raw`\d+\w+`,
|
||||
})
|
||||
const frame = JSON.parse(normalizeStdout(raw, { sessionIds: [], cwd: '/unused' })) as {
|
||||
path: string
|
||||
content: string
|
||||
regex: string
|
||||
}
|
||||
expect(frame).toEqual({
|
||||
path: 'nested/AGENTS.md',
|
||||
content: '<path>./nested/task.txt</path>\nAdditional instructions from: nested/AGENTS.md',
|
||||
regex: String.raw`\d+\w+`,
|
||||
})
|
||||
})
|
||||
|
||||
it('can preserve native cwd-rooted separators for a platform golden', () => {
|
||||
const windowsCtx: NormalizeContext = { sessionIds: [], cwd: String.raw`C:\work\snapshot` }
|
||||
const raw = JSON.stringify({ path: `${windowsCtx.cwd}\\nested\\proof.txt` })
|
||||
const frame = JSON.parse(normalizeStdout(raw, windowsCtx, { cwdPathMode: 'native' })) as { path: string }
|
||||
expect(frame.path).toBe(String.raw`{{cwd}}\nested\proof.txt`)
|
||||
})
|
||||
|
||||
it('scrubs a stray UUID not in the known list', () => {
|
||||
const raw = JSON.stringify({ jsonrpc: '2.0', method: 'x', params: { id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' } })
|
||||
expect(normalizeStdout(raw, ctx)).toContain('{{sessionId}}')
|
||||
@@ -55,6 +105,24 @@ describe('normalizeStdout', () => {
|
||||
expect(out).not.toContain('"id"')
|
||||
})
|
||||
|
||||
it('stabilizes the timestamp carried by session title updates', () => {
|
||||
const raw = JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'session/update',
|
||||
params: {
|
||||
sessionId: ctx.sessionIds[0],
|
||||
update: {
|
||||
sessionUpdate: 'session_info_update',
|
||||
title: 'Stable title',
|
||||
updatedAt: '2026-07-20T17:03:13.689Z',
|
||||
},
|
||||
},
|
||||
})
|
||||
const out = normalizeStdout(raw, ctx)
|
||||
expect(out).toContain('"updatedAt":"{{updatedAt}}"')
|
||||
expect(out).not.toContain('2026-07-20T17:03:13.689Z')
|
||||
})
|
||||
|
||||
it('throws on a non-JSON stdout line (the purity check)', () => {
|
||||
const raw = `${JSON.stringify({ jsonrpc: '2.0', id: 1 })}\noops a log leaked\n`
|
||||
expect(() => normalizeStdout(raw, ctx)).toThrow()
|
||||
@@ -139,6 +207,48 @@ describe('normalizeSessionLog', () => {
|
||||
expect(out).not.toContain('/tmp/dsh-acp-snapshot-spill')
|
||||
})
|
||||
|
||||
it('scrubs scenario-owned snapshot spill paths', () => {
|
||||
const ev = JSON.stringify({
|
||||
type: 'tool/result', seq: 2, time: 5,
|
||||
data: {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: 'Full formatted result stored at: /tmp/dsh-acp-snap-012345678/session-c22bc3f1d2af/8a7b6c5d4e3f-bash.txt. Use read with offset/limit, or grep this path to search within it.',
|
||||
}],
|
||||
},
|
||||
})
|
||||
const out = normalizeSessionLog(`${header({ cwd: ctx.cwd })}\n${ev}\n`, ctx)
|
||||
expect(out).toContain('{{spillLocator:bash.txt}}')
|
||||
expect(out).not.toContain('/tmp/dsh-acp-snap-012345678')
|
||||
})
|
||||
|
||||
it('scrubs scenario-owned snapshot spill paths with Windows drive and separators', () => {
|
||||
const ev = JSON.stringify({
|
||||
type: 'tool/result', seq: 2, time: 5,
|
||||
data: {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: String.raw`Full formatted result stored at: C:\t\dsh-acp-snap-012345678\session-c22bc3f1d2af\8a7b6c5d4e3f-bash.txt. Use read with offset/limit, or grep this path to search within it.`,
|
||||
}],
|
||||
},
|
||||
})
|
||||
const out = normalizeSessionLog(`${header({ cwd: ctx.cwd })}\n${ev}\n`, ctx)
|
||||
expect(out).toContain('{{spillLocator:bash.txt}}')
|
||||
expect(out).not.toContain('C:\\t\\dsh-acp-snap-012345678')
|
||||
})
|
||||
|
||||
it('shares cwd-rooted path handling with stdout normalization', () => {
|
||||
const windowsCtx: NormalizeContext = { sessionIds: [], cwd: String.raw`C:\work\snapshot` }
|
||||
const ev = JSON.stringify({
|
||||
type: 'tool/result', seq: 2, time: 5,
|
||||
data: { path: `${windowsCtx.cwd}\\nested\\proof.txt` },
|
||||
})
|
||||
expect(normalizeSessionLog(`${header({ cwd: windowsCtx.cwd })}\n${ev}\n`, windowsCtx))
|
||||
.toContain('{{cwd}}/nested/proof.txt')
|
||||
expect(normalizeSessionLog(`${header({ cwd: windowsCtx.cwd })}\n${ev}\n`, windowsCtx, { cwdPathMode: 'native' }))
|
||||
.toContain(String.raw`{{cwd}}\\nested\\proof.txt`)
|
||||
})
|
||||
|
||||
it('scrubs the session id in the header', () => {
|
||||
const out = normalizeSessionLog(`${header({ id: ctx.sessionIds[0] })}\n`, ctx)
|
||||
expect(out).toContain('{{sessionId}}')
|
||||
|
||||
@@ -15,9 +15,11 @@ import {
|
||||
normalizedToolSchemas,
|
||||
parseToolSchemasSnapshot,
|
||||
refreshFixtureReplacements,
|
||||
scenarioSkipped,
|
||||
sessionFixtureNames,
|
||||
restorePinnedToolSchemas,
|
||||
stabilizeRefreshLog,
|
||||
stdoutExpectedVariants,
|
||||
unknownToolCallIds,
|
||||
} from '../src/suite.ts'
|
||||
|
||||
@@ -230,6 +232,48 @@ describe('sessionFixtureNames', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('stdoutExpectedVariants', () => {
|
||||
const scenario: Scenario = {
|
||||
name: 'windows-native',
|
||||
hasModelTurn: true,
|
||||
recorded: true,
|
||||
pinsNativeWindowsStdout: true,
|
||||
}
|
||||
|
||||
it('adds the native sidecar after the shared golden on Windows', () => {
|
||||
expect(stdoutExpectedVariants(scenario, 'win32')).toEqual([
|
||||
{ file: 'stdout.expected.jsonl', cwdPathMode: 'canonical' },
|
||||
{ file: 'stdout.expected.windows.jsonl', cwdPathMode: 'native' },
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps only the shared golden on other platforms or without the declaration', () => {
|
||||
expect(stdoutExpectedVariants(scenario, 'linux')).toEqual([
|
||||
{ file: 'stdout.expected.jsonl', cwdPathMode: 'canonical' },
|
||||
])
|
||||
expect(stdoutExpectedVariants({ ...scenario, pinsNativeWindowsStdout: false }, 'win32')).toEqual([
|
||||
{ file: 'stdout.expected.jsonl', cwdPathMode: 'canonical' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('scenarioSkipped', () => {
|
||||
const authored: Scenario = { name: 'authored', hasModelTurn: true, recorded: false }
|
||||
const posix: Scenario = { name: 'posix-cancel', hasModelTurn: true, recorded: false, posixOnly: true }
|
||||
|
||||
it('skips authored scenarios only while recording', () => {
|
||||
expect(scenarioSkipped(authored, true, 'linux')).toBe(true)
|
||||
expect(scenarioSkipped(authored, false, 'linux')).toBe(false)
|
||||
})
|
||||
|
||||
it('skips posixOnly scenarios on Windows and nowhere else', () => {
|
||||
expect(scenarioSkipped(posix, false, 'win32')).toBe(true)
|
||||
expect(scenarioSkipped(posix, false, 'linux')).toBe(false)
|
||||
expect(scenarioSkipped(posix, false, 'darwin')).toBe(false)
|
||||
expect(scenarioSkipped(authored, false, 'win32')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('fixtureContext', () => {
|
||||
it('reads the fixture header id and cwd', () => {
|
||||
const ctx = fixtureContext('{"type":"session","id":"abc","cwd":"/rec"}\n{"type":"turn/start"}\n')
|
||||
@@ -407,6 +451,36 @@ describe('refreshFixtureReplacements', () => {
|
||||
})
|
||||
|
||||
describe('stabilizeRefreshLog', () => {
|
||||
it('aligns volatile times across a newly inserted log event', () => {
|
||||
const fresh = [
|
||||
'{"type":"session","id":"same","createdAt":200}',
|
||||
'{"type":"turn/start","seq":0,"time":21}',
|
||||
'{"type":"user/message","seq":1,"time":22}',
|
||||
'{"type":"session/title","seq":2,"time":999}',
|
||||
'{"type":"step/start","seq":3,"time":1000}',
|
||||
'{"type":"request/header","seq":4,"time":1001}',
|
||||
'',
|
||||
].join('\n')
|
||||
const existing = [
|
||||
'{"type":"session","id":"same","createdAt":100}',
|
||||
'{"type":"turn/start","seq":0,"time":11}',
|
||||
'{"type":"user/message","seq":1,"time":12}',
|
||||
'{"type":"step/start","seq":2,"time":13}',
|
||||
'{"type":"request/header","seq":3,"time":14}',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
expect(stabilizeRefreshLog(fresh, existing, [])).toBe([
|
||||
'{"type":"session","id":"same","createdAt":100}',
|
||||
'{"type":"turn/start","seq":0,"time":11}',
|
||||
'{"type":"user/message","seq":1,"time":12}',
|
||||
'{"type":"session/title","seq":2,"time":12}',
|
||||
'{"type":"step/start","seq":3,"time":13}',
|
||||
'{"type":"request/header","seq":4,"time":14}',
|
||||
'',
|
||||
].join('\n'))
|
||||
})
|
||||
|
||||
it('keeps volatile fixture fields while preserving fresh meaningful payloads', () => {
|
||||
const fresh = [
|
||||
'{"type":"session","id":"new-child","createdAt":200,"cwd":"/new","parentSession":"new-parent","seedLength":1}',
|
||||
|
||||
Reference in New Issue
Block a user