Files
writer-work-flow/docs/qa/frontend-qa-2026-06-24.md
Yaojia Wang 2fe3bedfba fix(qa): 修 QA C1/H1/H2——写章/规则缺项目校验 + 立项向导字段覆盖
C1 (CRITICAL) stream_draft:对不存在 project 流式写章先 fail-fast 404,
  否则非法 id 静默烧一次付费/限流 LLM 调用并返 200。在触网关前查 project_repo.get。
H2 (HIGH) create_rule:给不存在 project 加规则原 FK 违例逃逸成 500 → 改为入库前
  校验项目存在返 404(仿 chain/_require_project)。
H1 (HIGH) ProjectWizard:第3步「立意」与第4步「主角/金手指」原共用 form.premise
  互相覆盖丢数据 → 新增独立 form.protagonist,toCreateRequest 合并两段进 premise
  (M1 projects 表仍只有 premise,不编造 API)。

回归测试:
- test_projects.py:stream_draft 不存在 project → 404 且网关零调用;已有 draft
  用例改 seed 真项目。
- test_rules.py:create_rule 不存在 project → 404 不写库;已有用例 seed 真项目。
- wizard.test.ts:premise+protagonist 合并不互相覆盖(2 例)。
门禁绿:ruff/format clean · mypy 210 · pytest 749 · 前端 tsc/lint/vitest 干净。
2026-06-24 17:17:35 +02:00

13 KiB

Web Frontend QA Report

1. Executive Summary

