Merge origin/master into worktree/explicit-turn-signal

This commit is contained in:
Yichen Jiang
2026-07-20 21:38:49 +08:00
1322 changed files with 47895 additions and 20257 deletions

View File

@@ -1,5 +1,5 @@
/**
* Property-based tests for the Session event log (the property-testing RFC).
* Property-based tests for the Session event log (the property-testing Agent Note).
*
* Generates arbitrary event logs and asserts the derivation invariants the
* agent loop and replay depend on: deriveMessages is deterministic and

View File

@@ -1,10 +1,18 @@
import { describe, expect, it, vi } from 'vitest'
import { describe, expect, expectTypeOf, it, vi } from 'vitest'
import { Context } from 'cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import SessionStore, { SESSION_FORMAT_VERSION, Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
import type { CreateSessionOptions, SessionEventType, SessionHeader, TodoItem } from '@deepseek-ai/dsh-session'
import type { CreateSessionOptions, SessionEventType, SessionHeader, SessionSurface, TodoItem } from '@deepseek-ai/dsh-session'
describe('Session', () => {
it('exposes one stable readonly surface view', () => {
const session = new Session(SessionId('surface-view'))
const surface = session.surface
expectTypeOf(surface).toEqualTypeOf<SessionSurface>()
expect(surface).toBe(session.surface)
})
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' } } })
@@ -50,7 +58,7 @@ describe('Session', () => {
expect(turnEnd?.type === 'turn/end' && turnEnd.data.reason).toEqual({ kind: 'aborted' })
})
it('renders context and steering messages as tagged synthetic user content', () => {
it('renders context and steering messages as plain user content', () => {
const session = new Session(SessionId('s2'))
session.append('context/message', {
content: [{ type: 'text', text: 'file changed: a.ts' }],
@@ -64,12 +72,12 @@ describe('Session', () => {
const [contextMessage, steeringMessage] = session.deriveMessages()
expect(contextMessage!.role).toBe('user')
expect(contextMessage!.content[0]).toMatchObject({ type: 'text', text: '<context source="plugin">' })
expect(contextMessage!.content.at(-1)).toMatchObject({ type: 'text', text: '</context>' })
expect(steeringMessage!.content[0]).toMatchObject({ type: 'text', text: '<steering source="user">' })
expect(contextMessage!.content).toEqual([{ type: 'text', text: 'file changed: a.ts' }])
expect(steeringMessage!.role).toBe('user')
expect(steeringMessage!.content).toEqual([{ type: 'text', text: 'focus on tests' }])
})
it('renders raw context without a generic envelope while preserving structured metadata', () => {
it('keeps context meta durable in the event while hiding it from the projection', () => {
const session = new Session(SessionId('s2-raw'))
const meta = {
kind: 'workspace-instructions',
@@ -79,7 +87,6 @@ describe('Session', () => {
session.append('context/message', {
content: [{ type: 'text', text: '<system-reminder>Additional instructions from: pkg/AGENTS.md</system-reminder>' }],
source: { kind: 'plugin', plugin: 'workspace-context' },
envelope: 'raw',
meta,
}, { surfaceOp: 'append' })
@@ -884,6 +891,19 @@ describe('SessionStore', () => {
})
})
it('attaches delegationDepth from meta to the header', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const session = ctx.sessions.create(SessionId('delegated-child'), {
meta: { parentSession: SessionId('parent'), delegationDepth: 2 },
})
expect(session.header).toMatchObject({
id: 'delegated-child',
parentSession: 'parent',
delegationDepth: 2,
})
})
it('rejects non-JSON and invalid scalar session metadata', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
@@ -895,6 +915,9 @@ describe('SessionStore', () => {
{ 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/ },
{ meta: { delegationDepth: '1' }, error: /delegationDepth must be a non-negative safe integer/ },
{ meta: { delegationDepth: 0.5 }, error: /delegationDepth must be a non-negative safe integer/ },
{ meta: { delegationDepth: -1 }, error: /delegationDepth must be a non-negative safe integer/ },
]
for (const [index, { meta, error }] of cases.entries()) {
@@ -1041,6 +1064,45 @@ describe('SessionStore', () => {
expect(observed).toEqual([appended])
})
it('does not publish a surface transition rejected by internal dispatch', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const session = ctx.sessions.create(SessionId('surface-dispatch-veto'))
session.append('user/message', {
content: [{ type: 'text', text: 'source' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
const surface = session.surface
let reject = true
ctx.on('internal/dispatch', (_mode, name) => {
if (name === 'session/event' && reject) {
reject = false
throw new Error('reject surface candidate')
}
})
expect(() => session.append('assistant/message', {
provenance: { provider: 'mock', model: 'mock' },
turn: 1,
step: 1,
content: [{ type: 'text', text: 'replacement' }],
}, {
surfaceOp: { op: 'replace', start: 0, end: 0 },
sourceEventSeqs: [0],
})).toThrow('reject surface candidate')
expect(session.events).toHaveLength(1)
expect(surface.nodes).toEqual([0])
expect(surface.replaceGeneration).toBe(0)
session.append('user/message', {
content: [{ type: 'text', text: 'next' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
expect(surface.nodes).toEqual([0, 1])
expect(surface.replaceGeneration).toBe(0)
})
it('resolves session/event dispatch before commit so instrumentation failure cannot hide a logged event', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)

View File

@@ -142,6 +142,11 @@ describe('SurfaceManager', () => {
it('leaves incremental state unchanged when candidate validation fails', () => {
const s = new Session(SessionId('atomic-validation'))
s.append('user/message', { content: [{ type: 'text', text: 'a' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
const surface = s.surface
const nodes = surface.nodes
expect(nodes).toEqual(foldSurface(s.events).nodes)
expect(surface.replaceGeneration).toBe(0)
expect(() => s.append(
'assistant/message',
@@ -150,8 +155,16 @@ describe('SurfaceManager', () => {
)).toThrow(/missing 0/)
expect(s.events).toHaveLength(1)
expect(s.surface).toBe(surface)
expect(surface.nodes).toEqual([0])
expect(surface.replaceGeneration).toBe(0)
expect(surface.nodes).toEqual(foldSurface(s.events).nodes)
s.append('user/message', { content: [{ type: 'text', text: 'b' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
expect(s.surface.nodes).toEqual([0, 1])
expect(surface.nodes).toBe(nodes)
expect(surface.nodes).toEqual([0, 1])
expect(surface.replaceGeneration).toBe(0)
expect(surface.nodes).toEqual(foldSurface(s.events).nodes)
})
it('foldSurface rejects a surface-eligible event without its mandatory marker', () => {
@@ -356,8 +369,8 @@ describe('deriveMessages with surface', () => {
s.append('steering/message', { turn: 1, content: [{ type: 'text', text: 'focus' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
const messages = s.deriveMessages()
expect(messages).toHaveLength(2)
expect(messages[0]!.content[0]).toMatchObject({ type: 'text', text: '<context source="plugin">' })
expect(messages[1]!.content[0]).toMatchObject({ type: 'text', text: '<steering source="user">' })
expect(messages[0]!.content).toEqual([{ type: 'text', text: 'file changed' }])
expect(messages[1]!.content).toEqual([{ type: 'text', text: 'focus' }])
})
})