Merge remote-tracking branch 'origin/master' into worktree/custom-deepseek-models

# Conflicts:
#	packages/host/apiproxy/README.i18n.yaml
This commit is contained in:
Yichen Jiang
2026-07-31 14:48:50 +08:00
113 changed files with 1614 additions and 278 deletions

View File

@@ -441,4 +441,44 @@ describe('Host Workspace increments', () => {
expect(expectOk(await api.sessions.list(request({}))).items.map(item => item.sessionId)).toContain(sessionId)
abort.abort()
})
it('archives a session into the global set, keeps its accounting, and streams the set once', async () => {
const { api } = await harness()
const workspace = expectOk(await api.workspace.create(request({ name: 'archive-home' }))).workspace
const sessionId = SessionId('session-to-archive')
expectOk(await api.sessions.create(request({ workspaceId: workspace.workspaceId, sessionId })))
expect(expectOk(await api.workspace.list(request({}))).archivedSessionIds).toEqual([])
const abort = new AbortController()
const stream: AsyncIterator<RpcRequest<HostFrame>> =
api.events.host(request({}), abort.signal)[Symbol.asyncIterator]()
const changed = nextHostFrame(stream)
expect(expectOk(await api.workspace.archiveSession(request({ sessionId }))).archivedSessionIds)
.toEqual([sessionId])
expect(await changed).toMatchObject({
payload: { type: 'host/archived-sessions-changed', archivedSessionIds: [sessionId] },
})
// Accounting and the session itself are untouched; list re-baselines the set.
const listed = expectOk(await api.workspace.list(request({})))
expect(listed.archivedSessionIds).toEqual([sessionId])
expect(listed.items[0]?.sessionIds).toEqual([sessionId])
expect(expectOk(await api.sessions.list(request({}))).items.map(item => item.sessionId)).toContain(sessionId)
// The idempotent repeat emits no second frame: the next observed frame is
// the workspace-changed of a later attach, not another archive snapshot.
const after = nextHostFrame(stream)
expect(expectOk(await api.workspace.archiveSession(request({ sessionId }))).archivedSessionIds)
.toEqual([sessionId])
const otherSession = SessionId('session-after-archive')
expectOk(await api.sessions.create(request({ workspaceId: workspace.workspaceId, sessionId: otherSession })))
expect((await after).payload.type).not.toBe('host/archived-sessions-changed')
const missing = await api.workspace.archiveSession(request({ sessionId: SessionId('session-ghost') }))
expect(missing.result).toMatchObject({
ok: false,
error: { code: 'session-not-found', details: { sessionId: 'session-ghost' } },
})
abort.abort()
})
})

View File

@@ -66,11 +66,12 @@ function scriptedApi(overrides: {
...overrides.host,
},
workspace: {
list: r => ok(r, { items: [] }),
list: r => ok(r, { items: [], archivedSessionIds: [] }),
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' } }),
delete: r => ok(r, { deleted: true as const }),
insertSessionBefore: r => ok(r, { workspace: { workspaceId: 'w1' as never, path: '/t', title: 't', sessionIds: [], createdAt: '0', updatedAt: '0' } }),
archiveSession: r => ok(r, { archivedSessionIds: [r.payload.sessionId] }),
},
commands: {
list: r => ok(r, { commands: [] }),
@@ -360,10 +361,12 @@ 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: [] } })
expect(list.result).toEqual({ ok: true, value: { items: [], archivedSessionIds: [] } })
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)
const archivedResponse = await c.workspace.archiveSession({ sessionId: 's-arch' as never })
expect(archivedResponse.result).toEqual({ ok: true, value: { archivedSessionIds: ['s-arch'] } })
})
it('rejects a create payload violating the exactly-one refine at the handler', async () => {

View File

@@ -122,7 +122,7 @@ function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFra
},
workspace: {
async list(request) {
return { rpcId: request.rpcId, result: { ok: true, value: { items: [] } } }
return { rpcId: request.rpcId, result: { ok: true, value: { items: [], archivedSessionIds: [] } } }
},
async create(request) {
return {
@@ -145,6 +145,9 @@ function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFra
result: { ok: true, value: { workspace: { workspaceId: 'w1' as never, path: '/w', title: 'w', sessionIds: [], createdAt: 't', updatedAt: 't' } } },
}
},
async archiveSession(request) {
return { rpcId: request.rpcId, result: { ok: true, value: { archivedSessionIds: [request.payload.sessionId] } } }
},
},
commands: {
async list(request) {

View File

@@ -20,6 +20,7 @@ import {
hostListDirectoryRequestSchema, hostListDirectoryValueSchema,
} from '../src/api/host.schema.ts'
import {
workspaceArchiveSessionRequestSchema, workspaceArchiveSessionValueSchema,
workspaceCreateRequestSchema, workspaceCreateValueSchema, workspaceIdSchema,
workspaceDeleteRequestSchema, workspaceDeleteValueSchema,
workspaceInsertSessionBeforeRequestSchema, workspaceInsertSessionBeforeValueSchema,
@@ -311,7 +312,16 @@ describe('workspace domain schemas', () => {
expect(workspaceViewSchema.parse(view).sessionIds).toEqual(['s1'])
expect(() => workspaceViewSchema.parse({ ...view, sessionIds: 's1' })).toThrow()
expect(workspaceListRequestSchema.parse({})).toEqual({})
expect(workspaceListValueSchema.parse({ items: [view] }).items).toHaveLength(1)
expect(workspaceListValueSchema.parse({ items: [view], archivedSessionIds: ['s1'] }).items).toHaveLength(1)
expect(() => workspaceListValueSchema.parse({ items: [view] })).toThrow()
})
it('archiveSession request/value carry the id and the full updated set', () => {
expect(workspaceArchiveSessionRequestSchema.parse({ sessionId: 's1' }).sessionId).toBe('s1')
expect(() => workspaceArchiveSessionRequestSchema.parse({})).toThrow()
expect(workspaceArchiveSessionValueSchema.parse({ archivedSessionIds: ['s1', 's2'] }).archivedSessionIds)
.toEqual(['s1', 's2'])
expect(() => workspaceArchiveSessionValueSchema.parse({ archivedSessionIds: 's1' })).toThrow()
})
it('create requires exactly one of path/name (both refine arms)', () => {