From 6b1e3937d709b74d407379c210a4bf09e8f4b158 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Mon, 7 Sep 2026 23:48:28 +0800 Subject: [PATCH] test(design): cover chat quote handoff --- ...chat-generation-handoff-client-7d3a9c41.md | 92 +++++++++++++ tests/unit/image-workspace-store.test.ts | 130 ++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 .project-docs/30-worklog/tasks/20260907-design-chat-generation-handoff-client-7d3a9c41.md diff --git a/.project-docs/30-worklog/tasks/20260907-design-chat-generation-handoff-client-7d3a9c41.md b/.project-docs/30-worklog/tasks/20260907-design-chat-generation-handoff-client-7d3a9c41.md new file mode 100644 index 0000000..c442f51 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260907-design-chat-generation-handoff-client-7d3a9c41.md @@ -0,0 +1,92 @@ +# Task: Handle AI Design chat generation handoff + +## Identity + +- Task ID: 20260907-design-chat-generation-handoff-client-7d3a9c41 +- Mode: Feature +- Branch: codex/20260907-design-chat-generation-handoff-client-7d3a9c41-design-chat-generation-handoff +- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260907-design-chat-generation-handoff-client-7d3a9c41 +- Base commit: bce20d9e4dad99565714bd2c0ca1ef4806426f57 +- Owner: codex-root +- Status: Ready for Integration + +## Scope + +- Verify that the existing client event contract can consume a server-owned + `开始生成` handoff without introducing a second Renderer command or public DTO. +- Preserve the original optimistic user message, activity/reply settlement, stable + operation identity, and the offered Quote created immediately after the committed + chat event. +- Add focused Store coverage for two consecutive Direction projections belonging to + the same accepted chat command. + +## Intent And Constraints + +- Electron Main remains the only network authority. Source inspection showed that no + new Renderer orchestration is needed: the server can emit the committed chat and + subsequent Quote as two existing `design.direction.updated` events in one Run. +- Never infer generation intent from assistant prose or duplicate phrase matching in + the client. The server owns interpretation and the Quote transaction. +- Keep Quote and generation confirmation separate: automatic handoff may create an + immutable Quote, but only explicit user confirmation may create a Task or reserve + Token Points. +- Derive a stable follow-up operation identity from the committed chat Interaction, + and do not replay an already accepted/unknown mutation under a new identity. + +## Project Context Loaded + +- Task context: feature task `20260907-design-chat-generation-handoff-client-7d3a9c41` + owns this managed linked worktree at base `bce20d9e…`. +- Read: agent-entry and planning-gate docs, memory index, project positioning, + integrated current state, decision index/system overview, ADR-007, architecture + module/data-flow maps, domain/success criteria, evidence/reflection/commitment/stale + indexes, and the recent Design activity and repeated-turn peer task records. +- Project goal: a youth-friendly Canvas whose Current Specification, immutable Quote, + and confirmed Task remain server-authoritative. +- Current integrated focus: optimistic chat, operation-scoped activity and committed + assistant streaming are already present; repeated identical user text is valid. +- Decisions affecting this task: ADR-007 requires stable operation identity, Main-owned + transport, immutable Quote display, and explicit Quote-ID confirmation. +- Evidence and commitments: production logs show an input Run completed without any + Quote/Task command; the real paid confirmation smoke remains separately pending. +- Likely modules: `shared/image-workspace.ts`, the Works Square adapter, Renderer + image-workspace Store, and their focused tests. +- Unknowns/conflicts: source inspection must identify the narrowest post-commit event + seam and ensure reconnect/replay cannot create duplicate Quote requests. The root + worktree's unrelated untracked records are outside this isolated task. No active + peer has a conflicting Design scope. +- Gate result: Passed. + +## Outcome + +- Confirmed that Main maps every Gateway `design.direction.updated` event + independently and the Store applies canonical form projections even after the + original optimistic operation has settled. +- Added a Store regression for the real sequence: committed `chat` projection, + immediately followed by `quote_request` with the same client operation identity. + The test proves the draft/pending chat settles once, the completed activity remains + bound to its Turn, and the offered Quote remains visible. +- No client product code, shared contract, routing, or UI change was necessary. + +## Verification + +- `pnpm exec vitest run tests/unit/image-workspace-store.test.ts --maxWorkers=1` + passed (33 tests). +- `pnpm exec eslint tests/unit/image-workspace-store.test.ts` passed. +- `pnpm run typecheck` passed. +- `git diff --check` passed. + +## Follow-ups + +- Integrate and deploy with server task + `20260907-design-chat-generation-intent-4f8c2a91`, then perform the recorded real + Quote/confirmation smoke. + +## Promotion Candidates + +- Target: `.project-docs/20-architecture/data-flow.md` and integrated current state. + Proposal: record that one accepted Design chat Run may carry a committed chat + Direction projection followed by an immutable Quote projection under the same + client command identity; the Store converges both without issuing a second + mutation. Evidence is the 33-test Store suite and the cross-repository server task. + This is compatible with ADR-007 and needs no separate product-direction decision. diff --git a/tests/unit/image-workspace-store.test.ts b/tests/unit/image-workspace-store.test.ts index 49bb83d..94944e6 100644 --- a/tests/unit/image-workspace-store.test.ts +++ b/tests/unit/image-workspace-store.test.ts @@ -14,6 +14,7 @@ import { useImageWorkspaceStore } from '@/stores/image-workspace'; import { designBootstrapFixture, designFormFixture, + designQuoteFixture, designSummaryFixture, designWorkspaceFixture, } from '../fixtures/design-workspace-v2'; @@ -688,6 +689,135 @@ describe('V2 Living Form store', () => { }); }); + it('keeps the Quote created immediately after a committed generation chat', async () => { + const source = await loadedStore(); + const message = '开始生成'; + const quoteForm = designFormFixture({ + directionRevision: 6, + rawTurnSequence: 2, + workspaceViewRevision: 8, + activeQuotes: [designQuoteFixture()], + }); + const canonical = designWorkspaceFixture({ + form: quoteForm, + turns: [ + ...designWorkspaceFixture().turns, + { + turnId: 'turn-2', + rawTurnSequence: 2, + userMessage: message, + assistantMessage: '我先检查制作方案。准备好后需要你确认一次,确认后才会开始生成。', + }, + ], + }); + fetchProjectMock.mockResolvedValue(canonical); + useImageWorkspaceStore.setState({ + chatDraft: message, + pendingOperations: { + 'operation-1': { + id: 'operation-1', + label: '发送创作想法', + command: { + kind: 'apply_input', + workspaceId: 'workspace-1', + sessionId: 'session-1', + expectedDirectionRevision: 4, + clientOperationId: 'operation-1', + input: { kind: 'chat', message }, + }, + status: 'submitting', + error: null, + clearDraftPaths: [], + clearChatDraft: true, + baseRawTurnSequence: 1, + }, + }, + assistantActivities: { + 'operation-1': { + workspaceId: 'workspace-1', + directionId: 'direction-1', + status: 'submitting', + turnId: null, + steps: [], + }, + }, + }); + + source.emit('design.direction.updated', { + id: 'session-1:19', + type: 'design.direction.updated', + clientOperationId: 'operation-1', + replayed: false, + operation: { + operationKind: 'chat', + interactionId: 'interaction-2', + baseDirectionRevision: 4, + newDirectionRevision: 5, + rawTurnSequence: 2, + specificationRevision: 3, + specificationRevisionId: 'specification-revision-3', + workspaceViewRevision: 7, + specificationRevisionCreated: false, + meaningChanged: false, + changeSet: { interaction_id: 'interaction-2', changes: [] }, + turnId: 'turn-2', + assistantMessage: '我先检查制作方案。准备好后需要你确认一次,确认后才会开始生成。', + createdDecisionPromptIds: [], + resolvedDecisionPromptId: null, + supersededDecisionPromptIds: [], + supersededQuoteCount: 0, + quoteId: null, + generationTaskId: null, + }, + form: designFormFixture({ + directionRevision: 5, + rawTurnSequence: 2, + workspaceViewRevision: 7, + }), + }); + source.emit('design.direction.updated', { + id: 'session-1:20', + type: 'design.direction.updated', + clientOperationId: 'operation-1', + replayed: false, + operation: { + operationKind: 'quote_request', + interactionId: 'interaction-quote-1', + baseDirectionRevision: 5, + newDirectionRevision: 6, + rawTurnSequence: 2, + specificationRevision: 3, + specificationRevisionId: 'specification-revision-3', + workspaceViewRevision: 8, + specificationRevisionCreated: false, + meaningChanged: false, + changeSet: { interaction_id: 'interaction-quote-1', changes: [] }, + turnId: null, + assistantMessage: null, + createdDecisionPromptIds: [], + resolvedDecisionPromptId: null, + supersededDecisionPromptIds: [], + supersededQuoteCount: 0, + quoteId: 'quote-1', + generationTaskId: null, + }, + form: quoteForm, + }); + + await vi.waitFor(() => { + expect(useImageWorkspaceStore.getState()).toMatchObject({ + chatDraft: '', + pendingOperations: {}, + workspace: { form: { directionRevision: 6 } }, + }); + }); + expect(useImageWorkspaceStore.getState().workspace?.form.activeQuotes) + .toEqual([designQuoteFixture()]); + expect(useImageWorkspaceStore.getState().assistantActivities).toMatchObject({ + 'operation-1': { status: 'completed', turnId: 'turn-2' }, + }); + }); + it('keeps the optimistic chat until a terminal success is visible in the canonical Workspace', async () => { const source = await loadedStore(); const message = '画一只在月球踢球的熊猫';