From 6d35c80ef026115f92844b09caf04e5314d8e4b4 Mon Sep 17 00:00:00 2001 From: jaeyunha Date: Tue, 12 May 2026 23:48:16 -0700 Subject: [PATCH] Prove project item lifecycle against the real fixture Make the browser smoke seed and open the dedicated Projects workspace fixture before exercising the side-panel lifecycle. Preserve route-visible archive/remove confirmations so the acceptance path is observable after navigation. Constraint: controller rejected the previous QA pass because the env-loaded system-Chrome side-panel gate timed out on the acceptance path. Rejected: relying on the organization projects list to discover a workspace | clean .env.test seeding can render Projects 0 unless the projects workspace fixture is explicitly enabled. Confidence: high Scope-risk: narrow Directive: keep projects item lifecycle E2E tied to PROJECTS_WORKSPACE_E2E and the returned projectsWorkspaceHref; do not replace it with a mocked or list-discovery pass. Tested: make doctor; ./hack/cargo_locked.sh test -p opengithub-api --test projects_workspace_contract -- --nocapture; make check; cd web && npx vitest run tests/project-workspace-page.test.tsx; cd web && set -a; . ../.env.test; set +a; PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome CARGO_TARGET_DIR="/home/jaeyunha/dev/namuh/opengithub-visible/projects-005-visible-retry-20260512233455/../.scratch/cargo-target" npx playwright test tests/e2e/projects-items-side-panel.spec.ts --project=chromium Not-tested: full make test wrapper; deterministic required subcommands passed. --- prd.json | 2 +- qa-report-summary.json | 50 +++++++++++-- qa-report.json | 73 +++++++++++++++++++ .../components/ProjectArchivedItemsPage.tsx | 15 +++- web/src/components/ProjectWorkspacePage.tsx | 30 +++++++- .../e2e/projects-items-side-panel.spec.ts | 62 +++++++--------- web/tests/project-workspace-page.test.tsx | 2 +- 7 files changed, 182 insertions(+), 52 deletions(-) diff --git a/prd.json b/prd.json index c6070582..b5de16ec 100644 --- a/prd.json +++ b/prd.json @@ -1890,7 +1890,7 @@ ], "needs_structure": true, "build_pass": true, - "qa_pass": false + "qa_pass": true }, { "id": "projects-006", diff --git a/qa-report-summary.json b/qa-report-summary.json index 02d6eb41..d034081c 100644 --- a/qa-report-summary.json +++ b/qa-report-summary.json @@ -8,29 +8,29 @@ ], "totals": { "features_total": 119, - "features_passed": 85, + "features_passed": 86, "features_failed": 1, "features_exhausted": 0, "features_crashed": 0 }, "sub_phase_totals": { "functional": { - "pass": 81, + "pass": 82, "fail": 0, "skip": 0 }, "api_contract": { - "pass": 77, + "pass": 78, "fail": 0, "skip": 4 }, "security": { - "pass": 78, + "pass": 79, "fail": 0, "skip": 3 }, "accessibility": { - "pass": 43, + "pass": 44, "fail": 0, "skip": 39 } @@ -2861,10 +2861,44 @@ "feature_id": "projects-005", "description": "Implement project items, draft issues, linked issue/PR sync, conversion of draft", "category": "crud", - "qa_pass": false, - "attempts": 0, + "qa_pass": true, + "attempts": 1, "exhausted": false, - "sub_phases": {} + "sub_phases": { + "functional": { + "status": "pass", + "notes": "Verified the Projects item side panel lifecycle against the real .env.test Postgres using system Chrome. The E2E now enables the projects workspace seed fixture, opens the seeded workspace href directly, opens the draft item detail panel, edits title/body, adds a project-only comment, converts the draft to an issue, archives it, views/restores it from the archive page, removes an item, and repeats a mobile side-panel smoke." + }, + "api_contract": { + "status": "pass", + "endpoints_tested": [ + "GET /api/projects/:project_id/workspace", + "GET /api/projects/:project_id/items/:item_id detail via item route", + "PATCH /api/projects/:project_id/items/:item_id/draft", + "POST /api/projects/:project_id/items/:item_id/comments", + "POST /api/projects/:project_id/items/:item_id/convert-to-issue", + "PATCH /api/projects/:project_id/items/:item_id/archive", + "GET /api/projects/:project_id/archived-items", + "PATCH /api/projects/:project_id/items/:item_id/restore", + "DELETE /api/projects/:project_id/items/:item_id" + ], + "notes": "DB-backed projects_workspace_contract passed 15/15 with no self-skips, including project_item_detail_and_archived_list_enforce_visibility, project_draft_editing_and_comments_are_project_only, project_draft_conversion_creates_linked_issue, and project_workspace_adds_reorders_and_removes_items." + }, + "security": { + "status": "pass", + "checks": [ + "private project item detail visibility remains covered by projects_workspace_contract", + "archive list visibility remains covered by projects_workspace_contract", + "real signed-in browser flow used seeded authenticated session instead of bypassing auth" + ], + "notes": "No endpoint or auth weakening was introduced; the browser gate exercises authenticated UI against the real DB, and the Rust contract confirms private guards for item detail and archive list." + }, + "accessibility": { + "status": "pass", + "violations": [], + "notes": "Focused browser smoke asserts the complementary Project item detail landmark, named forms/buttons/links, no dead href controls, and no page overflow in desktop and mobile viewport." + } + } }, { "feature_id": "projects-006", diff --git a/qa-report.json b/qa-report.json index 88146b4c..d4beabc5 100644 --- a/qa-report.json +++ b/qa-report.json @@ -6007,5 +6007,78 @@ "set -a; . ./.env.test; set +a; ./hack/cargo_locked.sh test -p opengithub-api --test projects_workspace_contract project_iteration_settings_create_breaks_and_filter_tokens -- --nocapture", "cd web && set -a; . ../.env.test; set +a; PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome npx playwright test --project=chromium tests/e2e/projects-field-settings.spec.ts" ] + }, + { + "feature_id": "projects-005", + "attempt": 1, + "status": "pass", + "sub_phases": { + "functional": { + "status": "pass", + "notes": "Verified the Projects item side panel lifecycle against the real .env.test Postgres using system Chrome. The E2E now enables the projects workspace seed fixture, opens the seeded workspace href directly, opens the draft item detail panel, edits title/body, adds a project-only comment, converts the draft to an issue, archives it, views/restores it from the archive page, removes an item, and repeats a mobile side-panel smoke." + }, + "api_contract": { + "status": "pass", + "endpoints_tested": [ + "GET /api/projects/:project_id/workspace", + "GET /api/projects/:project_id/items/:item_id detail via item route", + "PATCH /api/projects/:project_id/items/:item_id/draft", + "POST /api/projects/:project_id/items/:item_id/comments", + "POST /api/projects/:project_id/items/:item_id/convert-to-issue", + "PATCH /api/projects/:project_id/items/:item_id/archive", + "GET /api/projects/:project_id/archived-items", + "PATCH /api/projects/:project_id/items/:item_id/restore", + "DELETE /api/projects/:project_id/items/:item_id" + ], + "notes": "DB-backed projects_workspace_contract passed 15/15 with no self-skips, including project_item_detail_and_archived_list_enforce_visibility, project_draft_editing_and_comments_are_project_only, project_draft_conversion_creates_linked_issue, and project_workspace_adds_reorders_and_removes_items." + }, + "security": { + "status": "pass", + "checks": [ + "private project item detail visibility remains covered by projects_workspace_contract", + "archive list visibility remains covered by projects_workspace_contract", + "real signed-in browser flow used seeded authenticated session instead of bypassing auth" + ], + "notes": "No endpoint or auth weakening was introduced; the browser gate exercises authenticated UI against the real DB, and the Rust contract confirms private guards for item detail and archive list." + }, + "accessibility": { + "status": "pass", + "violations": [], + "notes": "Focused browser smoke asserts the complementary Project item detail landmark, named forms/buttons/links, no dead href controls, and no page overflow in desktop and mobile viewport." + } + }, + "tested_steps": [ + "export CARGO_TARGET_DIR=\"$PWD/.scratch/cargo-target\"; make doctor: passed; container runtime, opengithub-postgres-test on :55433, .env/.env.test, and Cargo target dir all healthy.", + "Reproduced origin/main failure: env-loaded system-Chrome Playwright spec failed because /orgs/namuh/projects listed Projects 0 and no /projects/*/views/* workspace link without PROJECTS_WORKSPACE_E2E seed.", + "export CARGO_TARGET_DIR=\"$PWD/.scratch/cargo-target\"; set -a; . ./.env.test; set +a; ./hack/cargo_locked.sh test -p opengithub-api --test projects_workspace_contract -- --nocapture: passed; 15 passed, 0 failed, no self-skips.", + "export CARGO_TARGET_DIR=\"$PWD/.scratch/cargo-target\"; make check: passed; Cargo check, Web typecheck, Clippy, and Biome passed.", + "cd web && npx vitest run tests/project-workspace-page.test.tsx: passed; 1 file / 19 tests passed.", + "cd web && set -a; . ../.env.test; set +a; PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome CARGO_TARGET_DIR=\"$PWD/../.scratch/cargo-target\" npx playwright test tests/e2e/projects-items-side-panel.spec.ts --project=chromium: passed; 2 passed including auth setup and final item lifecycle smoke." + ], + "bugs_found": [ + "The side-panel E2E did not enable the projects workspace DB fixture, so a clean .env.test run could land on /orgs/namuh/projects with Projects 0 and never reach the item side panel.", + "The test relied on broad project-list link discovery instead of the seeded workspace href returned by the seeder, making the acceptance path nondeterministic.", + "After item removal from the side panel, the success message could be lost during navigation back to the workspace; the product now carries a notice query and renders the confirmation on return.", + "Archive Back to project used the API-provided selectedView.href, which was not guaranteed to be the routed org/user workspace URL expected by the browser path." + ], + "fixes_applied": [ + "Enabled PROJECTS_WORKSPACE_E2E in the projects item side-panel spec seeder and opened seeded.projectsWorkspaceHref directly.", + "Targeted the seeded draft item by accessible link name and scoped Project-only draft assertions to the side-panel metadata paragraph.", + "Preserved side-panel removal confirmation across the route change with a notice query parameter.", + "Computed archive Back to project href from scope/owner/project number/view number for stable routed navigation." + ], + "artifacts": [ + "web/test-results/projects-items-side-panel--65877--final-item-lifecycle-smoke-chromium/error-context.md", + "web/test-results/projects-items-side-panel--65877--final-item-lifecycle-smoke-chromium/trace.zip", + "ralph/screenshots/build/projects-005-final-item-panel.jpg", + "ralph/screenshots/build/projects-005-final-draft-editor.jpg", + "ralph/screenshots/build/projects-005-final-convert-dialog.jpg", + "ralph/screenshots/build/projects-005-final-linked-issue-sync.jpg", + "ralph/screenshots/build/projects-005-final-archive-confirmation.jpg", + "ralph/screenshots/build/projects-005-final-archived-list.jpg", + "ralph/screenshots/build/projects-005-final-restore-confirmation.jpg", + "ralph/screenshots/build/projects-005-final-mobile.jpg" + ], + "remaining_risks": [] } ] diff --git a/web/src/components/ProjectArchivedItemsPage.tsx b/web/src/components/ProjectArchivedItemsPage.tsx index 0c1c42e6..8f99556f 100644 --- a/web/src/components/ProjectArchivedItemsPage.tsx +++ b/web/src/components/ProjectArchivedItemsPage.tsx @@ -3,7 +3,11 @@ import Link from "next/link"; import { useState } from "react"; import type { ProjectArchivedItem, ProjectWorkspace } from "@/lib/api"; -import { projectItemHref } from "@/lib/navigation"; +import { + organizationProjectWorkspaceHref, + projectItemHref, + userProjectWorkspaceHref, +} from "@/lib/navigation"; type ProjectArchivedItemsPageProps = { workspace: ProjectWorkspace; @@ -47,7 +51,14 @@ export function ProjectArchivedItemsPage({ setMessage("Item restored"); } - const workspaceHref = workspace.selectedView.href; + const workspaceHref = + scope === "organization" + ? organizationProjectWorkspaceHref( + owner, + workspace.project.number, + viewNumber, + ) + : userProjectWorkspaceHref(owner, workspace.project.number, viewNumber); return (
diff --git a/web/src/components/ProjectWorkspacePage.tsx b/web/src/components/ProjectWorkspacePage.tsx index e13b2a6e..98c7d0a5 100644 --- a/web/src/components/ProjectWorkspacePage.tsx +++ b/web/src/components/ProjectWorkspacePage.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { type FormEvent, Fragment, useMemo, useState } from "react"; +import { type FormEvent, Fragment, useEffect, useMemo, useState } from "react"; import type { ProjectConversionTargets, ProjectItemComment, @@ -231,6 +231,14 @@ function requestValueForField(field: ProjectWorkspaceField, raw: string) { return raw; } +function hrefWithNotice(href: string, notice: string) { + const [path, hash = ""] = href.split("#", 2); + const separator = path.includes("?") ? "&" : "?"; + return `${path}${separator}notice=${encodeURIComponent(notice)}${ + hash ? `#${hash}` : "" + }`; +} + export function ProjectWorkspacePage({ workspace, scope, @@ -282,6 +290,7 @@ export function ProjectWorkspacePage({ const [itemSaving, setItemSaving] = useState(false); const [itemMessage, setItemMessage] = useState(null); const [itemError, setItemError] = useState(null); + const [noticeMessage, setNoticeMessage] = useState(null); const [emptyColumnsVisible, setEmptyColumnsVisible] = useState( workspace.boardConfig?.emptyColumnsVisible ?? true, ); @@ -341,6 +350,15 @@ export function ProjectWorkspacePage({ view: viewNumber, }; + useEffect(() => { + setNoticeMessage( + new URLSearchParams(window.location.search).get("notice") === + "item_removed" + ? "Item removed from project." + : null, + ); + }, []); + function submitFilter(event: FormEvent) { event.preventDefault(); const href = workspaceHref( @@ -713,8 +731,8 @@ export function ProjectWorkspacePage({ ); return; } - setItemMessage("Item removed"); - window.location.assign(currentHref); + setItemMessage("Item removed from project."); + window.location.assign(hrefWithNotice(currentHref, "item_removed")); } return ( @@ -1659,6 +1677,9 @@ export function ProjectWorkspacePage({ {itemMessage ? ( {itemMessage} ) : null} + {noticeMessage ? ( + {noticeMessage} + ) : null}