From 96f83bb4353d6f313e94fc3446095ceca6b393f2 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Thu, 1 Oct 2026 09:37:00 +0800 Subject: [PATCH] fix(build): move the persisted-replay research harness out of frontend/scripts; guard runtime sources against tests/ imports (BUG-1140) The staging publish job built the web image without frontend/tests, and next build type-checks every .ts under frontend/, so the harness importing ../tests/helpers/postgres-fixture.ts failed with TS2307. The harness depends on the test fixture, so it now lives in frontend/tests/research; the Python driver points there. A new test fails if src/ or scripts/ import tests/. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8 --- .../rectification-segment-persisted-replay.ts | 30 +++++++-------- .../runtime-sources-no-test-imports.test.ts | 37 +++++++++++++++++++ .../varga_resolution_persisted_replay.py | 2 +- 3 files changed, 53 insertions(+), 16 deletions(-) rename frontend/{scripts => tests/research}/rectification-segment-persisted-replay.ts (91%) create mode 100644 frontend/tests/runtime-sources-no-test-imports.test.ts diff --git a/frontend/scripts/rectification-segment-persisted-replay.ts b/frontend/tests/research/rectification-segment-persisted-replay.ts similarity index 91% rename from frontend/scripts/rectification-segment-persisted-replay.ts rename to frontend/tests/research/rectification-segment-persisted-replay.ts index 12c24e0e..141315af 100644 --- a/frontend/scripts/rectification-segment-persisted-replay.ts +++ b/frontend/tests/research/rectification-segment-persisted-replay.ts @@ -2,20 +2,20 @@ import assert from "node:assert/strict"; import { randomUUID } from "node:crypto"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; -import { closeLocalPostgresDataPool, createLocalPostgresDataClient } from "../src/lib/db/local-postgres-client-core.ts"; -import { resolveActiveSkillPackage } from "../src/lib/skill-package-registry.ts"; -import { publicNextAction } from "../src/lib/rectification-agentic/core/rectification-decision.ts"; -import { rawMinuteWeights, segmentsForMinutes, summarizeSegmentWeights, targetChartsForDomain } from "../src/lib/rectification-agentic/core/segment-summary.ts"; -import { applyRectificationChoice, inferenceForPersistedAnswer, persistNextInterviewAfterChoice } from "../src/lib/rectification-agentic/v9/answer-choice.ts"; -import { CHOICE_ACTION, outcomeIdForOption } from "../src/lib/rectification-agentic/v9/choice-action.ts"; -import type { ChoiceKey } from "../src/lib/rectification-agentic/v9/choice-card.ts"; -import { decideAfterInferenceChange, rectificationFollowupCatalog } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts"; -import type { EvidenceKind } from "../src/lib/rectification-agentic/v9/evidence-model.ts"; -import { resolveRectificationProductDomain } from "../src/lib/rectification-agentic/v9/product-domain.ts"; -import { scoreAndPersistCurrentEvidence } from "../src/lib/rectification-agentic/v9/score-persist.ts"; -import { confirmV9Evidence, loadV9CaseDossier, proposeV9Evidence } from "../src/lib/rectification-agentic/v9/tool-service.ts"; -import { startPostgresFixture } from "../tests/helpers/postgres-fixture.ts"; -import { segmentReplayOracleAnswer } from "./rectification-segment-oracle.ts"; +import { closeLocalPostgresDataPool, createLocalPostgresDataClient } from "../../src/lib/db/local-postgres-client-core.ts"; +import { resolveActiveSkillPackage } from "../../src/lib/skill-package-registry.ts"; +import { publicNextAction } from "../../src/lib/rectification-agentic/core/rectification-decision.ts"; +import { rawMinuteWeights, segmentsForMinutes, summarizeSegmentWeights, targetChartsForDomain } from "../../src/lib/rectification-agentic/core/segment-summary.ts"; +import { applyRectificationChoice, inferenceForPersistedAnswer, persistNextInterviewAfterChoice } from "../../src/lib/rectification-agentic/v9/answer-choice.ts"; +import { CHOICE_ACTION, outcomeIdForOption } from "../../src/lib/rectification-agentic/v9/choice-action.ts"; +import type { ChoiceKey } from "../../src/lib/rectification-agentic/v9/choice-card.ts"; +import { decideAfterInferenceChange, rectificationFollowupCatalog } from "../../src/lib/rectification-agentic/v9/decision-from-dossier.ts"; +import type { EvidenceKind } from "../../src/lib/rectification-agentic/v9/evidence-model.ts"; +import { resolveRectificationProductDomain } from "../../src/lib/rectification-agentic/v9/product-domain.ts"; +import { scoreAndPersistCurrentEvidence } from "../../src/lib/rectification-agentic/v9/score-persist.ts"; +import { confirmV9Evidence, loadV9CaseDossier, proposeV9Evidence } from "../../src/lib/rectification-agentic/v9/tool-service.ts"; +import { startPostgresFixture } from "../helpers/postgres-fixture.ts"; +import { segmentReplayOracleAnswer } from "../../scripts/rectification-segment-oracle.ts"; // Fictional owned Case seeds only. Production turn/evidence/choice calls are // never replaced by fixture inserts, including when the original ACL refuses. @@ -36,7 +36,7 @@ const url = fixture.connectionUrl("service_runtime", "service-runtime-test-passw const authenticatedUrl = fixture.connectionUrl("app_runtime", "app-runtime-test-password"); let fatal: string | null = null; try { - const migrated = spawnSync(process.execPath, [fileURLToPath(new URL("./db-migrate.mjs", import.meta.url))], { + const migrated = spawnSync(process.execPath, [fileURLToPath(new URL("../../scripts/db-migrate.mjs", import.meta.url))], { encoding: "utf8", env: { ...process.env, SCHEMA_DATABASE_URL: fixture.connectionUrl("schema_owner", "schema-owner-test-password") }, }); assert.equal(migrated.status, 0, migrated.stderr); diff --git a/frontend/tests/runtime-sources-no-test-imports.test.ts b/frontend/tests/runtime-sources-no-test-imports.test.ts new file mode 100644 index 00000000..962e0d28 --- /dev/null +++ b/frontend/tests/runtime-sources-no-test-imports.test.ts @@ -0,0 +1,37 @@ +import assert from "node:assert/strict"; +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +// BUG-1140: deploy/railway-web.Dockerfile copies frontend/src and frontend/scripts but not +// frontend/tests, and `next build` type-checks every *.ts under frontend/. A runtime or script +// file that imports from tests/ builds locally and then fails the staging image build. +const frontend = fileURLToPath(new URL("..", import.meta.url)); +const roots = ["src", "scripts"]; +const importsTests = /(?:from\s+|import\s*\(\s*|new URL\(\s*)["'](?:\.\.?\/)+(?:[^"']*\/)?tests\//; + +function sources(directory: string): string[] { + return readdirSync(directory).flatMap((name) => { + const path = join(directory, name); + if (statSync(path).isDirectory()) return sources(path); + return /\.(?:ts|tsx|mts|mjs|js)$/.test(name) ? [path] : []; + }); +} + +test("files copied into the web image never import from frontend/tests", () => { + const offenders = roots + .flatMap((root) => sources(join(frontend, root))) + .filter((path) => importsTests.test(readFileSync(path, "utf8"))) + .map((path) => relative(frontend, path)); + assert.deepEqual(offenders, []); +}); + +test("the guard recognises the shapes that broke the build", () => { + for (const line of [ + 'import { startPostgresFixture } from "../tests/helpers/postgres-fixture.ts";', + "const fixture = await import('../../tests/helpers/postgres-fixture.ts');", + 'new URL("../tests/fixtures/x.json", import.meta.url)', + ]) assert.ok(importsTests.test(line), line); + assert.equal(importsTests.test('import { x } from "../src/lib/tests-helpers.ts";'), false); +}); diff --git a/scripts/research/varga_resolution_persisted_replay.py b/scripts/research/varga_resolution_persisted_replay.py index 3b3e2025..c578abeb 100644 --- a/scripts/research/varga_resolution_persisted_replay.py +++ b/scripts/research/varga_resolution_persisted_replay.py @@ -58,7 +58,7 @@ def main() -> int: thread.start() try: env = {**os.environ, "JYOTISH_API_BASE": origin} - result = subprocess.run(["node", "--import", "tsx", "scripts/rectification-segment-persisted-replay.ts"], + result = subprocess.run(["node", "--import", "tsx", "tests/research/rectification-segment-persisted-replay.ts"], cwd=ROOT / "frontend", env=env, input=json.dumps({"rows": rows}), text=True, encoding="utf-8", stdout=subprocess.PIPE, check=False) payload = json.loads(result.stdout)