Merge remote-tracking branch 'origin/master' into worktree/web-theme-settings-integration-fde706

# Conflicts:
#	docs/event-producer-consumer.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/event-producer-consumer.zh.md
This commit is contained in:
Yichen Jiang
2026-08-10 15:34:01 +08:00
37 changed files with 477 additions and 35 deletions

View File

@@ -35,6 +35,7 @@ type FeedRow = {
origin?: 'subagent'
running?: boolean
blank?: boolean
agentPreset?: string
}
async function feedList(b: Bench, rows: FeedRow[]): Promise<void> {
@@ -44,6 +45,7 @@ async function feedList(b: Bench, rows: FeedRow[]): Promise<void> {
...(r.cwd !== undefined ? { cwd: r.cwd } : {}),
...(r.parentId !== undefined ? { parentSessionId: sid(r.parentId) } : {}),
...(r.origin !== undefined ? { origin: r.origin } : {}),
...(r.agentPreset !== undefined ? { agentPreset: r.agentPreset } : {}),
})),
}) as never)
await b.svc.refresh()
@@ -70,6 +72,38 @@ describe('list store projection', () => {
expect(state.byId[sid('s2')]?.title).toBeUndefined()
})
it('reprojects a blank session whose composition switched and nothing else moved', async () => {
const b = bench()
await feedList(b, [{ id: 's1', blank: true, agentPreset: 'standard' }])
expect(b.svc.list.getSnapshot().byId[sid('s1')]?.agentPreset).toBe('standard')
// A confirmed switch moves the preset alone: the row keeps its updatedAt,
// title, running, and blank bits, so an identity guard blind to the preset
// would serve the old row forever — and every reader (the hero chip's own
// no-op check, the header label) would keep the composition it replaced.
b.svc.noteAgentPreset(sid('s1'), 'minimal')
await Promise.resolve()
expect(b.svc.list.getSnapshot().byId[sid('s1')]?.agentPreset).toBe('minimal')
})
it('learns a preset switch from the host frame, not only from the tab that issued it', async () => {
const b = bench()
await feedList(b, [{ id: 's1', blank: true, agentPreset: 'standard' }])
// Every connected client gets this frame; only the switching tab gets the
// RPC echo. A client that ignored the payload would keep labelling the
// session with the composition it replaced.
b.svc.handleHostEnvelope({
rpcId: 'r1' as never,
payload: { type: 'host/session-preset-changed', sessionId: sid('s1'), agentPreset: 'minimal' } as never,
})
await Promise.resolve()
expect(b.svc.list.getSnapshot().byId[sid('s1')]?.agentPreset).toBe('minimal')
expect(b.svc.list.getSnapshot().byId[sid('s1')]?.blank).toBe(true)
})
it('reflects live increments (host stream via manager) into the store', async () => {
const b = bench()
await feedList(b, [{ id: 's1' }])

View File

@@ -1,6 +1,7 @@
/**
* Wire-to-typed-event bridge: host/commands-changed
* → ctx 'commands/changed'; each established connection generation
* → ctx 'commands/changed'; host/session-preset-changed
* ctx 'session/preset-changed'; each established connection generation →
* ctx 'connection/reset' (the forced cache-invalidation broadcast).
*/
import { Context } from 'cordis'
@@ -67,6 +68,17 @@ describe('wire event bridge', () => {
])
})
it('broadcasts session/preset-changed with the recomposed session and its new preset', async () => {
const bench = await mount()
const seen: Array<[string, string]> = []
bench.ctx.on('session/preset-changed', (sessionId, agentPreset) => { seen.push([sessionId, agentPreset]) })
bench.sinks?.onHostEnvelope?.({
rpcId: 'r1' as never,
payload: { type: 'host/session-preset-changed', sessionId: 's1' as never, agentPreset: 'minimal' },
})
expect(seen).toEqual([['s1', 'minimal']])
})
it('broadcasts connection/reset on every established generation (reconnect invalidation)', async () => {
const bench = await mount()
let resets = 0