From 971f1752aa43585f372b1fb300d44bab464b4483 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 09:52:56 +0900 Subject: [PATCH 01/11] fix(ci): validate stacked pull requests before domain stacks --- .github/workflows/app-ci.yml | 36 +++++++- .github/workflows/bandit.yml | 1 - .github/workflows/dependency-review.yml | 4 - .github/workflows/docker-publish.yml | 4 - AGENTS.md | 7 ++ backend/tests/test_release_governance.py | 8 +- .../test_stacked_pr_workflow_triggers.py | 51 ++++++++++++ tests/test_postgres_ci_contract.py | 83 +++++++++++++++++++ 8 files changed, 175 insertions(+), 19 deletions(-) create mode 100644 backend/tests/test_stacked_pr_workflow_triggers.py create mode 100644 tests/test_postgres_ci_contract.py diff --git a/.github/workflows/app-ci.yml b/.github/workflows/app-ci.yml index e8f445748..e13bba02f 100644 --- a/.github/workflows/app-ci.yml +++ b/.github/workflows/app-ci.yml @@ -2,10 +2,6 @@ name: Application CI on: pull_request: - branches: - - develop - - master - - "release/**" push: branches: - develop @@ -25,9 +21,24 @@ jobs: strategy: matrix: python-version: ["3.14"] + services: + postgres: + image: pgvector/pgvector:pg16@sha256:ccc6e83d6e35e931dc7c5def2022729d5a6c370318d099181995567ff1fb4d6b + env: + POSTGRES_USER: test + POSTGRES_PASSWORD: test + POSTGRES_DB: test_db + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U test -d test_db" + --health-interval 5s + --health-timeout 5s + --health-retries 12 env: PYTHONWARNINGS: error DISABLE_BACKGROUND_WORKERS: "1" + DATABASE_URL: postgresql+asyncpg://test:test@localhost:5432/test_db steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 @@ -66,6 +77,23 @@ jobs: cd backend python -m ruff check . + - name: Generate ephemeral CI runtime secret + run: | + python - <<'PY' + import os + import secrets + + value = "Ci9!" + secrets.token_urlsafe(48) + print(f"::add-mask::{value}") + with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as env_file: + env_file.write(f"AUTH_SESSION_HMAC_SECRET={value}\n") + PY + + - name: Run database migrations + run: | + cd backend + python scripts/migrate_db.py + - name: Run backend tests run: | set -o pipefail diff --git a/.github/workflows/bandit.yml b/.github/workflows/bandit.yml index c5c613c08..0e250389c 100644 --- a/.github/workflows/bandit.yml +++ b/.github/workflows/bandit.yml @@ -4,7 +4,6 @@ on: push: branches: [ develop, master ] pull_request: - branches: [ develop, master ] workflow_dispatch: permissions: diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index c303d1e61..21e607f7b 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -2,10 +2,6 @@ name: Dependency Review on: pull_request: - branches: - - develop - - master - - "release/**" workflow_dispatch: permissions: diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index fc7058413..dd1015812 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -5,10 +5,6 @@ on: tags: - "v*" pull_request: - branches: - - develop - - master - - "release/**" permissions: contents: read diff --git a/AGENTS.md b/AGENTS.md index 9104dd1f4..c8fbb38e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -189,6 +189,13 @@ in this repo. ## PR automation and review defaults +- Stacked-PR trigger tests must parse the YAML event configuration, not search + source text for `**`: a comment can satisfy that search, and a later + `!feature/**` pattern can exclude the very stack being validated. Preserve the + Actions `on` key when choosing a YAML loader, inspect ordered branch patterns, + and retain rejection tests for both cases. The installed hash-locked PyYAML + dependency is sufficient; do not add a second parser for this contract. + - Follow `docs/development/merge-gate-policy.md` for PR gate interpretation. - PR Governance must stay metadata-only: no PR-head checkout, no admin merge, no review dismissal, and no security-check suppression. diff --git a/backend/tests/test_release_governance.py b/backend/tests/test_release_governance.py index a23c70746..2e197b578 100644 --- a/backend/tests/test_release_governance.py +++ b/backend/tests/test_release_governance.py @@ -651,7 +651,6 @@ def test_app_ci_runs_backend_and_frontend_checks_without_duplicate_release_pushe workflow = read_repo_text(".github/workflows/app-ci.yml") assert "pull_request:" in workflow - assert "release/**" in workflow assert "python -m pytest" in workflow assert "PYTHONWARNINGS: error" in workflow assert 'DISABLE_BACKGROUND_WORKERS: "1"' in workflow @@ -666,6 +665,7 @@ def test_app_ci_runs_backend_and_frontend_checks_without_duplicate_release_pushe assert "uses: actions/setup-node@v" not in workflow push_block = workflow.split("push:", 1)[1].split("pull_request:", 1)[0] + assert "develop" in push_block assert "master" in push_block assert "release/**" not in push_block @@ -705,12 +705,8 @@ def test_docker_publish_validates_pr_images_and_publishes_semver_images_only_on_ == 2 ) push_block = workflow.split("push:", 1)[1].split("pull_request:", 1)[0] - pull_request_block = workflow.split("pull_request:", 1)[1].split("permissions:", 1)[ - 0 - ] assert "tags:" in push_block assert "branches:" not in push_block - assert "develop" in pull_request_block assert "ai_email_client-backend" in workflow assert "ai_email_client-frontend" in workflow assert workflow.count("image: naruon") == 2 @@ -1202,4 +1198,4 @@ def test_agents_records_ghcr_visibility_publication_runbook() -> None: assert "Package settings" in agents assert "Danger Zone" in agents assert "Change visibility" in normalized_agents - assert "anonymous pull/token access" in agents + assert "anonymous pull/token access" in agents \ No newline at end of file diff --git a/backend/tests/test_stacked_pr_workflow_triggers.py b/backend/tests/test_stacked_pr_workflow_triggers.py new file mode 100644 index 000000000..0a8425b51 --- /dev/null +++ b/backend/tests/test_stacked_pr_workflow_triggers.py @@ -0,0 +1,51 @@ +"""Guard repo-local PR validation on dependent stacked pull requests.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[2] +PR_VALIDATION_WORKFLOWS = ( + ".github/workflows/app-ci.yml", + ".github/workflows/bandit.yml", + ".github/workflows/dependency-review.yml", + ".github/workflows/docker-publish.yml", +) + + +@pytest.mark.parametrize("workflow_path", PR_VALIDATION_WORKFLOWS) +def test_repo_local_pr_validation_accepts_every_base_branch(workflow_path: str) -> None: + """Require an unfiltered pull_request trigger for every stacked PR base.""" + workflow_text = (REPO_ROOT / workflow_path).read_text(encoding="utf-8") + # BaseLoader preserves the Actions `on` key instead of YAML 1.1 boolean coercion. + workflow_events = yaml.load(workflow_text, Loader=yaml.BaseLoader)["on"] + assert "pull_request" in workflow_events + pull_request_config = workflow_events["pull_request"] or {} + assert "branches" not in pull_request_config + assert "branches-ignore" not in pull_request_config + + +@pytest.mark.parametrize( + "branch_filter", + [ + "branches: ['**']", + "branches: ['**', '!feature/**']", + "branches: [develop] # '**' is only a comment", + "branches-ignore: [archive/**]", + ], +) +def test_stacked_trigger_guard_rejects_any_base_filter( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, branch_filter: str +) -> None: + """Do not encode all-base verification through mutable branch patterns.""" + workflow_path = tmp_path / "workflow.yml" + workflow_path.write_text( + f"on:\n pull_request:\n {branch_filter}\n", encoding="utf-8" + ) + monkeypatch.setitem(globals(), "REPO_ROOT", tmp_path) + with pytest.raises(AssertionError): + test_repo_local_pr_validation_accepts_every_base_branch("workflow.yml") diff --git a/tests/test_postgres_ci_contract.py b/tests/test_postgres_ci_contract.py new file mode 100644 index 000000000..e6c86e4a6 --- /dev/null +++ b/tests/test_postgres_ci_contract.py @@ -0,0 +1,83 @@ +"""Repository contract for PostgreSQL-backed backend acceptance.""" + +from pathlib import Path + +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[1] +APP_CI = REPO_ROOT / ".github" / "workflows" / "app-ci.yml" +PGVECTOR_CI_IMAGE = ( + "pgvector/pgvector:pg16@" + "sha256:ccc6e83d6e35e931dc7c5def2022729d5a6c370318d099181995567ff1fb4d6b" +) + + +def _workflow() -> dict[str, object]: + """Load the workflow without YAML 1.1 coercing the `on` key to a boolean.""" + return yaml.load(APP_CI.read_text(encoding="utf-8"), Loader=yaml.BaseLoader) + + +def test_backend_ci_provisions_migrated_pgvector_database() -> None: + """Real PostgreSQL tests must run against a ready, migrated CI database.""" + workflow = _workflow() + jobs = workflow["jobs"] + assert isinstance(jobs, dict) + backend = jobs["backend"] + assert isinstance(backend, dict) + + services = backend.get("services") + assert isinstance(services, dict), "backend CI must provision PostgreSQL" + postgres = services.get("postgres") + assert isinstance(postgres, dict), "backend CI must declare a postgres service" + assert postgres.get("image") == PGVECTOR_CI_IMAGE + assert postgres.get("env") == { + "POSTGRES_USER": "test", + "POSTGRES_PASSWORD": "test", + "POSTGRES_DB": "test_db", + } + options = postgres.get("options") + assert isinstance(options, str) + assert "pg_isready -U test -d test_db" in options + + environment = backend.get("env") + assert isinstance(environment, dict) + assert environment.get("DATABASE_URL") == ( + "postgresql+asyncpg://test:test@localhost:5432/test_db" + ) + assert "AUTH_SESSION_HMAC_SECRET" not in environment, ( + "CI runtime auth material must be generated per job, not committed as a fixture" + ) + + steps = backend.get("steps") + assert isinstance(steps, list) + named_steps = { + step.get("name"): step + for step in steps + if isinstance(step, dict) and isinstance(step.get("name"), str) + } + runtime_secret = named_steps.get("Generate ephemeral CI runtime secret") + assert isinstance(runtime_secret, dict), ( + "backend CI must generate auth material before importing runtime settings" + ) + runtime_secret_script = str(runtime_secret.get("run", "")) + assert "secrets.token_urlsafe(48)" in runtime_secret_script + assert "AUTH_SESSION_HMAC_SECRET" in runtime_secret_script + assert "GITHUB_ENV" in runtime_secret_script + assert 'print(f"::add-mask::{value}")' in runtime_secret_script, ( + "generated runtime auth material must be masked before later steps expose env" + ) + assert runtime_secret_script.index("::add-mask::") < runtime_secret_script.index( + "GITHUB_ENV" + ) + + migration = named_steps.get("Run database migrations") + assert isinstance(migration, dict), "backend CI must migrate before pytest" + assert "python scripts/migrate_db.py" in str(migration.get("run", "")) + + step_names = [ + step.get("name") for step in steps if isinstance(step, dict) and step.get("name") + ] + assert step_names.index("Generate ephemeral CI runtime secret") < step_names.index( + "Run database migrations" + ) < step_names.index("Run backend tests") From 7eac164f5da837f3c047a31f8a0f1e1fb48fda0a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 10:40:55 +0900 Subject: [PATCH 02/11] test(ci): collect PostgreSQL contract in backend suite --- backend/tests/test_postgres_ci_contract.py | 83 ++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 backend/tests/test_postgres_ci_contract.py diff --git a/backend/tests/test_postgres_ci_contract.py b/backend/tests/test_postgres_ci_contract.py new file mode 100644 index 000000000..a86bfb2c2 --- /dev/null +++ b/backend/tests/test_postgres_ci_contract.py @@ -0,0 +1,83 @@ +"""Repository contract for PostgreSQL-backed backend acceptance.""" + +from pathlib import Path + +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[2] +APP_CI = REPO_ROOT / ".github" / "workflows" / "app-ci.yml" +PGVECTOR_CI_IMAGE = ( + "pgvector/pgvector:pg16@" + "sha256:ccc6e83d6e35e931dc7c5def2022729d5a6c370318d099181995567ff1fb4d6b" +) + + +def _workflow() -> dict[str, object]: + """Load the workflow without YAML 1.1 coercing the `on` key to a boolean.""" + return yaml.load(APP_CI.read_text(encoding="utf-8"), Loader=yaml.BaseLoader) + + +def test_backend_ci_provisions_migrated_pgvector_database() -> None: + """Real PostgreSQL tests must run against a ready, migrated CI database.""" + workflow = _workflow() + jobs = workflow["jobs"] + assert isinstance(jobs, dict) + backend = jobs["backend"] + assert isinstance(backend, dict) + + services = backend.get("services") + assert isinstance(services, dict), "backend CI must provision PostgreSQL" + postgres = services.get("postgres") + assert isinstance(postgres, dict), "backend CI must declare a postgres service" + assert postgres.get("image") == PGVECTOR_CI_IMAGE + assert postgres.get("env") == { + "POSTGRES_USER": "test", + "POSTGRES_PASSWORD": "test", + "POSTGRES_DB": "test_db", + } + options = postgres.get("options") + assert isinstance(options, str) + assert "pg_isready -U test -d test_db" in options + + environment = backend.get("env") + assert isinstance(environment, dict) + assert environment.get("DATABASE_URL") == ( + "postgresql+asyncpg://test:test@localhost:5432/test_db" + ) + assert "AUTH_SESSION_HMAC_SECRET" not in environment, ( + "CI runtime auth material must be generated per job, not committed as a fixture" + ) + + steps = backend.get("steps") + assert isinstance(steps, list) + named_steps = { + step.get("name"): step + for step in steps + if isinstance(step, dict) and isinstance(step.get("name"), str) + } + runtime_secret = named_steps.get("Generate ephemeral CI runtime secret") + assert isinstance(runtime_secret, dict), ( + "backend CI must generate auth material before importing runtime settings" + ) + runtime_secret_script = str(runtime_secret.get("run", "")) + assert "secrets.token_urlsafe(48)" in runtime_secret_script + assert "AUTH_SESSION_HMAC_SECRET" in runtime_secret_script + assert "GITHUB_ENV" in runtime_secret_script + assert 'print(f"::add-mask::{value}")' in runtime_secret_script, ( + "generated runtime auth material must be masked before later steps expose env" + ) + assert runtime_secret_script.index("::add-mask::") < runtime_secret_script.index( + "GITHUB_ENV" + ) + + migration = named_steps.get("Run database migrations") + assert isinstance(migration, dict), "backend CI must migrate before pytest" + assert "python scripts/migrate_db.py" in str(migration.get("run", "")) + + step_names = [ + step.get("name") for step in steps if isinstance(step, dict) and step.get("name") + ] + assert step_names.index("Generate ephemeral CI runtime secret") < step_names.index( + "Run database migrations" + ) < step_names.index("Run backend tests") From f985a00030028c9989637b3fafffac07d95e2de2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 10:41:06 +0900 Subject: [PATCH 03/11] test(ci): remove uncollected root PostgreSQL contract --- tests/test_postgres_ci_contract.py | 83 ------------------------------ 1 file changed, 83 deletions(-) delete mode 100644 tests/test_postgres_ci_contract.py diff --git a/tests/test_postgres_ci_contract.py b/tests/test_postgres_ci_contract.py deleted file mode 100644 index e6c86e4a6..000000000 --- a/tests/test_postgres_ci_contract.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Repository contract for PostgreSQL-backed backend acceptance.""" - -from pathlib import Path - -import yaml - - -REPO_ROOT = Path(__file__).resolve().parents[1] -APP_CI = REPO_ROOT / ".github" / "workflows" / "app-ci.yml" -PGVECTOR_CI_IMAGE = ( - "pgvector/pgvector:pg16@" - "sha256:ccc6e83d6e35e931dc7c5def2022729d5a6c370318d099181995567ff1fb4d6b" -) - - -def _workflow() -> dict[str, object]: - """Load the workflow without YAML 1.1 coercing the `on` key to a boolean.""" - return yaml.load(APP_CI.read_text(encoding="utf-8"), Loader=yaml.BaseLoader) - - -def test_backend_ci_provisions_migrated_pgvector_database() -> None: - """Real PostgreSQL tests must run against a ready, migrated CI database.""" - workflow = _workflow() - jobs = workflow["jobs"] - assert isinstance(jobs, dict) - backend = jobs["backend"] - assert isinstance(backend, dict) - - services = backend.get("services") - assert isinstance(services, dict), "backend CI must provision PostgreSQL" - postgres = services.get("postgres") - assert isinstance(postgres, dict), "backend CI must declare a postgres service" - assert postgres.get("image") == PGVECTOR_CI_IMAGE - assert postgres.get("env") == { - "POSTGRES_USER": "test", - "POSTGRES_PASSWORD": "test", - "POSTGRES_DB": "test_db", - } - options = postgres.get("options") - assert isinstance(options, str) - assert "pg_isready -U test -d test_db" in options - - environment = backend.get("env") - assert isinstance(environment, dict) - assert environment.get("DATABASE_URL") == ( - "postgresql+asyncpg://test:test@localhost:5432/test_db" - ) - assert "AUTH_SESSION_HMAC_SECRET" not in environment, ( - "CI runtime auth material must be generated per job, not committed as a fixture" - ) - - steps = backend.get("steps") - assert isinstance(steps, list) - named_steps = { - step.get("name"): step - for step in steps - if isinstance(step, dict) and isinstance(step.get("name"), str) - } - runtime_secret = named_steps.get("Generate ephemeral CI runtime secret") - assert isinstance(runtime_secret, dict), ( - "backend CI must generate auth material before importing runtime settings" - ) - runtime_secret_script = str(runtime_secret.get("run", "")) - assert "secrets.token_urlsafe(48)" in runtime_secret_script - assert "AUTH_SESSION_HMAC_SECRET" in runtime_secret_script - assert "GITHUB_ENV" in runtime_secret_script - assert 'print(f"::add-mask::{value}")' in runtime_secret_script, ( - "generated runtime auth material must be masked before later steps expose env" - ) - assert runtime_secret_script.index("::add-mask::") < runtime_secret_script.index( - "GITHUB_ENV" - ) - - migration = named_steps.get("Run database migrations") - assert isinstance(migration, dict), "backend CI must migrate before pytest" - assert "python scripts/migrate_db.py" in str(migration.get("run", "")) - - step_names = [ - step.get("name") for step in steps if isinstance(step, dict) and step.get("name") - ] - assert step_names.index("Generate ephemeral CI runtime secret") < step_names.index( - "Run database migrations" - ) < step_names.index("Run backend tests") From eb8fe7d24e59f8aa8e69fd2854cc269e935af4ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 23 Sep 2026 16:44:31 +0900 Subject: [PATCH 04/11] test(ci): require hosted Playwright evidence --- .../test_stacked_pr_workflow_triggers.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/backend/tests/test_stacked_pr_workflow_triggers.py b/backend/tests/test_stacked_pr_workflow_triggers.py index 0a8425b51..0dd5b86f0 100644 --- a/backend/tests/test_stacked_pr_workflow_triggers.py +++ b/backend/tests/test_stacked_pr_workflow_triggers.py @@ -15,6 +15,9 @@ ".github/workflows/dependency-review.yml", ".github/workflows/docker-publish.yml", ) +PLAYWRIGHT_UPLOAD_PIN = ( + "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" +) @pytest.mark.parametrize("workflow_path", PR_VALIDATION_WORKFLOWS) @@ -49,3 +52,27 @@ def test_stacked_trigger_guard_rejects_any_base_filter( monkeypatch.setitem(globals(), "REPO_ROOT", tmp_path) with pytest.raises(AssertionError): test_repo_local_pr_validation_accepts_every_base_branch("workflow.yml") + + +def test_application_ci_executes_browser_acceptance_and_preserves_artifacts() -> None: + """Require exact-head Playwright execution instead of treating smoke as E2E proof.""" + workflow_path = REPO_ROOT / ".github/workflows/app-ci.yml" + workflow = yaml.load(workflow_path.read_text(encoding="utf-8"), Loader=yaml.BaseLoader) + frontend_steps = workflow["jobs"]["frontend"]["steps"] + steps_by_name = {step.get("name"): step for step in frontend_steps} + + browser_step = steps_by_name["Run Playwright browser acceptance"] + browser_command = browser_step["run"] + assert "pnpm run test:e2e" in browser_command + browser_env = browser_step.get("env") or {} + assert "LIVE_BASE_URL" not in browser_env + assert "RUN_LIVE_E2E" not in browser_env + + evidence_step = steps_by_name["Upload Playwright browser evidence"] + assert evidence_step["uses"] == PLAYWRIGHT_UPLOAD_PIN + assert evidence_step["if"] == "${{ always() }}" + evidence_name = evidence_step["with"]["name"] + assert "${{ github.sha }}" in evidence_name + evidence_paths = evidence_step["with"]["path"] + assert "frontend/playwright-report/" in evidence_paths + assert "frontend/test-results/" in evidence_paths From 7970f7e56bf96140a28b69e762b8d33509036408 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 23 Sep 2026 16:45:17 +0900 Subject: [PATCH 05/11] fix(ci): execute exact-head browser acceptance --- .github/workflows/app-ci.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/app-ci.yml b/.github/workflows/app-ci.yml index e13bba02f..0679aa0d4 100644 --- a/.github/workflows/app-ci.yml +++ b/.github/workflows/app-ci.yml @@ -165,3 +165,19 @@ jobs: NARUON_FULL_PRODUCT_BASE_URL: "http://127.0.0.1:3001" NARUON_FULL_PRODUCT_SCREENSHOT_DIR: "/tmp/naruon-full-product-smoke" run: cd frontend && pnpm run full:smoke + + - name: Run Playwright browser acceptance + env: + PLAYWRIGHT_HTML_OPEN: "never" + run: cd frontend && pnpm run test:e2e -- --reporter=line,html + + - name: Upload Playwright browser evidence + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4.6.2 + with: + name: playwright-browser-evidence-${{ github.sha }}-${{ github.run_attempt }} + path: | + frontend/playwright-report/ + frontend/test-results/ + if-no-files-found: warn + retention-days: 14 From 9b27f0ca29d3e64860114047e1e148856afe628e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 23 Sep 2026 19:48:10 +0900 Subject: [PATCH 06/11] test(ci): require exact PR-head browser evidence --- .../tests/test_stacked_pr_workflow_triggers.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/tests/test_stacked_pr_workflow_triggers.py b/backend/tests/test_stacked_pr_workflow_triggers.py index 0dd5b86f0..a568716ea 100644 --- a/backend/tests/test_stacked_pr_workflow_triggers.py +++ b/backend/tests/test_stacked_pr_workflow_triggers.py @@ -18,6 +18,7 @@ PLAYWRIGHT_UPLOAD_PIN = ( "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" ) +EXACT_HEAD_REF = "${{ github.event.pull_request.head.sha || github.sha }}" @pytest.mark.parametrize("workflow_path", PR_VALIDATION_WORKFLOWS) @@ -54,10 +55,18 @@ def test_stacked_trigger_guard_rejects_any_base_filter( test_repo_local_pr_validation_accepts_every_base_branch("workflow.yml") -def test_application_ci_executes_browser_acceptance_and_preserves_artifacts() -> None: - """Require exact-head Playwright execution instead of treating smoke as E2E proof.""" +def test_application_ci_executes_and_labels_exact_head_browser_acceptance() -> None: + """Require Playwright evidence to execute and identify the exact PR head.""" workflow_path = REPO_ROOT / ".github/workflows/app-ci.yml" workflow = yaml.load(workflow_path.read_text(encoding="utf-8"), Loader=yaml.BaseLoader) + + for job_name in ("backend", "frontend"): + job_steps = workflow["jobs"][job_name]["steps"] + steps_by_name = {step.get("name"): step for step in job_steps} + checkout_step = steps_by_name["Checkout repository"] + checkout_options = checkout_step.get("with") or {} + assert checkout_options.get("ref") == EXACT_HEAD_REF + frontend_steps = workflow["jobs"]["frontend"]["steps"] steps_by_name = {step.get("name"): step for step in frontend_steps} @@ -72,7 +81,7 @@ def test_application_ci_executes_browser_acceptance_and_preserves_artifacts() -> assert evidence_step["uses"] == PLAYWRIGHT_UPLOAD_PIN assert evidence_step["if"] == "${{ always() }}" evidence_name = evidence_step["with"]["name"] - assert "${{ github.sha }}" in evidence_name + assert EXACT_HEAD_REF in evidence_name evidence_paths = evidence_step["with"]["path"] assert "frontend/playwright-report/" in evidence_paths assert "frontend/test-results/" in evidence_paths From 4bf3efd95b8c5002c281a4b47f93e78fc63730de Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 23 Sep 2026 19:48:38 +0900 Subject: [PATCH 07/11] fix(ci): execute and label exact PR head --- .github/workflows/app-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/app-ci.yml b/.github/workflows/app-ci.yml index 0679aa0d4..36679dbd6 100644 --- a/.github/workflows/app-ci.yml +++ b/.github/workflows/app-ci.yml @@ -49,6 +49,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 @@ -117,6 +118,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Install pnpm run: corepack enable pnpm @@ -175,7 +177,7 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4.6.2 with: - name: playwright-browser-evidence-${{ github.sha }}-${{ github.run_attempt }} + name: playwright-browser-evidence-${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_attempt }} path: | frontend/playwright-report/ frontend/test-results/ From 966a4e1e0c1ee93b677ac36333e8e7c7b7070336 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 24 Sep 2026 13:44:05 +0900 Subject: [PATCH 08/11] ci: stage one-shot Data evidence fixture repair --- .../one-shot-repair-data-evidence-fixture.yml | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 .github/workflows/one-shot-repair-data-evidence-fixture.yml diff --git a/.github/workflows/one-shot-repair-data-evidence-fixture.yml b/.github/workflows/one-shot-repair-data-evidence-fixture.yml new file mode 100644 index 000000000..05c6a7e72 --- /dev/null +++ b/.github/workflows/one-shot-repair-data-evidence-fixture.yml @@ -0,0 +1,162 @@ +name: One-shot repair Data evidence fixture + +on: + push: + branches: + - fix/stacked-pr-trigger-foundation + +permissions: + contents: write + +jobs: + repair: + name: Repair shared Playwright Data evidence fixture + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout exact staging head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: true + ref: ${{ github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + cache: pnpm + cache-dependency-path: frontend/pnpm-lock.yaml + + - name: Install pnpm + run: corepack enable pnpm + + - name: Apply bounded fixture repair and regression + shell: bash + run: | + set -euo pipefail + python - <<'PY' + from pathlib import Path + + helper_path = Path("frontend/tests/e2e/helpers.ts") + test_path = Path("frontend/src/lib/data-evidence-snapshot-e2e-fixture.test.ts") + helper = helper_path.read_text(encoding="utf-8") + + if "const dataEvidenceSnapshot =" in helper: + raise SystemExit("dataEvidenceSnapshot fixture already exists; refusing ambiguous rewrite") + if "/api/data/quality-surface/evidence-snapshot" in helper: + raise SystemExit("evidence-snapshot route already exists; refusing ambiguous rewrite") + if test_path.exists(): + raise SystemExit(f"{test_path} already exists; refusing overwrite") + + fixture_anchor = "\nconst accountConfig = {\n" + if helper.count(fixture_anchor) != 1: + raise SystemExit("accountConfig insertion anchor is not unique") + + fixture = r''' + const dataEvidenceSnapshot = { + snapshot_version: 'data_quality_evidence_snapshot.v1', + generated_at: '2026-05-28T05:47:00Z', + audit_event: 'data.quality_surface.evidence_snapshot.viewed', + scope_label: 'playwright_dashboard_fixture', + snapshot_digest: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + digest_algorithm: 'sha256', + canonical_payload_fields: [], + privacy_redaction_policy: { + raw_content_exposed: false, + stable_identifiers_exposed: false, + provider_credentials_exposed: false, + redacted_fields: [], + allowed_sample_fields: [], + }, + validation_status: { + status_code: 'ready', + display_name: 'Ready', + detail_text: 'Playwright evidence snapshot is redacted and verifier-ready.', + provider_write_executed: false, + }, + verification_handoff: { + handoff_text: 'Verify the copied Playwright snapshot JSON before sharing diligence materials.', + verifier_command: 'python scripts/verify_evidence_snapshot.py snapshot.json', + accepted_input: 'data_quality_evidence_snapshot.v1 JSON', + digest_algorithm: 'sha256', + excluded_digest_fields: ['snapshot_digest'], + success_exit_code: 0, + failure_exit_codes: { invalid_digest: 2 }, + provider_write_executed: false, + }, + parser_manifest_summary: [], + content_graph_evidence_samples: [], + knowledge_graph_evidence_samples: [], + evidence_packet_checklist: [], + data_room_package_manifest: [], + diligence_exception_register: [], + diligence_risk_matrix: [], + diligence_close_artifact_review_queue: [], + diligence_close_owner_handoff_queue: [], + diligence_close_traceability_map: [], + diligence_close_decision_summary: null, + diligence_close_proof_plan: [], + }; + ''' + helper = helper.replace(fixture_anchor, "\n" + fixture.strip() + "\n" + fixture_anchor, 1) + + route_anchor = """ if (path === '/api/data/quality-surface' && request.method() === 'GET') {\n await fulfillJson(route, dataQualitySurface);\n return;\n }\n\n""" + if helper.count(route_anchor) != 1: + raise SystemExit("data quality route insertion anchor is not unique") + + evidence_route = """ if (path === '/api/data/quality-surface/evidence-snapshot' && request.method() === 'GET') {\n await fulfillJson(route, dataEvidenceSnapshot);\n return;\n }\n\n""" + helper = helper.replace(route_anchor, route_anchor + evidence_route, 1) + helper_path.write_text(helper, encoding="utf-8") + + test_path.write_text( + """import type { Page, Route } from '@playwright/test';\nimport { describe, expect, it, vi } from 'vitest';\n\nimport { mockDashboardApi } from '../../tests/e2e/helpers';\n\ntype RouteHandler = (route: Route) => Promise;\n\ndescribe('mockDashboardApi Data evidence snapshot', () => {\n it('serves a redacted verifier-ready snapshot instead of falling through to 404', async () => {\n let routeHandler: RouteHandler | undefined;\n const page = {\n route: vi.fn(async (_pattern: string, handler: RouteHandler) => {\n routeHandler = handler;\n }),\n } as unknown as Page;\n\n await mockDashboardApi(page);\n expect(routeHandler).toBeDefined();\n\n const fulfill = vi.fn(async () => undefined);\n const route = {\n request: () => ({\n url: () => 'https://naruon.test/api/data/quality-surface/evidence-snapshot',\n method: () => 'GET',\n }),\n fulfill,\n } as unknown as Route;\n\n await routeHandler!(route);\n\n expect(fulfill).toHaveBeenCalledTimes(1);\n const response = fulfill.mock.calls[0]?.[0] as {\n status?: number;\n contentType?: string;\n body?: string;\n };\n expect(response.status).toBe(200);\n expect(response.contentType).toBe('application/json');\n\n const body = JSON.parse(response.body ?? '{}') as Record;\n expect(body.snapshot_version).toBe('data_quality_evidence_snapshot.v1');\n expect(body.digest_algorithm).toBe('sha256');\n expect(body.snapshot_digest).toMatch(/^[0-9a-f]{64}$/u);\n expect(body.privacy_redaction_policy).toMatchObject({\n raw_content_exposed: false,\n stable_identifiers_exposed: false,\n provider_credentials_exposed: false,\n });\n expect(body.validation_status).toMatchObject({ status_code: 'ready' });\n expect(body.verification_handoff).toMatchObject({\n accepted_input: 'data_quality_evidence_snapshot.v1 JSON',\n digest_algorithm: 'sha256',\n success_exit_code: 0,\n });\n });\n});\n""", + encoding="utf-8", + ) + PY + + - name: Install frontend dependencies + run: cd frontend && pnpm install --frozen-lockfile + + - name: Prove focused RED repair + run: | + set -euo pipefail + cd frontend + pnpm exec vitest run src/lib/data-evidence-snapshot-e2e-fixture.test.ts + pnpm exec eslint tests/e2e/helpers.ts src/lib/data-evidence-snapshot-e2e-fixture.test.ts --max-warnings 0 + + - name: Commit helper-free exact child + shell: bash + env: + BRANCH_NAME: fix/stacked-pr-trigger-foundation + run: | + set -euo pipefail + expected="${GITHUB_SHA}" + remote_head="$(git ls-remote origin "refs/heads/${BRANCH_NAME}" | awk '{print $1}')" + if [[ "${remote_head}" != "${expected}" ]]; then + echo "Remote head moved: expected ${expected}, got ${remote_head}" >&2 + exit 1 + fi + + rm .github/workflows/one-shot-repair-data-evidence-fixture.yml + + mapfile -t changed < <(git status --short | sed -E 's/^.. //' | sort) + expected_paths=( + ".github/workflows/one-shot-repair-data-evidence-fixture.yml" + "frontend/src/lib/data-evidence-snapshot-e2e-fixture.test.ts" + "frontend/tests/e2e/helpers.ts" + ) + mapfile -t expected_sorted < <(printf '%s\n' "${expected_paths[@]}" | sort) + if [[ "$(printf '%s\n' "${changed[@]}")" != "$(printf '%s\n' "${expected_sorted[@]}")" ]]; then + printf 'Unexpected changed paths:\n%s\n' "${changed[*]}" >&2 + exit 1 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "test(ci): cover Data evidence snapshot fixture" + git push origin "HEAD:${BRANCH_NAME}" From 6b59cf471f91223c57d6a58fa02c6d6581d4b4e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 24 Sep 2026 17:50:28 +0900 Subject: [PATCH 09/11] fix(ci): repair Data fixture adopter bootstrap --- ...emporary-data-fixture-product-adopter.yml} | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) rename .github/workflows/{one-shot-repair-data-evidence-fixture.yml => temporary-data-fixture-product-adopter.yml} (88%) diff --git a/.github/workflows/one-shot-repair-data-evidence-fixture.yml b/.github/workflows/temporary-data-fixture-product-adopter.yml similarity index 88% rename from .github/workflows/one-shot-repair-data-evidence-fixture.yml rename to .github/workflows/temporary-data-fixture-product-adopter.yml index 05c6a7e72..8a439b973 100644 --- a/.github/workflows/one-shot-repair-data-evidence-fixture.yml +++ b/.github/workflows/temporary-data-fixture-product-adopter.yml @@ -1,4 +1,4 @@ -name: One-shot repair Data evidence fixture +name: Temporary Data fixture product adopter on: push: @@ -9,8 +9,8 @@ permissions: contents: write jobs: - repair: - name: Repair shared Playwright Data evidence fixture + adopt: + name: Adopt shared Playwright Data evidence fixture runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) @@ -28,10 +28,8 @@ jobs: uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "24" - cache: pnpm - cache-dependency-path: frontend/pnpm-lock.yaml - - name: Install pnpm + - name: Enable pnpm run: corepack enable pnpm - name: Apply bounded fixture repair and regression @@ -121,14 +119,14 @@ jobs: - name: Install frontend dependencies run: cd frontend && pnpm install --frozen-lockfile - - name: Prove focused RED repair + - name: Prove focused repair run: | set -euo pipefail cd frontend pnpm exec vitest run src/lib/data-evidence-snapshot-e2e-fixture.test.ts pnpm exec eslint tests/e2e/helpers.ts src/lib/data-evidence-snapshot-e2e-fixture.test.ts --max-warnings 0 - - name: Commit helper-free exact child + - name: Commit and push product delta non-force shell: bash env: BRANCH_NAME: fix/stacked-pr-trigger-foundation @@ -141,22 +139,17 @@ jobs: exit 1 fi - rm .github/workflows/one-shot-repair-data-evidence-fixture.yml - - mapfile -t changed < <(git status --short | sed -E 's/^.. //' | sort) - expected_paths=( - ".github/workflows/one-shot-repair-data-evidence-fixture.yml" - "frontend/src/lib/data-evidence-snapshot-e2e-fixture.test.ts" - "frontend/tests/e2e/helpers.ts" - ) - mapfile -t expected_sorted < <(printf '%s\n' "${expected_paths[@]}" | sort) - if [[ "$(printf '%s\n' "${changed[@]}")" != "$(printf '%s\n' "${expected_sorted[@]}")" ]]; then - printf 'Unexpected changed paths:\n%s\n' "${changed[*]}" >&2 - exit 1 - fi + git add -- frontend/tests/e2e/helpers.ts frontend/src/lib/data-evidence-snapshot-e2e-fixture.test.ts + git diff --cached --check + git diff --cached --name-only | sort > /tmp/actual-paths + cat > /tmp/expected-paths <<'EOF' + frontend/src/lib/data-evidence-snapshot-e2e-fixture.test.ts + frontend/tests/e2e/helpers.ts + EOF + sed -i 's/^ //' /tmp/expected-paths + diff -u /tmp/expected-paths /tmp/actual-paths git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A git commit -m "test(ci): cover Data evidence snapshot fixture" git push origin "HEAD:${BRANCH_NAME}" From 0226514dc7f440a2b09feea89232c9ab5744e150 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:59:19 +0000 Subject: [PATCH 10/11] test(ci): cover Data evidence snapshot fixture --- ...data-evidence-snapshot-e2e-fixture.test.ts | 56 +++++++++++++++++++ frontend/tests/e2e/helpers.ts | 50 +++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 frontend/src/lib/data-evidence-snapshot-e2e-fixture.test.ts diff --git a/frontend/src/lib/data-evidence-snapshot-e2e-fixture.test.ts b/frontend/src/lib/data-evidence-snapshot-e2e-fixture.test.ts new file mode 100644 index 000000000..5e4dc530d --- /dev/null +++ b/frontend/src/lib/data-evidence-snapshot-e2e-fixture.test.ts @@ -0,0 +1,56 @@ +import type { Page, Route } from '@playwright/test'; +import { describe, expect, it, vi } from 'vitest'; + +import { mockDashboardApi } from '../../tests/e2e/helpers'; + +type RouteHandler = (route: Route) => Promise; + +describe('mockDashboardApi Data evidence snapshot', () => { + it('serves a redacted verifier-ready snapshot instead of falling through to 404', async () => { + let routeHandler: RouteHandler | undefined; + const page = { + route: vi.fn(async (_pattern: string, handler: RouteHandler) => { + routeHandler = handler; + }), + } as unknown as Page; + + await mockDashboardApi(page); + expect(routeHandler).toBeDefined(); + + const fulfill = vi.fn(async () => undefined); + const route = { + request: () => ({ + url: () => 'https://naruon.test/api/data/quality-surface/evidence-snapshot', + method: () => 'GET', + }), + fulfill, + } as unknown as Route; + + await routeHandler!(route); + + expect(fulfill).toHaveBeenCalledTimes(1); + const response = fulfill.mock.calls[0]?.[0] as { + status?: number; + contentType?: string; + body?: string; + }; + expect(response.status).toBe(200); + expect(response.contentType).toBe('application/json'); + + const body = JSON.parse(response.body ?? '{}') as Record; + expect(body.snapshot_version).toBe('data_quality_evidence_snapshot.v1'); + expect(body.digest_algorithm).toBe('sha256'); + expect(body.snapshot_digest).toMatch(/^[0-9a-f]{64}$/u); + expect(body.privacy_redaction_policy).toMatchObject({ + raw_content_exposed: false, + stable_identifiers_exposed: false, + provider_credentials_exposed: false, + }); + expect(body.validation_status).toMatchObject({ status_code: 'ready' }); + expect(body.verification_handoff).toMatchObject({ + accepted_input: 'data_quality_evidence_snapshot.v1 JSON', + digest_algorithm: 'sha256', + success_exit_code: 0, + }); + }); +}); diff --git a/frontend/tests/e2e/helpers.ts b/frontend/tests/e2e/helpers.ts index d98fac63d..8a58bbc75 100644 --- a/frontend/tests/e2e/helpers.ts +++ b/frontend/tests/e2e/helpers.ts @@ -745,6 +745,51 @@ const dataQualitySurface = { ], }; +const dataEvidenceSnapshot = { + snapshot_version: 'data_quality_evidence_snapshot.v1', + generated_at: '2026-05-28T05:47:00Z', + audit_event: 'data.quality_surface.evidence_snapshot.viewed', + scope_label: 'playwright_dashboard_fixture', + snapshot_digest: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + digest_algorithm: 'sha256', + canonical_payload_fields: [], + privacy_redaction_policy: { + raw_content_exposed: false, + stable_identifiers_exposed: false, + provider_credentials_exposed: false, + redacted_fields: [], + allowed_sample_fields: [], + }, + validation_status: { + status_code: 'ready', + display_name: 'Ready', + detail_text: 'Playwright evidence snapshot is redacted and verifier-ready.', + provider_write_executed: false, + }, + verification_handoff: { + handoff_text: 'Verify the copied Playwright snapshot JSON before sharing diligence materials.', + verifier_command: 'python scripts/verify_evidence_snapshot.py snapshot.json', + accepted_input: 'data_quality_evidence_snapshot.v1 JSON', + digest_algorithm: 'sha256', + excluded_digest_fields: ['snapshot_digest'], + success_exit_code: 0, + failure_exit_codes: { invalid_digest: 2 }, + provider_write_executed: false, + }, + parser_manifest_summary: [], + content_graph_evidence_samples: [], + knowledge_graph_evidence_samples: [], + evidence_packet_checklist: [], + data_room_package_manifest: [], + diligence_exception_register: [], + diligence_risk_matrix: [], + diligence_close_artifact_review_queue: [], + diligence_close_owner_handoff_queue: [], + diligence_close_traceability_map: [], + diligence_close_decision_summary: null, + diligence_close_proof_plan: [], +}; + const accountConfig = { user_id: 'default', smtp_server: 'smtp.example.com', @@ -876,6 +921,11 @@ export async function mockDashboardApi(page: Page, onApiRequest?: (path: string, return; } + if (path === '/api/data/quality-surface/evidence-snapshot' && request.method() === 'GET') { + await fulfillJson(route, dataEvidenceSnapshot); + return; + } + if ( path === '/api/data/documents/doc_repository_ready/webdav-materialization-intent' && request.method() === 'POST' From 892a52ad9834151628c3cbc7b19e9f533cd98124 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 24 Sep 2026 21:49:30 +0900 Subject: [PATCH 11/11] chore(ci): remove completed Data fixture adopter --- ...temporary-data-fixture-product-adopter.yml | 155 ------------------ 1 file changed, 155 deletions(-) delete mode 100644 .github/workflows/temporary-data-fixture-product-adopter.yml diff --git a/.github/workflows/temporary-data-fixture-product-adopter.yml b/.github/workflows/temporary-data-fixture-product-adopter.yml deleted file mode 100644 index 8a439b973..000000000 --- a/.github/workflows/temporary-data-fixture-product-adopter.yml +++ /dev/null @@ -1,155 +0,0 @@ -name: Temporary Data fixture product adopter - -on: - push: - branches: - - fix/stacked-pr-trigger-foundation - -permissions: - contents: write - -jobs: - adopt: - name: Adopt shared Playwright Data evidence fixture - runs-on: ubuntu-latest - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 - with: - egress-policy: audit - - - name: Checkout exact staging head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: true - ref: ${{ github.sha }} - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: "24" - - - name: Enable pnpm - run: corepack enable pnpm - - - name: Apply bounded fixture repair and regression - shell: bash - run: | - set -euo pipefail - python - <<'PY' - from pathlib import Path - - helper_path = Path("frontend/tests/e2e/helpers.ts") - test_path = Path("frontend/src/lib/data-evidence-snapshot-e2e-fixture.test.ts") - helper = helper_path.read_text(encoding="utf-8") - - if "const dataEvidenceSnapshot =" in helper: - raise SystemExit("dataEvidenceSnapshot fixture already exists; refusing ambiguous rewrite") - if "/api/data/quality-surface/evidence-snapshot" in helper: - raise SystemExit("evidence-snapshot route already exists; refusing ambiguous rewrite") - if test_path.exists(): - raise SystemExit(f"{test_path} already exists; refusing overwrite") - - fixture_anchor = "\nconst accountConfig = {\n" - if helper.count(fixture_anchor) != 1: - raise SystemExit("accountConfig insertion anchor is not unique") - - fixture = r''' - const dataEvidenceSnapshot = { - snapshot_version: 'data_quality_evidence_snapshot.v1', - generated_at: '2026-05-28T05:47:00Z', - audit_event: 'data.quality_surface.evidence_snapshot.viewed', - scope_label: 'playwright_dashboard_fixture', - snapshot_digest: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', - digest_algorithm: 'sha256', - canonical_payload_fields: [], - privacy_redaction_policy: { - raw_content_exposed: false, - stable_identifiers_exposed: false, - provider_credentials_exposed: false, - redacted_fields: [], - allowed_sample_fields: [], - }, - validation_status: { - status_code: 'ready', - display_name: 'Ready', - detail_text: 'Playwright evidence snapshot is redacted and verifier-ready.', - provider_write_executed: false, - }, - verification_handoff: { - handoff_text: 'Verify the copied Playwright snapshot JSON before sharing diligence materials.', - verifier_command: 'python scripts/verify_evidence_snapshot.py snapshot.json', - accepted_input: 'data_quality_evidence_snapshot.v1 JSON', - digest_algorithm: 'sha256', - excluded_digest_fields: ['snapshot_digest'], - success_exit_code: 0, - failure_exit_codes: { invalid_digest: 2 }, - provider_write_executed: false, - }, - parser_manifest_summary: [], - content_graph_evidence_samples: [], - knowledge_graph_evidence_samples: [], - evidence_packet_checklist: [], - data_room_package_manifest: [], - diligence_exception_register: [], - diligence_risk_matrix: [], - diligence_close_artifact_review_queue: [], - diligence_close_owner_handoff_queue: [], - diligence_close_traceability_map: [], - diligence_close_decision_summary: null, - diligence_close_proof_plan: [], - }; - ''' - helper = helper.replace(fixture_anchor, "\n" + fixture.strip() + "\n" + fixture_anchor, 1) - - route_anchor = """ if (path === '/api/data/quality-surface' && request.method() === 'GET') {\n await fulfillJson(route, dataQualitySurface);\n return;\n }\n\n""" - if helper.count(route_anchor) != 1: - raise SystemExit("data quality route insertion anchor is not unique") - - evidence_route = """ if (path === '/api/data/quality-surface/evidence-snapshot' && request.method() === 'GET') {\n await fulfillJson(route, dataEvidenceSnapshot);\n return;\n }\n\n""" - helper = helper.replace(route_anchor, route_anchor + evidence_route, 1) - helper_path.write_text(helper, encoding="utf-8") - - test_path.write_text( - """import type { Page, Route } from '@playwright/test';\nimport { describe, expect, it, vi } from 'vitest';\n\nimport { mockDashboardApi } from '../../tests/e2e/helpers';\n\ntype RouteHandler = (route: Route) => Promise;\n\ndescribe('mockDashboardApi Data evidence snapshot', () => {\n it('serves a redacted verifier-ready snapshot instead of falling through to 404', async () => {\n let routeHandler: RouteHandler | undefined;\n const page = {\n route: vi.fn(async (_pattern: string, handler: RouteHandler) => {\n routeHandler = handler;\n }),\n } as unknown as Page;\n\n await mockDashboardApi(page);\n expect(routeHandler).toBeDefined();\n\n const fulfill = vi.fn(async () => undefined);\n const route = {\n request: () => ({\n url: () => 'https://naruon.test/api/data/quality-surface/evidence-snapshot',\n method: () => 'GET',\n }),\n fulfill,\n } as unknown as Route;\n\n await routeHandler!(route);\n\n expect(fulfill).toHaveBeenCalledTimes(1);\n const response = fulfill.mock.calls[0]?.[0] as {\n status?: number;\n contentType?: string;\n body?: string;\n };\n expect(response.status).toBe(200);\n expect(response.contentType).toBe('application/json');\n\n const body = JSON.parse(response.body ?? '{}') as Record;\n expect(body.snapshot_version).toBe('data_quality_evidence_snapshot.v1');\n expect(body.digest_algorithm).toBe('sha256');\n expect(body.snapshot_digest).toMatch(/^[0-9a-f]{64}$/u);\n expect(body.privacy_redaction_policy).toMatchObject({\n raw_content_exposed: false,\n stable_identifiers_exposed: false,\n provider_credentials_exposed: false,\n });\n expect(body.validation_status).toMatchObject({ status_code: 'ready' });\n expect(body.verification_handoff).toMatchObject({\n accepted_input: 'data_quality_evidence_snapshot.v1 JSON',\n digest_algorithm: 'sha256',\n success_exit_code: 0,\n });\n });\n});\n""", - encoding="utf-8", - ) - PY - - - name: Install frontend dependencies - run: cd frontend && pnpm install --frozen-lockfile - - - name: Prove focused repair - run: | - set -euo pipefail - cd frontend - pnpm exec vitest run src/lib/data-evidence-snapshot-e2e-fixture.test.ts - pnpm exec eslint tests/e2e/helpers.ts src/lib/data-evidence-snapshot-e2e-fixture.test.ts --max-warnings 0 - - - name: Commit and push product delta non-force - shell: bash - env: - BRANCH_NAME: fix/stacked-pr-trigger-foundation - run: | - set -euo pipefail - expected="${GITHUB_SHA}" - remote_head="$(git ls-remote origin "refs/heads/${BRANCH_NAME}" | awk '{print $1}')" - if [[ "${remote_head}" != "${expected}" ]]; then - echo "Remote head moved: expected ${expected}, got ${remote_head}" >&2 - exit 1 - fi - - git add -- frontend/tests/e2e/helpers.ts frontend/src/lib/data-evidence-snapshot-e2e-fixture.test.ts - git diff --cached --check - git diff --cached --name-only | sort > /tmp/actual-paths - cat > /tmp/expected-paths <<'EOF' - frontend/src/lib/data-evidence-snapshot-e2e-fixture.test.ts - frontend/tests/e2e/helpers.ts - EOF - sed -i 's/^ //' /tmp/expected-paths - diff -u /tmp/expected-paths /tmp/actual-paths - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "test(ci): cover Data evidence snapshot fixture" - git push origin "HEAD:${BRANCH_NAME}"