feat(session-title): integrate queries ACP and TUI

This commit is contained in:
Tianyi Cui
2026-07-21 01:53:37 +08:00
parent 58dc5f94de
commit f71f96d292
131 changed files with 6142 additions and 5771 deletions

View File

@@ -41,6 +41,8 @@ export {
} from './normalize.ts'
export {
defineAcpSnapshotSuite,
refreshFixtureReplacements,
stabilizeRefreshLog,
type Scenario,
type SnapshotSuiteOptions,
} from './suite.ts'

View File

@@ -11,6 +11,7 @@ const CWD = '{{cwd}}'
const SYSTEM = '{{system}}'
const TOOLS = '{{tools}}'
const MESSAGE_PREFIX = '{{messagePrefix}}'
const UPDATED_AT = '{{updatedAt}}'
/** A UUID v4 string, the shape `randomUUID()` produces for session ids. */
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi
@@ -85,6 +86,8 @@ export function normalizeStdout(rawStdout: string, ctx: NormalizeContext): strin
if ('id' in frame && frame.id !== undefined && frame.id !== null) {
frame.id = stableId(frame.id)
}
const update = (frame.params as { update?: Record<string, unknown> } | undefined)?.update
if (update?.sessionUpdate === 'session_info_update') update.updatedAt = UPDATED_AT
return scrubValue(frame, ctx) as Record<string, unknown>
})
return frames.map(f => JSON.stringify(f)).join('\n') + '\n'

View File

@@ -420,8 +420,21 @@ export function stabilizeRefreshLog(fresh: string, existing: string, replacement
for (const { from, to } of replacements) stable = stable.split(from).join(to)
const existingRecords = parseJsonlRecords(existing)
const records = parseJsonlRecords(stable)
let existingIndex = 0
let previousEventTime: unknown
for (let i = 0; i < records.length; i++) {
preserveFixtureVolatiles(records[i] as Record<string, unknown>, existingRecords[i])
const record = records[i] as Record<string, unknown>
const existingRecord = existingRecords[existingIndex]
const insertedTitle = record.type === 'session/title' && existingRecord?.type !== 'session/title'
if (insertedTitle) {
/* v8 ignore next -- a title is turn-enclosed, so a preceding event time exists in every valid fixture. */
if (typeof previousEventTime !== 'number') throw new Error('acp-snapshot: inserted title has no preceding event time')
record.time = previousEventTime
} else {
preserveFixtureVolatiles(record, existingRecord)
existingIndex += 1
}
if (typeof record.time === 'number') previousEventTime = record.time
}
return records.map(record => JSON.stringify(record)).join('\n') + '\n'
}

View File

@@ -55,6 +55,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()

View File

@@ -407,6 +407,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}',