fix(workspace): unify pin IDs and preserve complete session order

This commit is contained in:
imccyu committed 2026-09-19 12:54:13 +08:00
1 parent f1e06a69ef
commit 7baccf5d85
69 files changed
+956 -543

No files matched your search

@@ -251,7 +251,7 @@ describe('WorkspaceController commands', () => {
})
await expect(controller.pinSession({ sessionId: session.id }))
.resolves.toEqual({ pinnedSessions: [{ sessionId: session.id, pinnedAt: expect.any(Number) }] })
.resolves.toEqual({ pinnedSessionIds: [session.id] })
await expect(controller.pinSession({ sessionId: SessionId('unknown') }))
.rejects.toMatchObject({ code: 'session/not-found' })
@@ -264,10 +264,10 @@ describe('WorkspaceController commands', () => {
.rejects.toMatchObject({ code: 'gateway/bad-request' })
await expect(controller.unpinSession({ sessionId: session.id }))
.resolves.toEqual({ pinnedSessions: [] })
.resolves.toEqual({ pinnedSessionIds: [] })
// Unpin is idempotent: an id that is not pinned is not an error.
await expect(controller.unpinSession({ sessionId: session.id }))
.resolves.toEqual({ pinnedSessions: [] })
.resolves.toEqual({ pinnedSessionIds: [] })
})
})
@@ -290,19 +290,38 @@ describe('WorkspaceController follow', () => {
initialized: true,
workspaceIds: ['missing'],
archivedSessionIds: [],
pinnedSessions: [],
pinnedSessionIds: [],
},
})
}).toThrow('references missing Workspace "missing"')
})
it('starts a fresh feed with existing pins and follows their removal', async () => {
const { controller, ctx, root } = await harness()
const session = ctx.sessions.create(SessionId('already-pinned'), { meta: { cwd: root } })
await controller.pinSession({ sessionId: session.id })
const feed = new WorkspaceFeed(ctx)
const abort = new AbortController()
const iterator = feed.follow(abort.signal)[Symbol.asyncIterator]()
try {
await expect(nextFrame(iterator)).resolves.toMatchObject({
type: 'baseline', value: { pinnedSessionIds: [session.id] },
})
await controller.unpinSession({ sessionId: session.id })
await expect(nextFrame(iterator)).resolves.toEqual({ type: 'pinned', pinnedSessionIds: [] })
} finally {
abort.abort()
await iterator.return?.()
}
})
it('starts with a complete baseline and emits committed increments in domain order', async () => {
const { controller, ctx, root } = await harness()
const abort = new AbortController()
const iterator = controller.follow(abort.signal)[Symbol.asyncIterator]()
await expect(nextFrame(iterator)).resolves.toEqual({
type: 'baseline',
value: { items: [], archivedSessionIds: [], pinnedSessions: [] },
value: { items: [], archivedSessionIds: [], pinnedSessionIds: [] },
})
const first = await controller.create({ path: stageDir(root, 'first') })
@@ -347,12 +366,12 @@ describe('WorkspaceController follow', () => {
})
await controller.pinSession({ sessionId: session.id })
await expect(nextFrame(iterator)).resolves.toEqual({
type: 'pinned', pinnedSessions: [{ sessionId: session.id, pinnedAt: expect.any(Number) }],
type: 'pinned', pinnedSessionIds: [session.id],
})
// Unpin rides the same complete-set increment: no new frame type.
await controller.unpinSession({ sessionId: session.id })
await expect(nextFrame(iterator)).resolves.toEqual({
type: 'pinned', pinnedSessions: [],
type: 'pinned', pinnedSessionIds: [],
})
await controller.delete({ workspaceId: second.workspace.workspaceId })
await expect(nextFrame(iterator)).resolves.toEqual({