Repository navigation
fix(webui): questionnaire goal countdown honesty + missing-presentation hardening (I-1) - #25
Merged
Merged
Conversation
…ve a missing presentation block
Two defects, both reproduced first against the built client before any
change:
1. A multi-step questionnaire whose presentation block is absent threw
'Cannot read properties of undefined (reading showProgress)' and
replaced the whole interaction surface with the error boundary (a
white page before Q-1). The wire type declares presentation required,
but the runtime's view builder materialises it with a spread — {}
for a payload that never had one — and every producer-side
normaliser (DEFAULT_PRESENTATION in local-runtime, the TUI's
event-normalizer) defaults the three fields to true. The progress
row now reads them through '?? true', the same convention the
replaceComposer read two siblings up already followed.
2. The expiry countdown was shown for ANY questionnaire carrying
expiresAt, titled 'will auto-submit in N seconds', and parked at a
bare zero when the window closed. All three claims were dishonest:
the runtime's QuestionnaireAutoReplyScheduler replies only for
purpose === 'goal' (applying each step's recommended option, with a
CAS so a concurrent manual answer wins), and the renderer is
display-only by that module's own contract. The countdown is now
gated to the goal window, titled 'automatically apply the
recommended option', marks that option (推荐) while the window is
open — the TUI's '(Recommended)' affordance — and says the window is
over at zero instead of parking at 0s. No client-side auto-submit is
added on purpose: the scheduler owns it, and a second submit from
the renderer would race the CAS that exists to let a manual answer
win.
webui-round3-acceptance's countdown fixture gains purpose: 1 so it
still exercises the window it always meant to (its request carried an
expiry with no purpose, which is exactly the dishonest shape removed).
Tests: questionnaire-goal-autoreply.test.tsx (7 cases — goal gating,
honest title, recommended mark, zero state, missing-presentation
render, explicit showProgress:false honoured) and
questionnaire.spec.mjs (5 browser cases — required-gated stepping
forward/back, multi-select, custom answer, submit payload on the wire,
close/skip routing, goal countdown contract, missing-presentation
render). The browser fixture gains setQuestionnaire so a request can
be staged for the composer's poll.
modacker
pushed a commit
that referenced
this pull request
Oct 5, 2026
#25 tightened the goal auto-reply window to `purpose === 1 && expiresAt !== undefined`, which matches what the runtime actually acts on. The guard still let a non-numeric `expiresAt` through, and the countdown clamps to zero: `null` rendered as "⏱ 0s" and `NaN` rendered the literal string "NaNs" into the panel. Both claim a deadline the payload never carried. `goalAutoReplyDeadline` in the terminal client guards with `typeof expiresAt === "number" && Number.isFinite(expiresAt)`, and this panel's comment already points at that function as its reference. Take the same pair. Zero and a past timestamp stay valid — they are real deadlines that have passed. Also corrects the premise in the MCP status suite. `LocalMcpPublicServerStatus` does classify every server, but that is the MCP tool surface; the plugin page's `listConfiguredServers` → `configuredSummary` returns no `status` at all and hardcodes `configJson` to `"{}"`, so every real row reads 未知状态 and the badge is inert until that path carries the field. The suite now pins today's honest answer with the real server shape, so the day the field arrives the change is visible rather than silent.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Change
Roadmap I-1 (多步问卷交互): from 「组件在、没人逐项实测」 to item-by-item verified in a real browser, plus two defects found by that verification and fixed. Both were reproduced against the built client before any change was made.
Suspect 2a — countdown auto-submit: investigated, verdict is NOT a client gap
QuestionnaireAutoReplyScheduler(packages/local-runtime/src/questionnaire/auto-reply-scheduler.ts) fires only forpurpose === 'goal', andGoalQuestionnaireService.runAutoReplyapplies each step's recommended option (falls back to the first option, skips a step with none) through the real reply path, with version/CAS arbitration so a concurrent manual reply/dismiss wins. Its own contract: "The renderer may display a countdown, but only this runtime-owned scheduler mutates questionnaire state."⏱ 6sshown, title 「将在 6 秒后自动提交」; at zero it parked at⏱ 0s; noreplyQuestionnairewas sent by the client — as the architecture intends.InteractionPanel.tsx:expiresAt— reproduced with a purpose-less request (countdown appeared) even though the runtime answers goal-only. Now gated topurpose === 1(QuestionnairePurpose.Goalon this wire), matching the scheduler and the TUI'sgoalAutoReplyDeadline.⏱ 0s. Now 「⏱ 时间到 · 正在采用推荐选项…」 — the TUI's "Time is up · applying the recommended option…" beat.Suspect 2b — missing presentation: confirmed throw, fixed
TypeError: Cannot read properties of undefined (reading 'showProgress')— the render died and the error boundary replaced the whole surface (a white page before Q-1). A single-step request survives only becausesteps.length > 1short-circuits the read.toQuestionnaireRequestView) materialises the block with{...request.presentation}—{}when absent — and every producer-side normaliser (DEFAULT_PRESENTATIONin local-runtime, the TUI's event-normalizer) defaults the three fields to true.presentation?.showProgress ?? trueandpresentation?.allowBackNavigation ?? true— the same convention thereplaceComposerread two siblings up already followed. After the fix the reproduced payload renders with the progress row (default true) instead of throwing.Validation — the item-by-item browser record (the hard requirement)
Environment for every row below: built client (
dist-webuifrom this branch), headless Chromium 153, 1440×900, zh-CN; the fixture transport serves controlledgetPendingQuestionnairepayloads; staging a questionnaire and switching sessions (A↔B) triggers the composer's poll. "Clicks" = real Playwright clicks/typing, not evaluate calls, except where noted.‹1/3›→‹2/3›; 提交 absent on non-final stepsdata-selected=true; toggling works‹1/3›, first step's selection retaineddata-selected=true, typed text acceptedreplyQuestionnairesent once; payload verified exactly:[{s1:[o1a]},{s2:[o2a,o2c]},{s3:selectedOther+otherText}]dismissQuestionnaire+1dismissQuestionnaire+1; noreplyQuestionnaireever sent⏱ Ns, title 「自动采用推荐选项」, recommended option marked (推荐), other unmarked; at zero 「⏱ 时间到 · 正在采用推荐选项…」‹1/2›, no error boundary (threw before the fix)Automated:
pnpm test:webui— 81 files / 1624 tests passed, including newquestionnaire-goal-autoreply.test.tsx(7 cases).npx playwright test— 85/85 passed, including newquestionnaire.spec.mjs(5 cases, harness-importedtestper the serverId guard).pnpm typecheck:webui-full,pnpm check:source(inventory regenerated),pnpm verify— passed, 20 gates on darwin.webui-round3-acceptance.test.tsx's countdown request gainspurpose: 1— its request carried an expiry with no purpose, exactly the dishonest shape this PR removes; the assertion (countdown renders inside the goal window) is unchanged in intent. No browser spec was modified.NOT RUN / boundaries
pnpm dev:serversession. No live-service acceptance claimed.Boundaries (per the task brief)
DESKTOP_SETTINGS_TABSinSettingsModal.tsx, the same array E-zone (安天齐, PR fix(webui): report a failed revert/reapply instead of relabelling it a missing capability #20 in flight), K-zone (izzy) and P-zone (unclaimed) are unlocking tabs in. It should be its own PR, coordinated with those three first, so the same tab is not unlocked twice.1304e4d(webui HEAD at 10-05 16:06), not from any in-flight branch.Publication and contribution checks
release/public-source.json; new tests are declared intest/vitest-suites.jsonwhere applicable.Maintainer handoff
Publication scope or license changes (if any): none.
Shared-source port: not needed.