Merge master (slash/input/session architecture) into web-session-model-selector

This commit is contained in:
imccyu
2026-07-27 10:23:51 +08:00
2673 changed files with 101832 additions and 39946 deletions

View File

@@ -20,6 +20,8 @@ function ok<T>(request: RpcRequest<unknown>, value: T): Promise<RpcResponse<T>>
function scriptedApi(overrides: {
sessions?: Partial<ApiProxy['sessions']>
host?: Partial<ApiProxy['host']>
commands?: Partial<ApiProxy['commands']>
skills?: Partial<ApiProxy['skills']>
events?: Partial<ApiProxy['events']>
respond?: ApiProxy['respond']
} = {}): ApiProxy {
@@ -46,6 +48,18 @@ function scriptedApi(overrides: {
...overrides.sessions,
},
host: { describe: r => ok(r, { version: '0-test', cwd: '/t', attachedSessions: 0 }), ...overrides.host },
workspace: {
list: r => ok(r, { items: [] }),
create: r => ok(r, { workspace: { workspaceId: 'w1' as never, path: '/t', title: 't', sessionIds: [], createdAt: '0', updatedAt: '0' }, created: true }),
rename: r => ok(r, { workspace: { workspaceId: 'w1' as never, path: '/t', title: 't', sessionIds: [], createdAt: '0', updatedAt: '0' } }),
insertSessionBefore: r => ok(r, { workspace: { workspaceId: 'w1' as never, path: '/t', title: 't', sessionIds: [], createdAt: '0', updatedAt: '0' } }),
},
commands: {
list: r => ok(r, { commands: [] }),
execute: r => ok(r, { matched: false }),
...overrides.commands,
},
skills: { list: r => ok(r, { skills: [] }), ...overrides.skills },
events: { mux: () => empty<MuxFrame>(), host: () => empty<HostFrame>(), ...overrides.events },
respond: overrides.respond ?? (() => Promise.resolve({ accepted: false as const, reason: 'not-pending' as const })),
}
@@ -62,7 +76,7 @@ describe('unary round trip', () => {
sessions: {
list: (r) => {
seen = r
return ok(r, { items: [{ sessionId: sid('s1'), updatedAt: 7, running: false }] })
return ok(r, { items: [{ sessionId: sid('s1'), updatedAt: 7, running: false, blank: false }] })
},
},
})
@@ -71,7 +85,20 @@ describe('unary round trip', () => {
expect(seen?.payload).toEqual({ cursor: 'c1' })
expect(seen?.rpcId).toBeTruthy()
expect(response.rpcId).toBe(seen?.rpcId)
expect(response.result).toEqual({ ok: true, value: { items: [{ sessionId: 's1', updatedAt: 7, running: false }] } })
expect(response.result).toEqual({ ok: true, value: { items: [{ sessionId: 's1', updatedAt: 7, running: false, blank: false }] } })
})
it('routes workspace rename and insertSessionBefore through the wire', async () => {
const api = scriptedApi()
const c = client(api)
const renamed = await c.workspace.rename({ workspaceId: 'w1' as never, title: 'next' })
expect(renamed.result.ok).toBe(true)
const blankTitle = await c.workspace.rename({ workspaceId: 'w1' as never, title: ' ' })
expect(blankTitle.result).toMatchObject({ ok: false, error: { code: 'bad-request' } })
const anchored = await c.workspace.insertSessionBefore({ workspaceId: 'w1' as never, sessionId: sid('s1'), beforeSessionId: sid('s2') })
expect(anchored.result.ok).toBe(true)
const appended = await c.workspace.insertSessionBefore({ workspaceId: 'w1' as never, sessionId: sid('s1') })
expect(appended.result.ok).toBe(true)
})
it('passes business errors through as 200 + err result, not a throw', async () => {
@@ -202,6 +229,23 @@ describe('unary round trip', () => {
})
})
describe('workspace domain round trip', () => {
it('routes both workspace methods through their handler rows and value schemas', async () => {
const c = client(scriptedApi())
const list = await c.workspace.list({})
expect(list.result).toEqual({ ok: true, value: { items: [] } })
const created = await c.workspace.create({ path: '/t' })
expect(created.result.ok).toBe(true)
if (created.result.ok) expect(created.result.value.created).toBe(true)
})
it('rejects a create payload violating the exactly-one refine at the handler', async () => {
const response = await client(scriptedApi()).workspace.create({})
expect(response.result.ok).toBe(false)
if (!response.result.ok) expect(response.result.error.code).toBe('bad-request')
})
})
describe('SSE stream path', () => {
it('yields frames in order and skips the comment preamble', async () => {
const frames: MuxFrame[] = [
@@ -253,7 +297,7 @@ describe('SSE stream path', () => {
const api = scriptedApi({
events: {
async *host(request): AsyncGenerator<RpcRequest<HostFrame>> {
yield { rpcId: RpcId(`p-${request.rpcId}`), payload: { type: 'host/session-added', sessionId: sid('s1') } }
yield { rpcId: RpcId(`p-${request.rpcId}`), payload: { type: 'host/session-added', sessionId: sid('s1'), blank: true } }
throw new Error('impl died mid-stream')
},
},