Merge remote-tracking branch 'origin/master' into worktree/web-model-request-retry

# Conflicts:
#	apps/cli/README.i18n.yaml
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/README.zh.md
#	packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx
This commit is contained in:
Yichen Jiang
2026-07-29 14:38:55 +08:00
308 changed files with 10894 additions and 1737 deletions
@@ -76,8 +76,19 @@ describe('createFixtureApi', () => {
// Fixture composes the todos + plan units (host parallel when tool-todo
// and plan-mode are mounted): the empty-log values.
expect(empty.result.value).toEqual({
events: [], hasMore: false,
projections: { asOfSeq: -1, values: { goal: null, todos: null, plan: { active: false, pending: false } } },
events: [], hasMore: false, projections: { asOfSeq: -1, values: {
todos: null,
// Permission unit composed: the composition-default select.
permissions: {
options: [
{ value: 'workspace-write', name: 'workspace-write', description: 'Write inside the workspace and permitted temporary directories; wider retries require approval.' },
{ value: 'danger-full-access', name: 'danger-full-access', description: 'Full file access without approval prompts.' },
],
currentValue: 'workspace-write',
},
plan: { active: false, pending: false },
goal: null,
} },
})
})
@@ -212,7 +223,7 @@ describe('createFixtureApi', () => {
const envelopes: RpcRequest<MuxFrame>[] = []
for await (const envelope of api.events.mux(req({}), abort.signal)) {
envelopes.push(envelope)
if (envelopes.length >= 7) abort.abort()
if (envelopes.length >= 8) abort.abort()
}
return envelopes
}
@@ -220,15 +231,16 @@ describe('createFixtureApi', () => {
const second = await openOnce()
expect(first[0]?.payload).toMatchObject({ type: 'session/subscribed', sessionId: 'fx-alpha' })
expect((first[0]?.payload as { lastSeq: number }).lastSeq).toBeGreaterThan(0)
// Projection baseline frames follow the subscribed frame (title + todos + plan + goal units).
// Projection baseline frames follow the subscribed frame (title + todos + permissions + plan + goal units).
expect(first[1]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'title', value: 'Fixture 历史会话' })
expect(first[2]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'todos' })
expect(first[3]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'plan', value: { active: false, pending: false } })
expect(first[4]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'goal', value: null })
expect(first[5]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' })
expect(second[5]?.rpcId).toBe(first[5]?.rpcId) // stable rpcId across replays (host replay semantics)
expect(first[6]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' })
expect(second[6]?.rpcId).toBe(first[6]?.rpcId)
expect(first[3]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'permissions' })
expect(first[4]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'plan', value: { active: false, pending: false } })
expect(first[5]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'goal', value: null })
expect(first[6]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' })
expect(second[6]?.rpcId).toBe(first[6]?.rpcId) // stable rpcId across replays (host replay semantics)
expect(first[7]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' })
expect(second[7]?.rpcId).toBe(first[7]?.rpcId)
})
it('steer with no replay in flight falls through to a fresh queued turn; non-text blocks stringify empty', async () => {
@@ -315,6 +327,44 @@ describe('createFixtureApi', () => {
})).toEqual({ accepted: true })
})
it('respond answers the resident approval once: routing, validation, resolved broadcast, then not-pending', async () => {
const api = createFixtureApi()
// Discover the resident approval's stable rpcId from the mux baseline.
const abort = new AbortController()
const seen: { rpcId: string; frame: MuxFrame }[] = []
const consuming = (async () => {
for await (const envelope of api.events.mux(req({}), abort.signal)) seen.push({ rpcId: envelope.rpcId, frame: envelope.payload })
})()
await vi.waitFor(() => {
expect(seen.some(s => s.frame.type === 'approval/requested')).toBe(true)
})
const requested = seen.find(s => s.frame.type === 'approval/requested')
if (requested === undefined || requested.frame.type !== 'approval/requested') throw new Error('unreachable')
const approvalId = requested.frame.approvalId
// Routed but malformed answers.
expect(await api.respond({ type: 'client-response', rpcId: RpcId(requested.rpcId), result: { ok: false, error: { code: 'internal', message: 'x', details: {} } } }))
.toEqual({ accepted: false, reason: 'bad-response' })
expect(await api.respond({ type: 'client-response', rpcId: RpcId(requested.rpcId), result: { ok: true, value: { approvalId: 'wrong', outcome: 'rejected' } } }))
.toEqual({ accepted: false, reason: 'bad-response' })
expect(await api.respond({ type: 'client-response', rpcId: RpcId(requested.rpcId), result: { ok: true, value: { approvalId, outcome: 'maybe' } } }))
.toEqual({ accepted: false, reason: 'bad-response' })
// The real answer settles the question and broadcasts resolved.
expect(await api.respond({ type: 'client-response', rpcId: RpcId(requested.rpcId), result: { ok: true, value: { sessionId: sid('fx-alpha'), approvalId, outcome: 'allowed-once' } } }))
.toEqual({ accepted: true })
await vi.waitFor(() => {
expect(seen.some(s => s.frame.type === 'approval/resolved' && s.frame.outcome === 'allowed-once')).toBe(true)
})
// Settled: a duplicate answer is late, and a fresh mux open replays nothing.
expect(await api.respond({ type: 'client-response', rpcId: RpcId(requested.rpcId), result: { ok: true, value: { sessionId: sid('fx-alpha'), approvalId, outcome: 'rejected' } } }))
.toEqual({ accepted: false, reason: 'not-pending' })
abort.abort()
await consuming
const abort2 = new AbortController()
const replayed = await collect(api.events.mux(req({}), abort2.signal), abort2, frames => frames.length === 2)
expect(replayed.some(f => f.type === 'approval/requested')).toBe(false)
})
it('describe answers the fixture identity', async () => {
const api = createFixtureApi()
const response = await api.host.describe(req({}))
@@ -323,6 +373,22 @@ describe('createFixtureApi', () => {
expect(empty.result).toMatchObject({ ok: true, value: { attachedSessions: 0 } })
})
it('createDirectory under the root mints /name whose listing and crumbs share the identity', async () => {
const api = createFixtureApi()
const created = await api.host.createDirectory(req({ path: '/', name: 'srv' }))
if (!created.result.ok) throw new Error('create failed')
expect(created.result.value.path).toBe('/srv')
const listed = await api.host.listDirectory(req({ path: '/srv' }), new AbortController().signal)
if (!listed.result.ok) throw new Error('list failed')
expect(listed.result.value.crumbs).toEqual([
{ name: '/', path: '/', hidden: false },
{ name: 'srv', path: '/srv', hidden: false },
])
const root = await api.host.listDirectory(req({ path: '/' }), new AbortController().signal)
if (!root.result.ok) throw new Error('root list failed')
expect(root.result.value.entries).toContainEqual({ name: 'srv', path: '/srv', hidden: false })
})
it('workspace.list serves the resident account and create reuses on path collision', async () => {
const api = createFixtureApi()
const listed = await api.workspace.list(req({}))