Merge origin/master into web-permission-sandbox

39 conflicts resolved per the reattachment plan (missions worktree-projbiz
0728-1859): baseline wins for deleted packages (host/runtime, old ui/acp,
ui-sidebar Rows/tree) and retired specs; unions for wire-layer exports and
client summary fields; the approval takeover, waitingApprovals tracking, and
PendingApproval domain face carry over onto the master structure. The two
new host specs follow the runtime->apiproxy rename. Dead PR-side wiring
(ConversationInjected permissions/setPermission spread, InputBar controls
prop, boot.ts sandbox composition) resolves to master and its replacement
lands in follow-up commits.
This commit is contained in:
imccyu
2026-07-28 21:20:00 +08:00
3427 changed files with 156166 additions and 59003 deletions

View File

@@ -1,14 +1,14 @@
import { describe, expect, expectTypeOf, it, vi } from 'vitest'
import { Context } from 'cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import { createUserMessage, CallId, createMessage, createToolResultMessage, MessageId, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import SessionStore, {
displayPromptContent,
findLastMessageTurnEnd,
hasOpenTurn,
SESSION_FORMAT_VERSION,
Session,
SessionEvent,
SessionId,
snapshotSessionEvent,
} from '@deepseek-ai/dsh-session'
import type { CreateSessionOptions, SessionEventType, SessionHeader, SessionSurface, TodoItem } from '@deepseek-ai/dsh-session'
@@ -24,16 +24,32 @@ describe('Session', () => {
it('derives message history from the event log', () => {
const session = new Session(SessionId('s1'))
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('user/message', { content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' },
}), { surfaceOp: 'append' })
session.append('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'hi' } })
session.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' },
session.append('assistant/message', {
turn: 1, step: 1,
content: [
{ type: 'text', text: 'let me check' },
{ type: 'tool-call', id: CallId('c1'), name: 'echo', arguments: '{}' },
],
message: createMessage({
role: 'assistant',
content: [
{ type: 'text', text: 'let me check' },
{ type: 'tool-call', id: CallId('c1'), name: 'echo', arguments: '{}' },
],
source: {
kind: 'model',
...{ provider: 'mock', model: 'mock' },
},
}),
}, { surfaceOp: 'append' })
session.append('tool/result', {
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('c1'),
content: [{ type: 'text', text: 'ok' }],
isError: false,
}),
}, { surfaceOp: 'append' })
session.append('tool/result', { turn: 1, step: 1, callId: CallId('c1'), content: [{ type: 'text', text: 'ok' }], isError: false }, { surfaceOp: 'append' })
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
const messages = session.deriveMessages()
@@ -63,10 +79,10 @@ describe('Session', () => {
turn: 1,
trigger: { kind: 'injection', source: { kind: 'plugin', plugin: 'before' } },
})
session.append('context/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'before' }],
source: { kind: 'plugin', plugin: 'before' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
expect(findLastMessageTurnEnd(session.events)).toBeUndefined()
@@ -74,19 +90,19 @@ describe('Session', () => {
turn: 2,
trigger: { kind: 'message', source: { kind: 'user' } },
})
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'bounded prompt' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
const messageEnd = session.append('turn/end', { turn: 2, reason: { kind: 'max-tokens' } })
session.append('turn/start', {
turn: 3,
trigger: { kind: 'injection', source: { kind: 'plugin', plugin: 'after' } },
})
session.append('context/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'after' }],
source: { kind: 'plugin', plugin: 'after' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
session.append('turn/end', { turn: 3, reason: { kind: 'completed' } })
expect(findLastMessageTurnEnd(session.events)).toBe(messageEnd)
@@ -129,16 +145,18 @@ describe('Session', () => {
.toThrow('seed turn/end at index 1 uses unsupported reason-bearing aborted format')
})
it('renders context and steering messages as plain user content', () => {
it('renders injected-context and steering messages as plain user content', () => {
const session = new Session(SessionId('s2'))
session.append('context/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'file changed: a.ts' }],
source: { kind: 'plugin', plugin: 'watcher' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
session.append('steering/message', {
turn: 1,
content: [{ type: 'text', text: 'focus on tests' }],
source: { kind: 'user' },
message: createUserMessage({
content: [{ type: 'text', text: 'focus on tests' }],
source: { kind: 'user' },
}),
}, { surfaceOp: 'append' })
const [contextMessage, steeringMessage] = session.deriveMessages()
@@ -148,61 +166,36 @@ describe('Session', () => {
expect(steeringMessage!.content).toEqual([{ type: 'text', text: 'focus on tests' }])
})
it('derives baked prompt context while exposing only the direct prompt for display', () => {
const session = new Session(SessionId('prompt-envelope'))
const event = session.append('user/message', {
content: [
{ type: 'text', text: 'background' },
{ type: 'text', text: '\n\n## My request:\n' },
{ type: 'text', text: 'question' },
],
source: { kind: 'user' },
envelope: {
displayContent: [{ type: 'text', text: 'question' }],
prefixContexts: [{ source: { kind: 'plugin', plugin: 'reference' }, meta: { kind: 'card' } }],
},
}, { surfaceOp: 'append' })
expect(session.deriveMessages()).toEqual([{
role: 'user',
content: [
{ type: 'text', text: 'background' },
{ type: 'text', text: '\n\n## My request:\n' },
{ type: 'text', text: 'question' },
],
}])
expect(displayPromptContent(event.data)).toEqual([{ type: 'text', text: 'question' }])
expect(Object.isFrozen(event.data.envelope?.displayContent)).toBe(true)
expect(new Session(SessionId('prompt-envelope-replay'), session.events).deriveMessages())
.toEqual(session.deriveMessages())
})
it('keeps context meta durable in the event while hiding it from the projection', () => {
it('keeps the exact identified context message in durable history and projection', () => {
const session = new Session(SessionId('s2-raw'))
const meta = {
kind: 'workspace-instructions',
version: 1,
changes: [{ action: 'set', scope: 'pkg', path: 'pkg/AGENTS.md', digest: 'abc123' }],
}
session.append('context/message', {
const message = createUserMessage({
content: [{ type: 'text', text: '<system-reminder>Additional instructions from: pkg/AGENTS.md</system-reminder>' }],
source: { kind: 'plugin', plugin: 'workspace-context' },
meta,
}, { surfaceOp: 'append' })
})
session.append('user/message', message, { surfaceOp: 'append' })
expect(session.deriveMessages()).toEqual([{
role: 'user',
content: [{ type: 'text', text: '<system-reminder>Additional instructions from: pkg/AGENTS.md</system-reminder>' }],
}])
expect(session.deriveMessages()).toEqual([message])
const event = session.events[0]
expect(event?.type === 'context/message' && event.data.meta).toEqual(meta)
expect(event?.type === 'user/message' && event.data.source).toEqual({ kind: 'plugin', plugin: 'workspace-context' })
})
it('replays identically from a seeded event log', () => {
const original = new Session(SessionId('s3'))
original.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
original.append('user/message', { content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
original.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' }, turn: 1, step: 1, content: [{ type: 'text', text: 'a' }] }, { surfaceOp: 'append' })
original.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'q' }], source: { kind: 'user' },
}), { surfaceOp: 'append' })
original.append('assistant/message', {
turn: 1, step: 1,
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text: 'a' }],
source: {
kind: 'model',
...{ provider: 'mock', model: 'mock' },
},
}),
}, { surfaceOp: 'append' })
original.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
const replayed = new Session(SessionId('s3-replay'), [...original.events])
@@ -224,7 +217,7 @@ describe('Session', () => {
surfaceOp: 'append',
} as unknown as SessionEvent
expect(() => new Session(SessionId('old-assistant'), [assistantMessage]))
.toThrow('seed assistant/message at index 0 lacks provider/model provenance')
.toThrow('seed assistant/message at index 0 lacks an identified message')
const malformedHeader = {
type: 'request/header', seq: 0, time: 1,
@@ -240,12 +233,197 @@ describe('Session', () => {
.toEqual([unrelatedPrimitiveData])
})
it('rejects event-specific malformed message shapes on seed/load', () => {
const user = {
id: 'user',
role: 'user',
content: [{ type: 'text', text: 'content' }],
source: { kind: 'user' },
}
const assistant = {
id: 'assistant',
role: 'assistant',
content: [{ type: 'text', text: 'content' }],
source: { kind: 'model', provider: 'mock', model: 'mock' },
}
const tool = {
id: 'tool',
role: 'user',
content: [{
type: 'tool-result',
toolCallId: 'call',
content: [{ type: 'text', text: 'result' }],
}],
source: { kind: 'tool', callId: 'call' },
}
const invalid = [
{
name: 'message record',
event: {
type: 'user/message', seq: 0, time: 1, surfaceOp: 'append',
data: null,
},
message: 'lacks an identified message',
},
{
name: 'user role',
event: {
type: 'user/message', seq: 0, time: 1, surfaceOp: 'append',
data: { ...user, role: 'assistant' },
},
message: 'message must have role "user"',
},
{
name: 'source',
event: {
type: 'user/message', seq: 0, time: 1, surfaceOp: 'append',
data: { ...user, source: null },
},
message: 'message has invalid source',
},
{
name: 'assistant source',
event: {
type: 'assistant/message', seq: 0, time: 1, surfaceOp: 'append',
data: {
turn: 1,
step: 1,
message: { ...assistant, source: { kind: 'user' } },
},
},
message: 'message must have model source',
},
{
name: 'content block',
event: {
type: 'steering/message', seq: 0, time: 1, surfaceOp: 'append',
data: {
turn: 1,
message: { ...user, content: 'not-an-array' },
},
},
message: 'message has invalid content',
},
{
name: 'tool source',
event: {
type: 'tool/result', seq: 0, time: 1, surfaceOp: 'append',
data: {
turn: 1,
step: 1,
message: { ...tool, source: { kind: 'user' } },
},
},
message: 'message must have tool source',
},
{
name: 'tool tuple',
event: {
type: 'tool/result', seq: 0, time: 1, surfaceOp: 'append',
data: {
turn: 1,
step: 1,
message: { ...tool, content: [{ type: 'text', text: 'not a result' }] },
},
},
message: 'message must contain one tool-result block',
},
{
name: 'tool correlation',
event: {
type: 'tool/result', seq: 0, time: 1, surfaceOp: 'append',
data: {
turn: 1,
step: 1,
message: {
...tool,
source: { kind: 'tool', callId: 'other-call' },
},
},
},
message: 'message has mismatched tool call ids',
},
] as const
for (const { name, event, message } of invalid) {
expect(
() => new Session(SessionId(`invalid-${name}`), [event as unknown as SessionEvent]),
name,
).toThrow(message)
}
})
it('snapshots message events without validating plugin-owned block details', () => {
const boundary = snapshotSessionEvent({
type: 'turn/start',
seq: 0,
time: 1,
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
})
expect(boundary).toEqual({
type: 'turn/start',
seq: 0,
time: 1,
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
})
const extended = snapshotSessionEvent({
type: 'user/message',
seq: 0,
time: 1,
surfaceOp: 'append',
data: {
id: 'extended-message',
role: 'user',
content: [{ type: 'plugin-block', value: 1 }],
source: { kind: 'plugin-source', value: 1 },
},
} as unknown as SessionEvent)
expect(extended.type === 'user/message' && extended.data.content)
.toEqual([{ type: 'plugin-block', value: 1 }])
})
it('round-trips a non-empty reasoning effort and rejects invalid durable values', () => {
const valid = {
type: 'request/header',
seq: 0,
time: 1,
data: {
header: {
config: {
provider: 'mock',
model: 'model',
reasoningEffort: ReasoningEffortId('adapter-owned'),
},
},
reason: 'initial',
},
} as const
expect(new Session(SessionId('reasoning-effort'), [valid]).events[0])
.toEqual(valid)
for (const reasoningEffort of ['', 1]) {
const invalid = structuredClone(valid) as unknown as SessionEvent
if (invalid.type !== 'request/header') throw new Error('test fixture must be a request header')
const config = invalid.data.header.config as unknown as Record<string, unknown>
config.reasoningEffort = reasoningEffort
expect(() => new Session(SessionId('invalid-reasoning-effort'), [invalid]))
.toThrow('seed request/header at index 0 has an invalid reasoningEffort')
}
})
it('isolates the log from mutation through a derived message (append-only contract)', () => {
const session = new Session(SessionId('s4'))
session.append('user/message', { content: [{ type: 'text', text: 'original' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'original' }], source: { kind: 'user' },
}), { surfaceOp: 'append' })
session.append('tool/result', {
turn: 1, step: 1, callId: CallId('c1'),
content: [{ type: 'text', text: 'tool out' }], isError: false,
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('c1'),
content: [{ type: 'text', text: 'tool out' }],
isError: false,
}),
}, { surfaceOp: 'append' })
const before = structuredClone(session.events)
@@ -301,7 +479,9 @@ describe('Session', () => {
// A widened SessionEventType bypasses the overload's conditional requirement,
// so the runtime guard must still reject the missing surface marker.
const widenedType = 'user/message' as SessionEventType
expect(() => session.append(widenedType, { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }))
expect(() => session.append(widenedType, createUserMessage({
content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' },
})))
.toThrow(/surface-eligible and requires a surfaceOp marker/)
// The rejected append never entered the log (only turn/start is present).
expect(session.events).toHaveLength(1)
@@ -337,7 +517,9 @@ describe('Session', () => {
// compile time; a raw seed must be rejected at runtime to match.
const markerlessSeed = [
{ type: 'turn/start' as const, seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message' as const, source: { kind: 'user' as const } } } },
{ type: 'user/message' as const, seq: 1, time: 2, data: { content: [{ type: 'text' as const, text: 'hi' }], source: { kind: 'user' as const } } },
{ type: 'user/message' as const, seq: 1, time: 2, data: createUserMessage({
content: [{ type: 'text' as const, text: 'hi' }], source: { kind: 'user' as const },
}) },
{ type: 'turn/end' as const, seq: 2, time: 3, data: { turn: 1, reason: { kind: 'completed' as const } } },
] as SessionEvent[]
expect(() => new Session(SessionId('seed-no-marker'), markerlessSeed)).toThrow(/requires a surfaceOp marker/)
@@ -346,7 +528,9 @@ describe('Session', () => {
it('accepts a well-formed contiguous serializable seed', () => {
const goodSeed = [
{ type: 'turn/start' as const, seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message' as const, source: { kind: 'user' as const } } } },
{ type: 'user/message' as const, seq: 1, time: 2, data: { content: [{ type: 'text' as const, text: 'hi' }], source: { kind: 'user' as const } }, surfaceOp: 'append' as const },
{ type: 'user/message' as const, seq: 1, time: 2, data: createUserMessage({
content: [{ type: 'text' as const, text: 'hi' }], source: { kind: 'user' as const },
}), surfaceOp: 'append' as const },
{ type: 'turn/end' as const, seq: 2, time: 3, data: { turn: 1, reason: { kind: 'completed' as const } } },
] as SessionEvent[]
const session = new Session(SessionId('seed-ok'), goodSeed)
@@ -399,7 +583,9 @@ describe('Session', () => {
type: 'user/message',
seq: 0,
time: 1,
data: { content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } },
data: createUserMessage({
content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' },
}),
surfaceOp: { op: 'replace', start: 1n, end: 2 },
}] as unknown as SessionEvent[]
@@ -417,7 +603,9 @@ describe('Session', () => {
type: 'user/message',
seq: 0,
time: 1,
data: { content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } },
data: createUserMessage({
content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' },
}),
surfaceOp: new ReplaceOp(),
}] as unknown as SessionEvent[]
@@ -464,13 +652,17 @@ describe('Session', () => {
type: 'user/message',
seq: 0,
time: 1,
data: { content: [{ type: 'text', text: 'source' }], source: { kind: 'user' } },
data: createUserMessage({
content: [{ type: 'text', text: 'source' }], source: { kind: 'user' },
}),
surfaceOp: 'append',
}, {
type: 'user/message',
seq: 1,
time: 2,
data: { content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } },
data: createUserMessage({
content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' },
}),
surfaceOp,
sourceEventSeqs: [0],
}] as unknown as SessionEvent[]
@@ -496,13 +688,17 @@ describe('Session', () => {
type: 'user/message',
seq: 0,
time: 1,
data: { content: [{ type: 'text', text: 'source' }], source: { kind: 'user' } },
data: createUserMessage({
content: [{ type: 'text', text: 'source' }], source: { kind: 'user' },
}),
surfaceOp: 'append',
}, {
type: 'user/message',
seq: 1,
time: 2,
data: { content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } },
data: createUserMessage({
content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' },
}),
surfaceOp: { op: 'replace', start: 0, end: 0 },
sourceEventSeqs: [0],
}] as unknown as SessionEvent[]
@@ -518,7 +714,11 @@ describe('Session', () => {
it('snapshots the seed: mutating the original after construction does not affect session.events', () => {
const seed = [
{ type: 'turn/start' as const, seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message' as const, source: { kind: 'user' as const } } } },
{ type: 'user/message' as const, seq: 1, time: 2, data: { content: [{ type: 'text' as const, text: 'original' }], source: { kind: 'user' as const } }, surfaceOp: 'append' as const },
{ type: 'user/message' as const, seq: 1, time: 2, data: {
id: MessageId('seed-input'),
role: 'user' as const,
content: [{ type: 'text' as const, text: 'original' }], source: { kind: 'user' as const },
}, surfaceOp: 'append' as const },
{ type: 'turn/end' as const, seq: 2, time: 3, data: { turn: 1, reason: { kind: 'completed' as const } } },
] as SessionEvent[]
const session = new Session(SessionId('seed-snapshot'), seed)
@@ -535,7 +735,12 @@ describe('Session', () => {
it('snapshots append data: mutating the passed object after append does not affect session.events', () => {
const session = new Session(SessionId('append-snapshot'))
const data = { content: [{ type: 'text' as const, text: 'original' }], source: { kind: 'user' as const } }
const data = {
id: MessageId('append-input'),
role: 'user' as const,
content: [{ type: 'text' as const, text: 'original' }],
source: { kind: 'user' as const },
}
const event = session.append('user/message', data, { surfaceOp: 'append' })
// Mutate the caller's object after append returns. A shared reference would
// make session.events diverge from the value that passed validation.
@@ -571,7 +776,9 @@ describe('Session', () => {
expect(() => session.append(
'user/message',
{ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } },
createUserMessage({
content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' },
}),
{ surfaceOp: { op: 'replace', start: 1n, end: 2 } } as never,
)).toThrow(/non-JSON-serializable surface metadata/)
expect(session.events).toEqual([])
@@ -587,7 +794,9 @@ describe('Session', () => {
expect(() => session.append(
'user/message',
{ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } },
createUserMessage({
content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' },
}),
{ surfaceOp: new ReplaceOp() },
)).toThrow(/non-JSON-serializable surface metadata/)
expect(session.events).toEqual([])
@@ -597,7 +806,9 @@ describe('Session', () => {
const session = new Session(SessionId('append-unstable-metadata'))
const source = session.append(
'user/message',
{ content: [{ type: 'text', text: 'source' }], source: { kind: 'user' } },
createUserMessage({
content: [{ type: 'text', text: 'source' }], source: { kind: 'user' },
}),
{ surfaceOp: 'append' },
)
let reads = 0
@@ -611,7 +822,9 @@ describe('Session', () => {
const event = session.append(
'user/message',
{ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } },
createUserMessage({
content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' },
}),
{ surfaceOp, sourceEventSeqs: [0] } as never,
)
@@ -769,7 +982,7 @@ describe('Session', () => {
{ header: 1, error: /not a plain JSON record/ },
{ header: null, error: /not a plain JSON record/ },
{ header: { ...base, version: 1 }, error: /header version/ },
{ header: { ...base, createdAt: '123' }, error: /createdAt must be a finite number/ },
{ header: { ...base, createdAt: '123' }, error: /createdAt must be a non-negative safe integer/ },
{ header: { ...base, cwd: 1 }, error: /header cwd must be a string/ },
{ header: { ...base, cwd: 'relative' }, error: /header cwd must be an absolute path/ },
{ header: { ...base, parentSession: 1 }, error: /header parentSession must be a string/ },
@@ -828,7 +1041,9 @@ describe('SessionStore', () => {
// but cannot suppress the durable event feed.
expect(Reflect.set(session, 'onAppend', undefined)).toBe(true)
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('user/message', { content: [{ type: 'text', text: 'x' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'x' }], source: { kind: 'user' },
}), { surfaceOp: 'append' })
expect(events).toHaveLength(2)
expect(events[1]![0]).toBe(session)
expect(events[1]![1].type).toBe('user/message')
@@ -844,7 +1059,9 @@ describe('SessionStore', () => {
expect(() => ctx.sessions.create(SessionId('fixed'))).toThrow('already exists')
a.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
a.append('user/message', { content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
a.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'q' }], source: { kind: 'user' },
}), { surfaceOp: 'append' })
const forked = ctx.sessions.create(SessionId('fork'), { seed: [...a.events] })
expect(forked.deriveMessages()).toEqual(a.deriveMessages())
})
@@ -974,7 +1191,7 @@ describe('SessionStore', () => {
await ctx.plugin(SessionStore)
const session = ctx.sessions.create(SessionId('plain'))
expect(session.header).toMatchObject({ version: SESSION_FORMAT_VERSION, id: 'plain' })
expect(typeof session.header.createdAt).toBe('number')
expect(Number.isSafeInteger(session.header.createdAt)).toBe(true)
expect(session.header.cwd).toBeUndefined()
expect(session.header.parentSession).toBeUndefined()
})
@@ -1013,7 +1230,10 @@ describe('SessionStore', () => {
{ meta: { parentSession: 1n }, error: /header is not losslessly JSON-serializable/ },
{ meta: { cwd: 1 }, error: /header cwd must be a string/ },
{ meta: { parentSession: 1 }, error: /header parentSession must be a string/ },
{ meta: { createdAt: '123' }, error: /header createdAt must be a finite number/ },
{ meta: { createdAt: '123' }, error: /header createdAt must be a non-negative safe integer/ },
{ meta: { createdAt: 1.5 }, error: /header createdAt must be a non-negative safe integer/ },
{ meta: { createdAt: -1 }, error: /header createdAt must be a non-negative safe integer/ },
{ meta: { createdAt: Number.MAX_SAFE_INTEGER + 1 }, error: /header createdAt must be a non-negative safe integer/ },
{ meta: { seedLength: '1' }, error: /seedLength must be a non-negative safe integer/ },
{ meta: { seedLength: 0.5 }, error: /seedLength must be a non-negative safe integer/ },
{ meta: { seedLength: -1 }, error: /seedLength must be a non-negative safe integer/ },
@@ -1059,7 +1279,9 @@ describe('SessionStore', () => {
await fiber.dispose()
expect(ctx.sessions.get(SessionId('scoped'))).toBeUndefined()
session.append('user/message', { content: [{ type: 'text', text: 'late' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'late' }], source: { kind: 'user' },
}), { surfaceOp: 'append' })
expect(observed).toBe(0)
})
@@ -1086,7 +1308,9 @@ describe('SessionStore', () => {
const session = ctx.sessions.create(SessionId('fixed'))
expect(ctx.sessions.get(SessionId('fixed'))).toBe(session)
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('user/message', { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' },
}), { surfaceOp: 'append' })
expect(events.at(-1)?.type).toBe('user/message')
})
@@ -1173,10 +1397,10 @@ describe('SessionStore', () => {
const session = ctx.sessions.create(SessionId('surface-dispatch-veto'))
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('step/start', { turn: 1, step: 1 })
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'source' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
const surface = session.surface
let reject = true
ctx.on('internal/dispatch', (_mode, name) => {
@@ -1187,10 +1411,16 @@ describe('SessionStore', () => {
})
expect(() => session.append('assistant/message', {
provenance: { provider: 'mock', model: 'mock' },
turn: 1,
step: 1,
content: [{ type: 'text', text: 'replacement' }],
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text: 'replacement' }],
source: {
kind: 'model',
...{ provider: 'mock', model: 'mock' },
},
}),
}, {
surfaceOp: { op: 'replace', start: 2, end: 2 },
sourceEventSeqs: [2],
@@ -1200,10 +1430,10 @@ describe('SessionStore', () => {
expect(surface.nodes).toEqual([2])
expect(surface.replaceGeneration).toBe(0)
session.append('user/message', {
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'next' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
expect(surface.nodes).toEqual([2, 3])
expect(surface.replaceGeneration).toBe(0)
})
@@ -1409,7 +1639,9 @@ describe('todo/write event', () => {
it('is NOT a surface event: it produces no derived message and joins no surface node', () => {
const session = new Session(SessionId('t3'))
session.append('user/message', { content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'q' }], source: { kind: 'user' },
}), { surfaceOp: 'append' })
const before = session.deriveMessages().length
session.append('todo/write', { todos: [{ content: 'a task', status: 'pending' }] })
// The todo event must not add a message to the derived history…