diff --git a/src/server/page-service.ts b/src/server/page-service.ts index 4b55f817..b103ad8a 100644 --- a/src/server/page-service.ts +++ b/src/server/page-service.ts @@ -75,6 +75,9 @@ export class PageService { name: page.title, }), ); + // The page may have been deleted while Intelligence created its thread. + // Recheck before binding a local conversation or marking it ready. + this.workspace.pages.get(spaceId, pageId); if (!this.workspace.canAccessSpace(dotId, spaceId)) throw new PageError('Space access has been revoked.'); const thread = diff --git a/tests/page-conversation-deletion.test.ts b/tests/page-conversation-deletion.test.ts new file mode 100644 index 00000000..1689168f --- /dev/null +++ b/tests/page-conversation-deletion.test.ts @@ -0,0 +1,37 @@ +import { expect, it, vi } from 'vitest'; +import { WorkspaceStore } from '../src/server/workspace.js'; +import { PageService } from '../src/server/page-service.js'; + +it('does not bind a conversation when its page is deleted during thread creation', async () => { + const ws = new WorkspaceStore(':memory:', 'owner'); + try { + const dot = ws.dots()[0]; + const page = ws.pages.create(dot.spaceId, { title: 'Temporary' }); + let resolve!: () => void; + const remote = new Promise((done) => { + resolve = done; + }); + const getOrCreateThread = vi.fn(() => remote); + const service = new PageService(ws, () => ({ + getOrCreateThread, + getThreadMessages: async () => ({ messages: [] }), + })); + const pending = service.conversation(dot.spaceId, page.id, dot.id); + const outcome = expect(pending).rejects.toThrow(/Page not found/); + expect(getOrCreateThread).toHaveBeenCalledTimes(1); + ws.pages.delete(dot.spaceId, page.id); + resolve(); + await outcome; + expect(ws.conversations()).toHaveLength(0); + expect(ws.pages.thread(page.id, dot.id)).toBeUndefined(); + const replacement = ws.pages.create(dot.spaceId, { title: 'Replacement' }); + const thread = await service.conversation( + dot.spaceId, + replacement.id, + dot.id, + ); + expect(ws.pages.forThread(thread.id)?.id).toBe(replacement.id); + } finally { + ws.close(); + } +});