Overall health: Solid at the contract/logic layer, blocked at the browser layer.

  • Areas tested: 15 (12 functional+API areas, 2 full-LLM E2E areas, 1 cross-route browser smoke pass).
  • Backend contract + component logic: Largely sound. Happy paths, validation (422), error envelopes (404/409), snake_case alignment, optimistic-update/rollback, and a11y structure mostly check out across all areas.
  • Full LLM E2E that genuinely ran: write-sse (real Kimi draft stream, 1261 tokens), toolbox-generators (all 12 generators + 3 ingest flows), style (16-dim fingerprint + refine), chain (API + awaiting/resume seam verified against real jobs).
  • What could NOT be verified (environmental): The running Next dev server had a corrupted .next build cache — every /projects/[id]/* SSR route returned HTTP 500 ("Cannot find module openapi-fetch@0.13.8.js"), and client JS chunks 404'd so React never hydrated. This blocked ALL in-browser/visual/keyboard/a11y verification. Root cause: stale build + two concurrent next dev processes; not a source defect. Fix: kill duplicate server, rm -rf apps/web/.next, restart.

Issue counts (de-duplicated): 23 total

  • CRITICAL: 2 (1 functional, 1 build-cache infra)
  • HIGH: 4 (1 functional, 3 infra — same root cause)
  • MEDIUM: 6
  • LOW: 11

The two functional CRITICAL/HIGH items are real and independent of the build issue. The build-cache 500 is a single environmental root cause counted once as CRITICAL (smoke pass) and re-observed as HIGH across multiple areas.

2. All Recorded Errors (de-duplicated, CRITICAL → LOW)

Severity Area / Component Symptom Repro
CRITICAL write-sse — routers/projects.py::stream_draft POST /draft for a NON-EXISTENT project returns 200 + a full LLM-generated chapter instead of 404 — burns a paid LLM call; sibling endpoints correctly 404 curl -X POST /projects/0000…0000/chapters/7/draft -H 'Accept: text/event-stream' → 200 + token/done
CRITICAL Build cache — apps/web/.next (all 10 project routes + hydration) Corrupt .next: server bundles require missing vendor-chunks/openapi-fetch@0.13.8.js → all /projects/[id]/* routes 500; client JS chunks 404 → React never hydrates → entire app non-interactive curl localhost:3000/projects/{PID}/write → 500; type valid title in wizard → 下一步 stays disabled
HIGH ProjectWizard.tsx (StepPremise + StepProtagonist) Step 3 (立意) and Step 4 (主角/金手指) bind to the SAME form.premise field — step-4 input silently overwrites step-3; both cannot be saved /projects/new → type 立意 (step3) → type 主角 (step4) → submit → only step-4 text persists
HIGH Build-cache 500 (re-observed in foreshadow / codex / skills / review / nav / chain) Same missing-openapi-fetch vendor chunk 500s every /projects/[id]/* route, blocking browser QA curl localhost:3000/projects/{PID}/{route} → 500
HIGH GET /skills backend registry Skills registry empty ({"skills":[]}); loads only from DB skills table with no seed rows and no POST/seed endpoint → SkillsPage permanently shows "暂无已注册技能" curl localhost:8000/skills{"skills":[]}
HIGH env — duplicate next dev processes Two concurrent next dev on same apps/web clobber shared .next → likely root cause of the corrupt build ps aux | grep 'next dev' → two PIDs
HIGH rules — routers/rules.py + rule_repo.py POST rule to non-existent project returns 500 (unhandled FK violation) instead of 404 curl -X POST /projects/{random-uuid}/rules -d '{"level":"project","content":"x"}' → 500
HIGH ReviewReport.tsx + style.md prompt + review_node One-click 回炉 can target wrong/empty paragraph: style-auditor LLM segment idx and frontend split(/\n{2,}/) paragraph index use unrelated splitting schemes (no shared contract) Draft with single-newline segments → style segments idx>0 → 回炉 refines wrong/empty para
MEDIUM OutlineEditor.tsx + server.ts fetchOutline 卷号 volume selector is misleading: GET ignores volume filter, editor always renders ALL volumes; picker only affects POST generate Change 卷号 to 2 → all volumes still listed
MEDIUM useOutline.ts:44-49 422 validation errors read apiError.error.code, but 422 uses {detail:[...]} → degrades to generic "OUTLINE_FAILED" message POST /outline volume=0 → generic error, validation cause lost
MEDIUM settings PUT /settings/providers + ProvidersSettings PUT accepts arbitrary/unknown tier+provider (no enum/whitelist), upsert-only with no DELETE → bogus tier_routing row persists invisibly, pollutes gateway config PUT {tier_routing:[{tier:'bogus',…}]} → 200, persists, unremovable via API
MEDIUM backend char persist/read + CodexPage.tsx Character relations silently dropped on persist/read (GET returns relations:[] despite ingest); codex list never renders relations anyway POST character with relations → GET → relations:[]
MEDIUM backend POST /projects/{id}/characters Character ingest non-idempotent: re-ingesting same name creates duplicate row → duplicate chips (UI dedup only session-vs-persisted) POST same card twice → GET count 2
MEDIUM foreshadow PATCH to_status (backend) Invalid to_status value returns 500 INTERNAL instead of 422 VALIDATION; to_status is a free string, not enum-validated PATCH /foreshadow/FS-01 -d '{"to_status":"BOGUS"}' → 500
MEDIUM ChainPage.tsx:61 + ChainAdjudication.tsx:102 Chain run/resume failures call friendlyError(undefined) — discard error code/message; 503 LLM_UNAVAILABLE loses its "go to settings" action link Trigger run with no provider → generic "出错了" toast, no settings link
MEDIUM generation.py list_rules (GET rules) GET rules for non-existent project returns 200 {"rules":[]} instead of 404, masking bad project ids curl /projects/{random-uuid}/rules → 200 empty
LOW rules / templates / foreshadow / projects / codex (CRUD gaps) No DELETE/edit endpoint for rules; no DELETE /projects; whitespace-only content accepted server-side (min_length before trim) on rules/projects/foreshadow → unremovable junk rows curl -X POST .../rules -d '{"content":" "}' → 201
LOW useDraftStream.ts (pre-stream error) Pre-stream error envelope: extracts code+message but drops error.request_id → weakens end-to-end greppability Force 503 before stream → surfaced error has no request_id
LOW FastAPI 422 envelope inconsistency (write-sse, outline, accept, injection) 422 errors return raw FastAPI {detail:[...]} instead of project {error:{code,message,request_id}} envelope → no request_id, no code mapping curl /projects/{PID}/chapters/abc/injection → 422 {detail:…}
LOW apps/api accept endpoint (useAccept.ts) Accept 422 (empty final_text) uses raw FastAPI detail shape, not standard envelope (subset of above, accept-specific) POST accept {final_text:'',decisions:[]} → 422 raw detail
LOW ProvidersSettings.tsx testConnection Test-connection collapses distinct 404 "needs key" vs 503 "probe failed" into one generic toast Click test on unconfigured vs unreachable provider → identical toast
LOW ProjectWizard.tsx submit + cards.ts + useGenerator Create/generate failures show generic toast; backend validation detail / specific message discarded Force a 422 → generic toast, backend reason lost
LOW CommandPalette.tsx Incomplete combobox a11y: input not role=combobox, no aria-activedescendant; <li role=option> has no id → AT won't announce highlighted command Open ⌘K, ArrowDown with screen reader → no announcement
LOW Toast.tsx Error toasts use polite live region (aria-live=polite/role=status) instead of assertive → failures don't interrupt SR show('…','error') → announced politely
LOW RulesPage.tsx Rule content textarea has no accessible label (placeholder only); list uses array index as React key (forced by GET not returning rule id) Inspect 新增规则 form
LOW TemplateFiller.tsx:23-40 After transient GET /templates failure, shows misleading "暂无模板" empty state and never retries on reopen (useEffect guards on templates===null, but failure sets []) Open filler while API down → toast + empty; restore API, reopen → still empty
LOW style refine_segment (routers/style.py) Refine endpoint never validates chapter existence; any chapter_no returns 200 POST .../chapters/99/refine -d '{"segment":"x"}' → 200
LOW TemplatesManager + backend No max-length cap on title/body (5000-char title accepted, rendered inline unbounded); backend silently ignores unknown request fields POST /templates long title → 201
LOW RegisterForm.tsx (toInt) Non-integer numeric input (e.g. 'abc', '1.5') silently dropped with no validation feedback Type 'abc' in 埋设章, submit → planted_at omitted silently
LOW toolbox empty ingest Empty ingest payload (world_entities:[]) returns 201 no-op instead of 4xx (client-guarded so UI-unreachable) POST .../glossary/ingest -d '{"world_entities":[]}' → 201

Several minor chain/style frontend timing edges (poll progress frozen 0→100, possible stale done-edge re-fire, no re-refine on identical segment text, ChainAdjudication 409 stuck panel, redundant poll.reset effect re-run) are noted in raw data as LOW/needs-browser and folded here to avoid noise.

3. Per-Area Status

Area Status Coverage note
projects-list+create issues-found (1 HIGH) API fully exercised via curl; wizard click-through needs-browser
outline-editor issues-found (2 MED) API + logic verified; read-only generate-only (no PUT by design); ⚑badge path dead vs real data
rules issues-found (1 HIGH, 3 MED) Full API curl + component read; a11y/SR needs-browser
foreshadow-board issues-found (1 HIGH infra, 1 MED) API fully exercised; board UI blocked by build 500
templates pass (3 LOW only) CRUD fully verified per contract; known blank-field HIGH confirmed fixed
settings-providers issues-found (1 MED) API + OAuth contracts verified; no plaintext leak; live OAuth completion not run
codex (设定库) issues-found (2 MED) API contract verified; in-browser blocked by build 500
skills issues-found (2 HIGH) Logic correct but registry empty + route 500; populated-state untested
review-display issues-found (1 HIGH) API + all normalizers verified; visual panels blocked by build 500
nav-shell+command issues-found (1 MED, 1 LOW a11y) All routes/commands map correctly; 28 unit tests pass; browser blocked
write-sse (LLM E2E) issues-found (1 CRITICAL) Real Kimi stream ran end-to-end; non-empty injection path untestable (no chars/entities)
toolbox-generators (LLM E2E) pass (2 LOW only) All 12 generators + 3 ingest flows ran real Kimi; 38 unit tests pass
style (LLM E2E) issues-found (1 HIGH) Full fingerprint+refine ran real Kimi; drift idx-alignment is contract analysis
chain (LLM E2E) issues-found (1 MED) API + awaiting/resume seam verified vs real jobs; 12 unit tests; browser blocked
browser smoke (all 14 routes) issues-found (2 CRITICAL infra) 4 top-level routes SSR 200; 10 project routes 500; zero hydration anywhere

4. Notable Gaps / Could Not Be Verified

  • All in-browser / visual / keyboard / screen-reader verification — blocked by the corrupt .next build (project routes 500, no hydration). Every a11y finding (combobox, toast severity, textarea label) is source-confirmed only; visual rendering of kanban, beat bars, annotated-text highlights, accept-gate disabled state, typewriter stream, and optimistic-update rendering is unverified.
  • Skills populated-state — registry is empty with no seed/write endpoint, so builtin/custom/community grouping + tier/genre/reads-writes badges were never rendered with real data (logic verified by code + unit tests only).
  • Style one-click 回炉 idx-alignment (HIGH) — could not be deterministically triggered: existing PID has no fingerprint, so the style auditor degrades to score=100/segments=[]. Finding is from code/contract analysis; needs a learned fingerprint + a write+review run to confirm live.
  • write-sse non-empty injection panel — the test PID has zero characters and zero world_entities, so selected[]/author_pin reason badge path is structurally untestable on this data.
  • Live 503 / LLM_UNAVAILABLE paths — providers are configured, so 503 branches (outline, chain, settings test, draft pre-stream) could not be forced; verified by code reading only.
  • Foreshadow ⚑ / 可回收 badge — all 30 chapters of the existing PID have empty foreshadow_windows, so this render path is exercised only by unit tests.
  • 409 CONFLICT_UNRESOLVED ingest path (toolbox) — could not trigger a real continuity conflict on throwaway data; adjudication logic verified by reading only.
  • Cleanup residue: No DELETE /projects endpoint exists (405), so ~8 throwaway QA projects created during mutation testing remain in the DB. One chain job (project 887af2d4) is still running. The existing rich PID (71b3725e…) data was never modified.