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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-10-01 09:37:00 +08:00
co-authored by Claude Opus 5.5
parent 437dcecbf2
commit 96f83bb435
3 changed files with 53 additions and 16 deletions
@@ -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);
@@ -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);
});