From 5d173b170d2e883bf53ffeb98ac9adb1dcdee5bf Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Thu, 17 Sep 2026 16:09:29 +0800 Subject: [PATCH] test(web): strengthen plan dismissal checks and clarify review contracts --- .../2026-09-17-persistent-plan-cards.i18n.yaml | 4 ++-- .../feature/2026-09-17-persistent-plan-cards.md | 2 +- .../feature/2026-09-17-persistent-plan-cards.zh.md | 2 +- apps/web/tests/plan-review.e2e.ts | 12 +++++++++++- packages/client/ui-plan/package.json | 1 + .../client/ui-plan/src/client/PlanPreview.module.css | 6 +++--- packages/client/ui-plan/src/client/PlanPreview.tsx | 10 ++++++++-- packages/client/ui-plan/src/client/plan-resource.ts | 5 ++++- .../ui-user-questions/src/client/contract/slots.ts | 2 +- .../ui-user-questions/src/client/markdown-labels.ts | 2 +- 10 files changed, 33 insertions(+), 13 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.i18n.yaml b/.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.i18n.yaml index bf06dd8eed..c1121b7795 100644 --- a/.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.md -2026-09-17-persistent-plan-cards.md: 53d0ca028a08faacec8f967c002fb525e50c33ac -2026-09-17-persistent-plan-cards.zh.md: cfc2a67c17a5b4ed76a7cb9df49b6fdfb15e3c71 +2026-09-17-persistent-plan-cards.md: 341175aabc41100b98b7d2f16120fc9ac91a7020 +2026-09-17-persistent-plan-cards.zh.md: 8276bb6cc9985f0b856193a4ab861751089dd8e9 diff --git a/.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.md b/.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.md index 53d0ca028a..341175aabc 100644 --- a/.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.md +++ b/.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.md @@ -26,4 +26,4 @@ The document is read-only. The pending review shows its status, View full plan l ## Consequences -Plans survive review closure and browser reload without a new Session event or file. Reads of old plans may require several history pages. The provider validates saved addresses and logged arguments, and reports unavailable history explicitly. The recorded Web plan-review scenario covers both entry points, repeated opening, process collapse, approval, and reload; focused tests cover multiple submissions, PTC deduplication, cropped history, and resource failures. +Plans survive review closure and browser reload without a new Session event or file. Complete plan arguments retain a card even when the tool rejects the call, including calls outside plan mode; the card does not certify approval or execution. Reads of old plans may require several history pages. The provider validates saved addresses and logged arguments, and reports unavailable history explicitly. The recorded Web plan-review scenario covers both entry points, repeated opening, process collapse, approval, and reload; focused tests cover multiple submissions, PTC deduplication, cropped history, and resource failures. diff --git a/.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.zh.md b/.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.zh.md index cfc2a67c17..8276bb6cc9 100644 --- a/.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.zh.md +++ b/.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.zh.md @@ -26,4 +26,4 @@ Status: implemented ## Consequences -计划在审批关闭和浏览器刷新后仍可阅读,无需新增会话事件或文件。读取旧计划可能需要多个历史分页。provider 校验保存的地址和调用参数,并明确报告历史不可用。已有 Web 计划审批录制场景覆盖两个入口、重复打开、运行过程折叠、批准和刷新;定向测试覆盖多次提交、PTC 去重、裁剪历史及资源读取失败。 +计划在审批关闭和浏览器刷新后仍可阅读,无需新增会话事件或文件。即使工具拒绝调用,包括在计划模式之外调用,完整的计划参数也会保留卡片;卡片不代表批准或执行成功。读取旧计划可能需要多个历史分页。provider 校验保存的地址和调用参数,并明确报告历史不可用。已有 Web 计划审批录制场景覆盖两个入口、重复打开、运行过程折叠、批准和刷新;定向测试覆盖多次提交、PTC 去重、裁剪历史及资源读取失败。 diff --git a/apps/web/tests/plan-review.e2e.ts b/apps/web/tests/plan-review.e2e.ts index 6df0c8ef1d..1aae08e395 100644 --- a/apps/web/tests/plan-review.e2e.ts +++ b/apps/web/tests/plan-review.e2e.ts @@ -190,8 +190,17 @@ describe('web e2e: dismissed plan history', () => { await page.locator('[data-sidebar-right-toggle]').click() await review.getByRole('button', { name: 'Request changes', exact: true }).click() await settled + const call = events.find((event): event is SessionEvent<'tool/call'> => + event.type === 'tool/call' && event.data.name === 'exit_plan_mode') + expect(call).toBeDefined() const results = events.filter(event => event.type === 'tool/result') - expect(results.some(event => JSON.stringify(event).includes('dismissed the plan review'))).toBe(true) + const result = results.find(event => event.data.message.source.callId === call?.data.callId) + expect(result?.data.message.content[0]).toMatchObject({ type: 'tool-result', isError: true }) + expect(JSON.stringify(result)).toContain('dismissed the plan review') + expect(results.some(event => JSON.stringify(event).includes('Plan approved'))).toBe(false) + const modes = events.filter(event => event.type === 'plan/mode') + expect(modes).toHaveLength(1) + expect(modes[0]).toMatchObject({ data: { active: true } }) expect(await review.count()).toBe(0) const card = page.locator('[data-plan-card]') await card.waitFor({ state: 'visible' }) @@ -208,6 +217,7 @@ describe('web e2e: dismissed plan history', () => { await page.locator('[data-plan-card]').waitFor({ state: 'visible' }) expect(await page.locator('[data-plan-preview]').isVisible()).toBe(false) expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) } finally { await browser?.close() await scaffold.close() diff --git a/packages/client/ui-plan/package.json b/packages/client/ui-plan/package.json index a2e837347a..1fc5b2f465 100644 --- a/packages/client/ui-plan/package.json +++ b/packages/client/ui-plan/package.json @@ -33,6 +33,7 @@ "@deepseek-ai/dsh-client-ui-conversation", "@deepseek-ai/dsh-client-ui-sidebar-right", "@deepseek-ai/dsh-client-ui-chat", + "@deepseek-ai/dsh-client-ui-user-questions", "@deepseek-ai/dsh-client-resources", "@deepseek-ai/dsh-api-session-controller" ], diff --git a/packages/client/ui-plan/src/client/PlanPreview.module.css b/packages/client/ui-plan/src/client/PlanPreview.module.css index f320f6af77..de4a4660a4 100644 --- a/packages/client/ui-plan/src/client/PlanPreview.module.css +++ b/packages/client/ui-plan/src/client/PlanPreview.module.css @@ -22,7 +22,7 @@ } :global(body[data-ds-dark-theme]) .card { --plan-card-fill: var(--dsw-static-neutral-850); --plan-card-hover: var(--dsw-static-neutral-800); } .card:hover { background: var(--plan-card-hover); } -.card:focus-visible, .reviewOpen:focus-visible { outline: 2px solid var(--dsw-alias-label-primary); outline-offset: 3px; } +.card:focus-visible, .iconButton:focus-visible { outline: 2px solid var(--dsw-alias-label-primary); outline-offset: 3px; } .cardIcon { box-sizing: border-box; flex: none; display: grid; place-items: center; width: 40px; height: 40px; border: 0.5px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--plan-card-fill); } .cardDetails { display: flex; flex: 1; flex-direction: column; gap: 2px; min-width: 0; } .cardTitle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 500; line-height: 20px; } @@ -31,8 +31,8 @@ .reviewLink { display: inline-flex; align-items: center; gap: 4px; padding: 0; border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; cursor: pointer; } .reviewLink:hover { color: var(--dsw-alias-label-primary); } .reviewLink:focus-visible { outline: 2px solid var(--dsw-alias-label-primary); outline-offset: 3px; } -.reviewOpen { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; } -.reviewOpen:hover { background: var(--dsw-alias-interactive-bg-hover); } +.iconButton { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; } +.iconButton:hover { background: var(--dsw-alias-interactive-bg-hover); } .preview { position: relative; height: 100%; overflow: auto; padding: 20px 24px 40px; box-sizing: border-box; } .toolbar { display: flex; justify-content: flex-end; margin-bottom: 8px; } .document { color: var(--dsw-alias-label-primary); font-size: 14px; line-height: 1.75; overflow-wrap: anywhere; } diff --git a/packages/client/ui-plan/src/client/PlanPreview.tsx b/packages/client/ui-plan/src/client/PlanPreview.tsx index 71d2e6444e..ff0ad857b8 100644 --- a/packages/client/ui-plan/src/client/PlanPreview.tsx +++ b/packages/client/ui-plan/src/client/PlanPreview.tsx @@ -16,7 +16,13 @@ type PlanPreviewProps = PropsRuntime<'sidebar.right.pane.tab'> & PropsLocale<'pl export function PlanPreview({ useTabInfo, useResource, t }: PlanPreviewProps) { const tab = useTabInfo() const resource = useResource<'plan'>(tab.tab.navigation.address) - const labels = useMemo(() => ({ code: { copyLabel: t('copy'), copiedLabel: t('copied') }, footnotes: t('markdown.footnotes') }), [t]) + const labels = useMemo(() => ({ + code: { + copyLabel: t('copy'), copiedLabel: t('copied'), sourceLabel: t('markdown.code.source'), + lineNumbersLabel: t('markdown.code.lineNumbers'), + }, + footnotes: t('markdown.footnotes'), + }), [t]) const plan = resource.value if (plan === undefined) return (
@@ -26,7 +32,7 @@ export function PlanPreview({ useTabInfo, useResource, t }: PlanPreviewProps) { ) return (
-
diff --git a/packages/client/ui-plan/src/client/plan-resource.ts b/packages/client/ui-plan/src/client/plan-resource.ts index 695ee27386..467170ddef 100644 --- a/packages/client/ui-plan/src/client/plan-resource.ts +++ b/packages/client/ui-plan/src/client/plan-resource.ts @@ -1,4 +1,4 @@ -/** Read immutable plan arguments through the existing cold-safe Session history API. */ +/** Read immutable plan arguments from a Session snapshot and earlier history pages. */ import type { Context } from '@deepseek-ai/cordis' import type { SessionFollowFrame } from '@deepseek-ai/dsh-api-session-controller/types' import type { ResourceProvider } from '@deepseek-ai/dsh-client-resources/client' @@ -28,6 +28,9 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { /** * Bind plan reads to the generated Session Remote face. + * Opening a follow reads projections and may activate a prepared Session on the Host. + * Generated Remote streams can throw carrier failures; the provider reports failed + * reads as resource failure frames and preserves Remote error codes. * @param remote - Existing Session history API. * @returns a provider whose reads stop after finding the exact invocation. */ diff --git a/packages/client/ui-user-questions/src/client/contract/slots.ts b/packages/client/ui-user-questions/src/client/contract/slots.ts index 61445a1d12..0fcdd8b713 100644 --- a/packages/client/ui-user-questions/src/client/contract/slots.ts +++ b/packages/client/ui-user-questions/src/client/contract/slots.ts @@ -94,7 +94,7 @@ export function planReviewOf(questions: readonly QuestionItem[]): PlanReview | u id: question.id, question: question.question, plan: question.detail, - ...intent.callId === undefined ? {} : { callId: intent.callId }, + ...(intent.callId === undefined ? {} : { callId: intent.callId }), approve, ...(decline === undefined ? {} : { decline }), } diff --git a/packages/client/ui-user-questions/src/client/markdown-labels.ts b/packages/client/ui-user-questions/src/client/markdown-labels.ts index ff2e62fcaf..414982af25 100644 --- a/packages/client/ui-user-questions/src/client/markdown-labels.ts +++ b/packages/client/ui-user-questions/src/client/markdown-labels.ts @@ -1,4 +1,4 @@ -/** Localized Markdown controls shared by question and plan-review bodies. */ +/** Localized Markdown controls for question details. */ import type { MarkdownLabels } from '@deepseek-ai/dsh-client-ui-primitives' import type { QuestionComposerProps } from './contract/slots.ts'