|
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
import { afterAll, describe, expect, it, vi } from 'vitest'
|
|
|
import type { Context } from 'cordis'
|
|
|
import { agentEvents } from '@deepseek-ai/dsh-agent'
|
|
|
+import { COMPACT_CHECKPOINT_SOURCE } from '@deepseek-ai/dsh-compact'
|
|
|
import { createUserMessage, CallId, type ContentBlock , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
|
|
|
import type {} from '@deepseek-ai/dsh-llm-retry'
|
|
|
import { SessionId, type JsonValue, type Session, type SessionEvent } from '@deepseek-ai/dsh-session'
|
|
|
@@ -50,6 +51,7 @@ const CHECKPOINTS = [
|
|
|
'surface-before-compaction',
|
|
|
'surface-after-compaction-narrow',
|
|
|
'surface-after-compaction-wide',
|
|
|
+ 'surface-replayed-compaction',
|
|
|
'model-selector',
|
|
|
'model-selector-filtered',
|
|
|
'model-switching',
|
|
|
@@ -181,6 +183,67 @@ function appendToolResult(
|
|
|
}, { surfaceOp: 'append' })
|
|
|
}
|
|
|
|
|
|
+/** Frozen clock for the compaction fixtures; see the live scenario for why. */
|
|
|
+const COMPACTION_FIXTURE_TIME = new Date(2026, 6, 21, 14, 40, 0).getTime()
|
|
|
+
|
|
|
+/** The surface range a compaction checkpoint replaces, with its provenance. */
|
|
|
+interface CompactionRange {
|
|
|
+ start: number
|
|
|
+ end: number
|
|
|
+ sources: number[]
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Append one prompt / tool-call / tool-result step, the history a compaction
|
|
|
+ * shadows on the model surface and the transcript must keep showing. The prompt
|
|
|
+ * text is rendered verbatim; the tool card's body comes from `bash`'s static
|
|
|
+ * presenter, so the fixtures pin that the shadowed step's card survives rather
|
|
|
+ * than the result content below.
|
|
|
+ */
|
|
|
+function appendPreCompactionLog(session: Session): CompactionRange {
|
|
|
+ const user = session.append('user/message', createUserMessage({
|
|
|
+ content: [{ type: 'text', text: 'Old prompt with a long line that exercises wrapping and stays visible after compaction.' }],
|
|
|
+ source: { kind: 'user' },
|
|
|
+ }), { surfaceOp: 'append' })
|
|
|
+ const assistant = session.append('assistant/message', {
|
|
|
+ turn: 1,
|
|
|
+ step: 1,
|
|
|
+ message: createMessage({
|
|
|
+ role: 'assistant',
|
|
|
+ content: [{ type: 'tool-call', id: CallId('old-tool'), name: 'bash', arguments: '{}' }],
|
|
|
+ source: {
|
|
|
+ kind: 'model',
|
|
|
+ ...{ provider: 'mock', model: 'deepseek-v4-flash' },
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ }, { surfaceOp: 'append' })
|
|
|
+ session.append('tool/call', { turn: 1, step: 1, callId: CallId('old-tool'), name: 'bash', arguments: '{}' })
|
|
|
+ const result = session.append('tool/result', {
|
|
|
+ turn: 1,
|
|
|
+ step: 1,
|
|
|
+ message: createToolResultMessage({
|
|
|
+ callId: CallId('old-tool'),
|
|
|
+ content: [{ type: 'text', text: 'shadowed step tool output' }],
|
|
|
+ isError: false,
|
|
|
+ }),
|
|
|
+ }, { surfaceOp: 'append' })
|
|
|
+ return { start: user.seq, end: result.seq, sources: [user.seq, assistant.seq, result.seq] }
|
|
|
+}
|
|
|
+
|
|
|
+/** Land a compaction: replace the range with the framed model-only checkpoint. */
|
|
|
+function appendCompactionCheckpoint(session: Session, range: CompactionRange): void {
|
|
|
+ session.append('user/message', createUserMessage({
|
|
|
+ content: [{
|
|
|
+ type: 'text',
|
|
|
+ text: '<context_checkpoint>\nModel-only summary payload that must never reach the transcript.\n</context_checkpoint>',
|
|
|
+ }],
|
|
|
+ source: COMPACT_CHECKPOINT_SOURCE,
|
|
|
+ }), {
|
|
|
+ surfaceOp: { op: 'replace', start: range.start, end: range.end },
|
|
|
+ sourceEventSeqs: range.sources,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
function visualTool(
|
|
|
name: string,
|
|
|
call: NonNullable<ToolDefinition['presentCall']>,
|
|
|
@@ -684,61 +747,22 @@ describe('TUI terminal-state snapshots', () => {
|
|
|
await disposeSnapshot(harness)
|
|
|
})
|
|
|
|
|
|
- it('pins compaction surface replacement and narrow-to-wide reflow', async () => {
|
|
|
+ it('pins preserved history, the compaction marker, and narrow-to-wide reflow', async () => {
|
|
|
// Freeze the clock: the timing header hides zero-duration buckets, so a
|
|
|
// real-clock millisecond tick between the fixture appends and the render
|
|
|
// would flip `Tools 0.0s` in and out of the pinned header.
|
|
|
- const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(new Date(2026, 6, 21, 14, 40, 0).getTime())
|
|
|
- let replacementStart = 0
|
|
|
- let replacementEnd = 0
|
|
|
- let replacementSources: number[] = []
|
|
|
+ const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(COMPACTION_FIXTURE_TIME)
|
|
|
+ // The awaited setup always invokes beforeMount, so the range the checkpoint
|
|
|
+ // replaces is assigned by the time the appends below need it.
|
|
|
+ let compacted!: CompactionRange
|
|
|
const harness = await setupSnapshot({
|
|
|
tools: ADVANCED_CARD_TOOLS,
|
|
|
- beforeMount(session) {
|
|
|
- const user = session.append('user/message', createUserMessage({
|
|
|
- content: [{ type: 'text', text: 'Old prompt with a long line that exercises wrapping before compaction.' }],
|
|
|
- source: { kind: 'user' },
|
|
|
- }), { surfaceOp: 'append' })
|
|
|
- const assistant = session.append('assistant/message', {
|
|
|
- turn: 1,
|
|
|
- step: 1,
|
|
|
- message: createMessage({
|
|
|
- role: 'assistant',
|
|
|
- content: [{ type: 'tool-call', id: CallId('old-tool'), name: 'bash', arguments: '{}' }],
|
|
|
- source: {
|
|
|
- kind: 'model',
|
|
|
- ...{ provider: 'mock', model: 'deepseek-v4-flash' },
|
|
|
- },
|
|
|
- }),
|
|
|
- }, { surfaceOp: 'append' })
|
|
|
- session.append('tool/call', { turn: 1, step: 1, callId: CallId('old-tool'), name: 'bash', arguments: '{}' })
|
|
|
- const result = session.append('tool/result', {
|
|
|
- turn: 1,
|
|
|
- step: 1,
|
|
|
- message: createToolResultMessage({
|
|
|
- callId: CallId('old-tool'),
|
|
|
- content: [{ type: 'text', text: 'obsolete output that must disappear' }],
|
|
|
- isError: false,
|
|
|
- }),
|
|
|
- }, { surfaceOp: 'append' })
|
|
|
- replacementStart = user.seq
|
|
|
- replacementEnd = result.seq
|
|
|
- replacementSources = [user.seq, assistant.seq, result.seq]
|
|
|
- },
|
|
|
+ beforeMount(session) { compacted = appendPreCompactionLog(session) },
|
|
|
}, { columns: 80, rows: 24 })
|
|
|
await checkpoint('surface-before-compaction', harness.terminal, { includeScrollback: true })
|
|
|
|
|
|
await renderAfter(harness, () => {
|
|
|
- harness.session.append('user/message', createUserMessage({
|
|
|
- content: [{
|
|
|
- type: 'text',
|
|
|
- text: '<system-reminder>\nAdditional instructions from: nested/AGENTS.md\n\nRender workspace context XML clearly.\n</system-reminder>',
|
|
|
- }],
|
|
|
- source: { kind: 'plugin', plugin: 'workspace-context' },
|
|
|
- }), {
|
|
|
- surfaceOp: { op: 'replace', start: replacementStart, end: replacementEnd },
|
|
|
- sourceEventSeqs: replacementSources,
|
|
|
- })
|
|
|
+ appendCompactionCheckpoint(harness.session, compacted)
|
|
|
harness.terminal.resize(44, 18)
|
|
|
})
|
|
|
await checkpoint('surface-after-compaction-narrow', harness.terminal, { includeScrollback: true })
|
|
|
@@ -749,6 +773,23 @@ describe('TUI terminal-state snapshots', () => {
|
|
|
nowSpy.mockRestore()
|
|
|
})
|
|
|
|
|
|
+ // The resume path, which is what regressed for real users: the replacement is
|
|
|
+ // already stored when the terminal mounts, so the transcript comes from replay
|
|
|
+ // rather than from live appends. Pinned against the same log the live scenario
|
|
|
+ // ends on, at its wide size, so the two fixtures are directly comparable.
|
|
|
+ it('pins a stored compaction replayed at mount', async () => {
|
|
|
+ const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(COMPACTION_FIXTURE_TIME)
|
|
|
+ const harness = await setupSnapshot({
|
|
|
+ tools: ADVANCED_CARD_TOOLS,
|
|
|
+ beforeMount(session) {
|
|
|
+ appendCompactionCheckpoint(session, appendPreCompactionLog(session))
|
|
|
+ },
|
|
|
+ }, { columns: 104, rows: 30 })
|
|
|
+ await checkpoint('surface-replayed-compaction', harness.terminal, { includeScrollback: true })
|
|
|
+ await disposeSnapshot(harness)
|
|
|
+ nowSpy.mockRestore()
|
|
|
+ })
|
|
|
+
|
|
|
it('pins wrapped and explicit multiline shell-prompt input', async () => {
|
|
|
const harness = await setupSnapshot({}, { columns: 44, rows: 18 })
|
|
|
await renderAfter(harness, () => {
|