@afokapu/atdd-bun 0.10.54 → 0.10.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/detectors/bun_interlocking_binding/checks/interlocking_runtime_executes.mjs +46 -1
- package/detectors/bun_interlocking_binding/detect.mjs +2 -2
- package/detectors/bun_interlocking_binding/fixtures/clean/server.ts +2 -2
- package/detectors/bun_interlocking_binding/fixtures/clean/src/trains/runner.ts +21 -3
- package/detectors/bun_interlocking_binding/fixtures/clean_flow_style_actions/server.ts +2 -2
- package/detectors/bun_interlocking_binding/fixtures/clean_flow_style_actions/src/trains/runner.ts +21 -3
- package/detectors/bun_interlocking_coverage/_shared/interlocking.mjs +181 -2
- package/detectors/bun_interlocking_coverage/checks/interlocking_production_runner.mjs +9 -4
- package/detectors/bun_interlocking_coverage/checks/interlocking_route_coverage.mjs +4 -4
- package/detectors/bun_interlocking_coverage/checks/interlocking_smoke_coverage.mjs +10 -3
- package/detectors/bun_interlocking_coverage/checks/interlocking_trace_binds.mjs +7 -5
- package/detectors/bun_interlocking_coverage/checks/interlocking_train_sequence.mjs +8 -13
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_production_runner/convex/trains/interlocking.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_production_runner/convex/trains/runner.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_route_coverage/convex/trains/interlocking.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_route_coverage/convex/trains/runner.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_smoke_coverage/convex/trains/interlocking.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_smoke_coverage/convex/trains/runner.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_smoke_coverage_module/convex/trains/interlocking.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_smoke_coverage_module/convex/trains/runner.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_trace_binds/convex/trains/interlocking.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/interlocking_trace_binds/convex/trains/runner.ts +1 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/interlocking_smoke_coverage_inert_module/e2e/smoke/resolve-match.smoke.test.ts +12 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/interlocking_smoke_coverage_inert_module/plan/_trains/_interlockings/match-resolution.yaml +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/interlocking_smoke_coverage_inert_module/src/server.ts +13 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/interlocking_smoke_coverage_mismatched_action/e2e/smoke/resolve-match.smoke.test.ts +12 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/interlocking_smoke_coverage_mismatched_action/plan/_trains/_interlockings/match-resolution.yaml +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/interlocking_smoke_coverage_mismatched_action/src/server.ts +9 -0
- package/detectors/bun_interlocking_infrastructure/checks/interlocking_station_routing.mjs +19 -0
- package/detectors/bun_interlocking_infrastructure/checks/journey_runner_boundary.mjs +27 -0
- package/detectors/bun_interlocking_infrastructure/checks/journey_station_routing.mjs +30 -0
- package/detectors/bun_interlocking_infrastructure/checks/wagon_contract_honoured.mjs +112 -12
- package/detectors/bun_interlocking_infrastructure/fixtures/clean/journey_runner_boundary/src/trains/journey.ts +17 -4
- package/detectors/bun_interlocking_infrastructure/fixtures/clean/journey_station_routing/src/trains/journey.ts +16 -3
- package/detectors/bun_interlocking_infrastructure/fixtures/clean/journey_topology_traversal/plan/_journeys/checkout.yaml +19 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/clean/journey_topology_traversal/server.ts +13 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/clean/journey_topology_traversal/src/trains/journey.ts +36 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/clean/wagon_contract_honoured/src/trains/runner.ts +5 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_dead_read/plan/_journeys/checkout.yaml +14 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_dead_read/src/trains/journey.ts +15 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_returns_early/plan/_journeys/checkout.yaml +19 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_returns_early/src/trains/journey.ts +16 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_self_loop/plan/_journeys/checkout.yaml +14 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_continuation_self_loop/src/trains/journey.ts +18 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_station_inert_runner/plan/_journeys/public.yaml +14 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_station_inert_runner/server.ts +16 -0
- package/detectors/bun_interlocking_infrastructure/fixtures/dirty/journey_station_inert_runner/src/trains/journey.ts +9 -0
- package/integrity.json +48 -19
- package/package.json +1 -1
|
@@ -29,6 +29,7 @@ export const RULE_EXECUTES = "coder.bun.runtime-executes-the-declaration";
|
|
|
29
29
|
const LOADS_DECLARATION =
|
|
30
30
|
/\breadFileSync\s*\(|\breadFile\s*\(|\bBun\s*\.\s*file\s*\(|\bcreateReadStream\s*\(|\bfs\s*\.\s*promises\s*\.\s*readFile\b/;
|
|
31
31
|
const RESOLVES = /\bInterlockingResolution\b|\bresolveTrain\s*\(/;
|
|
32
|
+
const TRAIN_RUNNER = /\bclass\s+TrainRunner\b/;
|
|
32
33
|
const EXCLUDES = ["node_modules", "dist", "build", ".next", "_generated"];
|
|
33
34
|
const PLAN_ROOT = process.env.ATDD_PLAN_ROOT || "plan";
|
|
34
35
|
|
|
@@ -159,10 +160,54 @@ export function scanExecution(scanRoot) {
|
|
|
159
160
|
return violations;
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
// TrainRunner is the other half of declaration execution. A correctly data-driven
|
|
164
|
+
// InterlockingRunner can select a train while its executor silently ignores that
|
|
165
|
+
// train and returns a hand-written trace. Require the executor itself to read a
|
|
166
|
+
// supplied train declaration, iterate its declared sequence, and feed each wagon's
|
|
167
|
+
// production result into the next call. These deliberately structural checks admit
|
|
168
|
+
// ordinary direct functions (executeWagon/runWagon/runTrain) without prescribing a
|
|
169
|
+
// framework, while rejecting the no-op and literal-trace shapes they replace.
|
|
170
|
+
export function scanTrainRunnerExecution(scanRoot) {
|
|
171
|
+
const violations = [];
|
|
172
|
+
for (const croot of findConsumerRoots(scanRoot)) {
|
|
173
|
+
const trainDeclarations = walk(join(croot, PLAN_ROOT, "_trains"), (f) => /train:.*\.ya?ml$/.test(f));
|
|
174
|
+
if (!trainDeclarations.length) continue;
|
|
175
|
+
const rtFiles = walk(join(croot, "src/trains"), (f) => /\.(ts|tsx|mjs|js)$/.test(f))
|
|
176
|
+
.map((f) => ({ file: f, text: readText(f) }));
|
|
177
|
+
for (const runner of rtFiles.filter((x) => TRAIN_RUNNER.test(x.text))) {
|
|
178
|
+
const readsDeclaration = LOADS_DECLARATION.test(runner.text);
|
|
179
|
+
const usesSuppliedDeclaration = /\b(?:parse|load)(?:Train|Declaration)\s*\(\s*this\.\w*(?:train|declaration|path)\w*\s*\)/i.test(runner.text) ||
|
|
180
|
+
/\b(?:readFileSync|readFile|Bun\s*\.\s*file)\s*\(\s*this\.\w*(?:train|declaration|path)\w*/i.test(runner.text);
|
|
181
|
+
const parsesSequence = /\bsequence\b/.test(runner.text);
|
|
182
|
+
const derivesOrder = /for\s*\(\s*const\s+\w+\s+of\s+\w+\.sequence\s*\)/.test(runner.text);
|
|
183
|
+
// The reassignment is the key data-flow proof: a production execution call
|
|
184
|
+
// receives the prior result and its output becomes the next result.
|
|
185
|
+
const carriesResults = /\b(\w+)\s*=\s*(?:await\s+)?(?:execute|run)\w*\s*\(\s*\w+\s*,\s*\1\s*\)/.test(runner.text);
|
|
186
|
+
const fabricatesTrace = /\b(?:trace|steps?|sequence)\s*:\s*\[\s*["'`]/.test(maskComments(runner.text));
|
|
187
|
+
if (readsDeclaration && usesSuppliedDeclaration && parsesSequence && derivesOrder && carriesResults && !fabricatesTrace) continue;
|
|
188
|
+
const missing = [
|
|
189
|
+
!readsDeclaration && "read the supplied train declaration",
|
|
190
|
+
!usesSuppliedDeclaration && "pass its supplied declaration into that parser or reader",
|
|
191
|
+
!parsesSequence && "parse its sequence",
|
|
192
|
+
!derivesOrder && "derive ordered iteration from train.sequence",
|
|
193
|
+
!carriesResults && "carry each production execution result into the next wagon",
|
|
194
|
+
fabricatesTrace && "avoid fabricating literal trace or step data",
|
|
195
|
+
].filter(Boolean).join("; ");
|
|
196
|
+
const rel = runner.file.startsWith(croot + sep) ? runner.file.slice(croot.length + 1) : runner.file;
|
|
197
|
+
violations.push({
|
|
198
|
+
rule_id: RULE_EXECUTES, file: rel, line: 1, col: 0,
|
|
199
|
+
evidence: `TrainRunner must ${missing}; declaration-selected wagon execution cannot be satisfied by a no-op or transcribed trace`,
|
|
200
|
+
source_line: "",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return violations;
|
|
205
|
+
}
|
|
206
|
+
|
|
162
207
|
if (import.meta.main ?? process.argv[1]?.endsWith("interlocking_runtime_executes.mjs")) {
|
|
163
208
|
let roots = [];
|
|
164
209
|
try { roots = JSON.parse(process.env.ATDD_SCAN_ROOTS || "[]"); } catch {}
|
|
165
|
-
const out = roots.flatMap((r) => scanExecution(r));
|
|
210
|
+
const out = roots.flatMap((r) => [...scanExecution(r), ...scanTrainRunnerExecution(r)]);
|
|
166
211
|
const rp = process.env.ATDD_VIOLATIONS_REPORT;
|
|
167
212
|
if (rp) writeFileSync(rp, JSON.stringify({ violations: out }, null, 2), "utf8");
|
|
168
213
|
process.stderr.write(`bun-executes: ${out.length} violation(s)\n`);
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { excludedPath } from "../../lib/scan.mjs";
|
|
25
25
|
import { parseInterlocking as parseInterlockingDoc } from "../../lib/interlocking.mjs";
|
|
26
26
|
import { readFileSync, writeFileSync, statSync, readdirSync } from "node:fs";
|
|
27
|
-
import { scanExecution } from "./checks/interlocking_runtime_executes.mjs";
|
|
27
|
+
import { scanExecution, scanTrainRunnerExecution } from "./checks/interlocking_runtime_executes.mjs";
|
|
28
28
|
import { join, sep, resolve } from "node:path";
|
|
29
29
|
|
|
30
30
|
const RULE = "coder.bun.interlocking-bilateral-binding";
|
|
@@ -544,7 +544,7 @@ function main() {
|
|
|
544
544
|
// coder.bun.runtime-executes-the-declaration — no longer staged. This family is
|
|
545
545
|
// monolithic rather than checks/-collecting, so it is called here rather than
|
|
546
546
|
// discovered.
|
|
547
|
-
violations.push(...scanExecution(scanRoot));
|
|
547
|
+
violations.push(...scanExecution(scanRoot), ...scanTrainRunnerExecution(scanRoot));
|
|
548
548
|
}
|
|
549
549
|
writeFileSync(reportPath, JSON.stringify({ violations }, null, 2), "utf8");
|
|
550
550
|
process.stderr.write(`bun-interlocking-binding: ${violations.length} violation(s)\n`);
|
|
@@ -12,8 +12,8 @@ export const JOURNEY_MAP = {
|
|
|
12
12
|
|
|
13
13
|
export function dispatch(action: string, inputs: Record<string, unknown>) {
|
|
14
14
|
const mapping = (JOURNEY_MAP as Record<string, unknown>)[action];
|
|
15
|
-
if (typeof mapping === "string") return new TrainRunner(mapping).execute(inputs);
|
|
15
|
+
if (typeof mapping === "string") return new TrainRunner(`plan/_trains/${mapping}.yaml`, mapping).execute(inputs);
|
|
16
16
|
const { path } = mapping as { interlockingId: string; path: string };
|
|
17
17
|
const res = new InterlockingRunner(path).resolveTrain(action, inputs);
|
|
18
|
-
return new TrainRunner(res.trainId).execute(inputs);
|
|
18
|
+
return new TrainRunner(res.trainPath, res.trainId).execute(inputs);
|
|
19
19
|
}
|
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
// TrainRunner — the
|
|
1
|
+
// TrainRunner — reads the selected train declaration and executes its declared wagons in order.
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
type Result = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
function parseTrain(path: string): { sequence: string[] } {
|
|
7
|
+
const text = readFileSync(path, "utf8");
|
|
8
|
+
return { sequence: [...text.matchAll(/^\s*-\s*([^#\s]+)\s*$/gm)].map((match) => match[1]) };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function executeWagon(wagon: string, result: Result): Result {
|
|
12
|
+
return { ...result, [wagon]: "executed" };
|
|
13
|
+
}
|
|
14
|
+
|
|
2
15
|
export class TrainRunner {
|
|
3
|
-
constructor(private readonly
|
|
16
|
+
constructor(private readonly trainDeclarationPath: string, private readonly trainId?: string) {}
|
|
4
17
|
execute(inputs: Record<string, unknown>) {
|
|
5
|
-
|
|
18
|
+
const train = parseTrain(this.trainDeclarationPath);
|
|
19
|
+
let result: Result = inputs;
|
|
20
|
+
for (const wagon of train.sequence) {
|
|
21
|
+
result = executeWagon(wagon, result);
|
|
22
|
+
}
|
|
23
|
+
return { selectedTrainId: this.trainId, result };
|
|
6
24
|
}
|
|
7
25
|
}
|
|
@@ -12,8 +12,8 @@ export const JOURNEY_MAP = {
|
|
|
12
12
|
|
|
13
13
|
export function dispatch(action: string, inputs: Record<string, unknown>) {
|
|
14
14
|
const mapping = (JOURNEY_MAP as Record<string, unknown>)[action];
|
|
15
|
-
if (typeof mapping === "string") return new TrainRunner(mapping).execute(inputs);
|
|
15
|
+
if (typeof mapping === "string") return new TrainRunner(`plan/_trains/${mapping}.yaml`, mapping).execute(inputs);
|
|
16
16
|
const { path } = mapping as { interlockingId: string; path: string };
|
|
17
17
|
const res = new InterlockingRunner(path).resolveTrain(action, inputs);
|
|
18
|
-
return new TrainRunner(res.trainId).execute(inputs);
|
|
18
|
+
return new TrainRunner(res.trainPath, res.trainId).execute(inputs);
|
|
19
19
|
}
|
package/detectors/bun_interlocking_binding/fixtures/clean_flow_style_actions/src/trains/runner.ts
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
// TrainRunner — the
|
|
1
|
+
// TrainRunner — reads the selected train declaration and executes its declared wagons in order.
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
type Result = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
function parseTrain(path: string): { sequence: string[] } {
|
|
7
|
+
const text = readFileSync(path, "utf8");
|
|
8
|
+
return { sequence: [...text.matchAll(/^\s*-\s*([^#\s]+)\s*$/gm)].map((match) => match[1]) };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function executeWagon(wagon: string, result: Result): Result {
|
|
12
|
+
return { ...result, [wagon]: "executed" };
|
|
13
|
+
}
|
|
14
|
+
|
|
2
15
|
export class TrainRunner {
|
|
3
|
-
constructor(private readonly
|
|
16
|
+
constructor(private readonly trainDeclarationPath: string, private readonly trainId?: string) {}
|
|
4
17
|
execute(inputs: Record<string, unknown>) {
|
|
5
|
-
|
|
18
|
+
const train = parseTrain(this.trainDeclarationPath);
|
|
19
|
+
let result: Result = inputs;
|
|
20
|
+
for (const wagon of train.sequence) {
|
|
21
|
+
result = executeWagon(wagon, result);
|
|
22
|
+
}
|
|
23
|
+
return { selectedTrainId: this.trainId, result };
|
|
6
24
|
}
|
|
7
25
|
}
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// data (snake_case, plan/_trains/_interlockings/**); the e2e tests are Bun/TS under e2e/**.
|
|
8
8
|
|
|
9
9
|
import { excludedPath } from "../../../lib/scan.mjs";
|
|
10
|
-
import { readFileSync, statSync, readdirSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { join, sep } from "node:path";
|
|
10
|
+
import { existsSync, readFileSync, statSync, readdirSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
12
12
|
|
|
13
13
|
export const DEFAULT_EXCLUDES = ["_generated", "node_modules", "dist", "build", ".next"];
|
|
14
14
|
export const PLAN_ROOT = process.env.ATDD_PLAN_ROOT || "plan";
|
|
@@ -315,6 +315,185 @@ export function isInterlockingTest(text, tokens) {
|
|
|
315
315
|
return tokens.some((t) => tokenCovered(t, text));
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
// Semantic witnesses for the production call model. Tokens get us to a candidate test; these
|
|
319
|
+
// small, deliberately conservative recognisers decide whether the candidate actually connects
|
|
320
|
+
// `InterlockingRunner.resolveTrain()` to `TrainRunner.execute()` and asserts evidence from that
|
|
321
|
+
// chain. We keep this in the shared layer so route, runner, trace, smoke, and sequence checks
|
|
322
|
+
// cannot quietly drift back to accepting a different descriptive proxy.
|
|
323
|
+
const ident = "[A-Za-z_$][\\w$]*";
|
|
324
|
+
|
|
325
|
+
function escaped(value) {
|
|
326
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function productionReceivers(text) {
|
|
330
|
+
const out = new Set();
|
|
331
|
+
const allocated = new RegExp(`\\b(?:const|let|var)\\s+(${ident})\\s*=\\s*new\\s+${PROD_INTERLOCKING}\\b`, "g");
|
|
332
|
+
for (const m of text.matchAll(allocated)) out.add(m[1]);
|
|
333
|
+
const factories = new RegExp(`\\bfunction\\s+(${ident})\\s*\\([^)]*\\)\\s*\\{[\\s\\S]{0,600}?\\breturn\\s+new\\s+${PROD_INTERLOCKING}\\b`, "g");
|
|
334
|
+
for (const m of text.matchAll(factories)) out.add(`${m[1]}()`);
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// [{ resolution, execution }] means an actual resolution value feeds actual train execution.
|
|
339
|
+
// `execution` is null for an unassigned execute result; route/runner proof may assert resolution,
|
|
340
|
+
// while trace and sequence proof require the named execution result.
|
|
341
|
+
export function hasResolvedProductionRunnerImports(text, file, croot) {
|
|
342
|
+
const imported = new Set();
|
|
343
|
+
const re = /^\s*import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/gm;
|
|
344
|
+
for (const match of text.matchAll(re)) {
|
|
345
|
+
if (!match[2].startsWith(".")) continue;
|
|
346
|
+
const names = match[1].split(",").map(name => name.trim().split(/\s+as\s+/)[0]);
|
|
347
|
+
const target = resolve(dirname(file), match[2]);
|
|
348
|
+
const candidates = [target, `${target}.ts`, `${target}.tsx`, `${target}.js`, `${target}.mjs`, join(target, "index.ts")];
|
|
349
|
+
const source = candidates.find(candidate => existsSync(candidate) && candidate.startsWith(resolve(croot) + sep) && !/\.(test|spec)\.[cm]?[jt]sx?$/.test(candidate));
|
|
350
|
+
if (!source) continue;
|
|
351
|
+
for (const name of names) if (name === PROD_INTERLOCKING || name === PROD_TRAIN) imported.add(name);
|
|
352
|
+
}
|
|
353
|
+
return imported.has(PROD_INTERLOCKING) && imported.has(PROD_TRAIN);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function productionExecutionProofs(text, file, croot) {
|
|
357
|
+
if (file && croot && !hasResolvedProductionRunnerImports(text, file, croot)) return [];
|
|
358
|
+
const receivers = productionReceivers(text);
|
|
359
|
+
const proofs = [];
|
|
360
|
+
const resolutions = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*([^\\n]{0,240}?)\\.resolveTrain\\s*\\(`, "g");
|
|
361
|
+
for (const m of text.matchAll(resolutions)) {
|
|
362
|
+
const [whole, resolution, receiver] = m;
|
|
363
|
+
const direct = new RegExp(`new\\s+${PROD_INTERLOCKING}\\b`).test(receiver);
|
|
364
|
+
const named = [...receivers].some((name) => receiver.trim().endsWith(name));
|
|
365
|
+
if (!direct && !named) continue;
|
|
366
|
+
const after = text.slice((m.index ?? 0) + whole.length);
|
|
367
|
+
const execute = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?new\\s+${PROD_TRAIN}\\s*\\(\\s*${escaped(resolution)}\\.(?:trainId|selectedTrainId)\\s*\\)\\s*\\.execute\\s*\\(`).exec(after);
|
|
368
|
+
const anonymous = new RegExp(`new\\s+${PROD_TRAIN}\\s*\\(\\s*${escaped(resolution)}\\.(?:trainId|selectedTrainId)\\s*\\)\\s*\\.execute\\s*\\(`).test(after);
|
|
369
|
+
if (execute || anonymous) proofs.push({ resolution, execution: execute?.[1] ?? null });
|
|
370
|
+
}
|
|
371
|
+
return proofs;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function assertsExpression(text, expression, expected = null) {
|
|
375
|
+
const pat = new RegExp(`\\bexpect\\s*\\(\\s*(?:await\\s+)?${expression}\\s*\\)\\s*\\.(?:to(?:Be|BeDefined|Equal|StrictEqual|Contain|ContainEqual|Match|BeTruthy)|not\\.to(?:Be|BeDefined|Equal|StrictEqual|Contain|ContainEqual|Match))\\b([\\s\\S]{0,220})`);
|
|
376
|
+
const m = pat.exec(text);
|
|
377
|
+
return Boolean(m && (!expected || tokenCovered(expected, m[0])));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function hasProductionExecutionProof(text, file, croot) {
|
|
381
|
+
return productionExecutionProofs(text, file, croot).some(({ resolution, execution }) => {
|
|
382
|
+
if (assertsExpression(text, `${escaped(resolution)}\\.(?:routeId|trainId|selectedTrainId)`) ||
|
|
383
|
+
(execution && assertsExpression(text, escaped(execution)))) return true;
|
|
384
|
+
if (!execution) return false;
|
|
385
|
+
const trace = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?${escaped(execution)}\\.trace\\b`).exec(text)?.[1];
|
|
386
|
+
return Boolean(trace && REQUIRED_TRACE_FIELDS.some(([field]) => assertsExpression(text, `${escaped(trace)}\\.${field}`)));
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function routeHasProductionProof(route, text, interlockingId = null, file, croot) {
|
|
391
|
+
const slug = interlockingId?.replace(/^interlocking:/, "");
|
|
392
|
+
return productionExecutionProofs(text, file, croot).some(({ resolution, execution }) => {
|
|
393
|
+
const assertedRoute = assertsExpression(text, `${escaped(resolution)}\\.routeId`, route.routeId);
|
|
394
|
+
const assertedTrain = assertsExpression(text, `${escaped(resolution)}\\.(?:trainId|selectedTrainId)`, route.trainId);
|
|
395
|
+
const scoped = !interlockingId || tokenCovered(interlockingId, text) || tokenCovered(slug, text);
|
|
396
|
+
return (assertedRoute && scoped) || assertedTrain || Boolean(scoped && execution && assertsExpression(text, escaped(execution)) && (tokenCovered(route.routeId, text) || tokenCovered(route.trainId, text)));
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function traceHasProductionProvenance(text, file, croot) {
|
|
401
|
+
return productionExecutionProofs(text, file, croot).some(({ execution }) => {
|
|
402
|
+
if (!execution) return false;
|
|
403
|
+
const trace = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?${escaped(execution)}\\.trace\\b`).exec(text)?.[1];
|
|
404
|
+
return Boolean(trace);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function assertedTraceFields(text, traceName = "trace") {
|
|
409
|
+
return REQUIRED_TRACE_FIELDS.filter(([field]) => !assertsExpression(text, `${escaped(traceName)}\\.${field}`)).map(([field]) => field);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// A smoke path has a real Station Master call and observes that call's returned value; separately
|
|
413
|
+
// requiring both production runner instances rules out imports used only as lexical decoration.
|
|
414
|
+
export function stationMasterExecutionProof(text, action) {
|
|
415
|
+
const call = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?${ident}\\.(?:handleAction|dispatch|executeAction)\\s*\\(\\s*["']${escaped(action)}["']`).exec(text);
|
|
416
|
+
if (!call) return false;
|
|
417
|
+
const result = call[1];
|
|
418
|
+
const instanceProof = new RegExp(`\\bexpect\\s*\\(\\s*${ident}\\.(?:interlockingRunner|trainRunner)\\s*\\)\\s*\\.toBeInstanceOf\\s*\\(\\s*(?:${PROD_INTERLOCKING}|${PROD_TRAIN})\\s*\\)`, "g");
|
|
419
|
+
const seen = new Set([...text.matchAll(instanceProof)].map((m) => m[0].includes(PROD_INTERLOCKING) ? PROD_INTERLOCKING : PROD_TRAIN));
|
|
420
|
+
return seen.has(PROD_INTERLOCKING) && seen.has(PROD_TRAIN) && assertsExpression(text, `${escaped(result)}(?:\\.[A-Za-z_$][\\w$]*)?`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function balancedBlock(text, open) {
|
|
424
|
+
let depth = 0, quote = null;
|
|
425
|
+
for (let i = open; i < text.length; i++) {
|
|
426
|
+
const c = text[i], next = text[i + 1];
|
|
427
|
+
if (quote) {
|
|
428
|
+
if (c === "\\") { i++; continue; }
|
|
429
|
+
if (c === quote) quote = null;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (c === "'" || c === '"' || c === "`") { quote = c; continue; }
|
|
433
|
+
if (c === "/" && next === "/") { i = text.indexOf("\n", i + 2); if (i < 0) return null; continue; }
|
|
434
|
+
if (c === "/" && next === "*") { const end = text.indexOf("*/", i + 2); if (end < 0) return null; i = end + 1; continue; }
|
|
435
|
+
if (c === "{") depth++;
|
|
436
|
+
if (c === "}" && --depth === 0) return text.slice(open + 1, i);
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function exportedActionBody(text, name) {
|
|
442
|
+
const n = escaped(name);
|
|
443
|
+
const headers = [
|
|
444
|
+
new RegExp(`\\bexport\\s+(?:async\\s+)?function\\s+${n}\\s*\\([^)]*\\)\\s*\\{`, "g"),
|
|
445
|
+
new RegExp(`\\bexport\\s+(?:const|let|var)\\s+${n}\\s*=\\s*(?:async\\s*)?\\([^)]*\\)\\s*=>\\s*\\{`, "g"),
|
|
446
|
+
];
|
|
447
|
+
for (const header of headers) {
|
|
448
|
+
const match = header.exec(text);
|
|
449
|
+
if (match) return balancedBlock(text, (match.index ?? 0) + match[0].lastIndexOf("{"));
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function exportedActionReturnsProductionExecution(stationText, action) {
|
|
455
|
+
const body = exportedActionBody(stationText, action);
|
|
456
|
+
if (!body) return false;
|
|
457
|
+
const receivers = productionReceivers(stationText);
|
|
458
|
+
const resolutions = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*([^\\n]{0,240}?)\\.resolveTrain\\s*\\(`, "g");
|
|
459
|
+
for (const m of body.matchAll(resolutions)) {
|
|
460
|
+
const [whole, resolution, receiver] = m;
|
|
461
|
+
const direct = new RegExp(`new\\s+${PROD_INTERLOCKING}\\b`).test(receiver);
|
|
462
|
+
const named = [...receivers].some((name) => receiver.trim().endsWith(name));
|
|
463
|
+
if (!direct && !named) continue;
|
|
464
|
+
const after = body.slice((m.index ?? 0) + whole.length);
|
|
465
|
+
const directReturn = new RegExp(`\\breturn\\s+(?:await\\s+)?new\\s+${PROD_TRAIN}\\s*\\(\\s*${escaped(resolution)}\\.(?:trainId|selectedTrainId)\\s*\\)\\s*\\.execute\\s*\\(`).test(after);
|
|
466
|
+
if (directReturn) return true;
|
|
467
|
+
const execution = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?new\\s+${PROD_TRAIN}\\s*\\(\\s*${escaped(resolution)}\\.(?:trainId|selectedTrainId)\\s*\\)\\s*\\.execute\\s*\\(`).exec(after)?.[1];
|
|
468
|
+
if (execution && new RegExp(`\\breturn\\s+(?:await\\s+)?${escaped(execution)}\\s*;?`).test(after)) return true;
|
|
469
|
+
}
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// HTTP/module entrypoints do not expose a StationMaster instance to the test. The asserted result
|
|
474
|
+
// must therefore come from the SAME exported action the test invokes, and that action's own return
|
|
475
|
+
// path must resolve then execute the selected train. Module-wide runner tokens are not evidence.
|
|
476
|
+
export function stationModuleExecutionProof(text, stationText, action) {
|
|
477
|
+
if (!stationText) return false;
|
|
478
|
+
const call = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*await\\s+(?:${ident}\\.)?(dispatch|handleAction|executeAction)\\s*\\(\\s*["']${escaped(action)}["']`).exec(text);
|
|
479
|
+
return Boolean(call && assertsExpression(text, `${escaped(call[1])}(?:\\.[A-Za-z_$][\\w$]*)?`) &&
|
|
480
|
+
exportedActionReturnsProductionExecution(stationText, call[2]));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Bun's native `expect` vocabulary provides a compact mutation witness: exact ordered equality
|
|
484
|
+
// kills reorder/removal mutants, objectContaining records a handoff edge, and at(-1) observes the
|
|
485
|
+
// final wagon. All three must be about the TrainRunner result, never a hand-built literal.
|
|
486
|
+
export function hasSequenceMutationProof(text, trainId, file, croot) {
|
|
487
|
+
return productionExecutionProofs(text, file, croot).some(({ resolution, execution }) => {
|
|
488
|
+
if (!execution || !assertsExpression(text, `${escaped(resolution)}\\.(?:trainId|selectedTrainId)`, trainId)) return false;
|
|
489
|
+
const result = escaped(execution);
|
|
490
|
+
const ordered = new RegExp(`expect\\s*\\(\\s*${result}\\.(?:steps|sequence|wagons)\\s*\\)\\s*\\.to(?:Equal|StrictEqual)\\s*\\(\\s*\\[`, "s").test(text);
|
|
491
|
+
const handoff = new RegExp(`expect\\s*\\(\\s*${result}\\.(?:steps|sequence|wagons)\\s*\\)\\s*\\.toContainEqual\\s*\\(\\s*expect\\.objectContaining\\s*\\(\\s*\\{[\\s\\S]{0,240}?(?:from|to)\\s*:`, "s").test(text);
|
|
492
|
+
const finalWagon = new RegExp(`expect\\s*\\(\\s*${result}\\.(?:steps|sequence|wagons)\\s*\\.at\\(\\s*-1\\s*\\)\\s*\\)\\s*\\.to(?:Be|Equal|StrictEqual|Match)`, "s").test(text);
|
|
493
|
+
return ordered && handoff && finalWagon;
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
318
497
|
export function writeReport(violations) {
|
|
319
498
|
const rp = process.env.ATDD_VIOLATIONS_REPORT;
|
|
320
499
|
if (!rp) {
|
|
@@ -22,6 +22,9 @@ import {
|
|
|
22
22
|
PROD_INTERLOCKING,
|
|
23
23
|
PROD_TRAIN,
|
|
24
24
|
FORBIDDEN_PATTERNS,
|
|
25
|
+
hasProductionExecutionProof,
|
|
26
|
+
stationMasterExecutionProof,
|
|
27
|
+
stationModuleExecutionProof,
|
|
25
28
|
rel,
|
|
26
29
|
mk,
|
|
27
30
|
writeReport,
|
|
@@ -67,16 +70,18 @@ for (const scanRoot of roots) {
|
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
const
|
|
71
|
-
|
|
73
|
+
const semantic = hasProductionExecutionProof(text, file, croot) ||
|
|
74
|
+
records.some((rec) => rec.actions.some((action) => stationMasterExecutionProof(text, action))) ||
|
|
75
|
+
(drivesStationMaster(text) && records.some((rec) => rec.actions.some((action) => stationModuleExecutionProof(text, station, action))));
|
|
76
|
+
if (!semantic) {
|
|
72
77
|
violations.push(
|
|
73
78
|
mk(
|
|
74
79
|
RULE,
|
|
75
80
|
r,
|
|
76
81
|
1,
|
|
77
82
|
0,
|
|
78
|
-
`interlocking test "${r}"
|
|
79
|
-
`
|
|
83
|
+
`interlocking test "${r}" has no asserted result flowing from production InterlockingRunner ` +
|
|
84
|
+
`resolution into TrainRunner execution; imports and route/train literals are discovery only`,
|
|
80
85
|
"",
|
|
81
86
|
),
|
|
82
87
|
);
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
interlockingFiles,
|
|
14
14
|
e2eFiles,
|
|
15
15
|
parseInterlocking,
|
|
16
|
-
|
|
16
|
+
routeHasProductionProof,
|
|
17
17
|
maskComments,
|
|
18
18
|
rel,
|
|
19
19
|
mk,
|
|
@@ -30,11 +30,11 @@ for (const scanRoot of roots) {
|
|
|
30
30
|
const records = interlockingFiles(croot)
|
|
31
31
|
.map((f) => ({ file: f, rec: parseInterlocking(readText(f)) }))
|
|
32
32
|
.filter((x) => x.rec);
|
|
33
|
-
const e2eTexts = e2eFiles(croot).map((
|
|
33
|
+
const e2eTexts = e2eFiles(croot).map((file) => ({ file, text: maskComments(readText(file)) }));
|
|
34
34
|
|
|
35
35
|
for (const { file, rec } of records) {
|
|
36
36
|
for (const route of rec.routes) {
|
|
37
|
-
if (
|
|
37
|
+
if (e2eTexts.some(({ file, text }) => routeHasProductionProof(route, text, rec.interlockingId, file, croot))) continue;
|
|
38
38
|
if (unbuiltWagons(croot, route.trainId).length) continue; // pending: a wagon on its train has no source yet
|
|
39
39
|
const cat =
|
|
40
40
|
route.category !== null
|
|
@@ -48,7 +48,7 @@ for (const scanRoot of roots) {
|
|
|
48
48
|
0,
|
|
49
49
|
`admissible route "${route.routeId}" of interlocking "${rec.interlockingId}" (${cat}, ` +
|
|
50
50
|
`resolves to train "${route.trainId}") has no e2e test exercising it; add an e2e/**/*.ts ` +
|
|
51
|
-
`test
|
|
51
|
+
`test whose asserted resolution selects this route/train and flows through InterlockingRunner -> TrainRunner`,
|
|
52
52
|
route.sourceLine.trim(),
|
|
53
53
|
),
|
|
54
54
|
);
|
|
@@ -20,10 +20,13 @@ import {
|
|
|
20
20
|
STATION_MASTER,
|
|
21
21
|
PROD_INTERLOCKING,
|
|
22
22
|
PROD_TRAIN,
|
|
23
|
+
stationMasterExecutionProof,
|
|
24
|
+
stationModuleExecutionProof,
|
|
23
25
|
rel,
|
|
24
26
|
mk,
|
|
25
27
|
writeReport,
|
|
26
28
|
} from "../_shared/interlocking.mjs";
|
|
29
|
+
import { join } from "node:path";
|
|
27
30
|
|
|
28
31
|
const RULE = "tester.bun.interlocking-smoke-coverage-for-station-master";
|
|
29
32
|
const roots = parseJsonEnv("ATDD_SCAN_ROOTS", []);
|
|
@@ -33,10 +36,11 @@ const violations = [];
|
|
|
33
36
|
// it under the smoke rules (no substituted collaborators, observable outcome). A local E2E that names the action beside
|
|
34
37
|
// the runners is not a smoke and no longer clears the rule (FWS #TzSCGS5ajYhP).
|
|
35
38
|
const SMOKE_PHASE = /^\s*\/\/\s*Phase:\s*SMOKE\b/m;
|
|
36
|
-
function actionSmokeCovered(action, e2eFiles) {
|
|
39
|
+
function actionSmokeCovered(action, e2eFiles, stationModule) {
|
|
37
40
|
return e2eFiles.some(
|
|
38
41
|
({ raw, text: t }) =>
|
|
39
|
-
SMOKE_PHASE.test(raw) && tokenCovered(action, t) && STATION_MASTER.test(t) && t.includes(PROD_INTERLOCKING) && t.includes(PROD_TRAIN)
|
|
42
|
+
SMOKE_PHASE.test(raw) && tokenCovered(action, t) && STATION_MASTER.test(t) && t.includes(PROD_INTERLOCKING) && t.includes(PROD_TRAIN) &&
|
|
43
|
+
(stationMasterExecutionProof(t, action) || stationModuleExecutionProof(t, stationModule, action)),
|
|
40
44
|
);
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -46,11 +50,14 @@ for (const scanRoot of roots) {
|
|
|
46
50
|
.map((f) => ({ file: f, rec: parseInterlocking(readText(f)) }))
|
|
47
51
|
.filter((x) => x.rec);
|
|
48
52
|
const e2eTexts = e2eFiles(croot).map((f) => { const raw = readText(f); return { raw, text: maskComments(raw) }; });
|
|
53
|
+
const stationModule = ["server.ts", join("src", "server.ts")]
|
|
54
|
+
.map((name) => readText(join(croot, name)))
|
|
55
|
+
.find((text) => text && text.includes(PROD_INTERLOCKING) && text.includes(PROD_TRAIN));
|
|
49
56
|
|
|
50
57
|
for (const { file, rec } of records) {
|
|
51
58
|
if (!rec.exposed) continue;
|
|
52
59
|
for (const action of rec.actions) {
|
|
53
|
-
if (actionSmokeCovered(action, e2eTexts)) continue;
|
|
60
|
+
if (actionSmokeCovered(action, e2eTexts, stationModule)) continue;
|
|
54
61
|
const [line, src] = lineOf(rec.rawText, new RegExp("^\\s*-\\s*['\"]?" + action + "['\"]?\\s*$"));
|
|
55
62
|
violations.push(
|
|
56
63
|
mk(
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
lineAt,
|
|
21
21
|
TRACE_OBJECT,
|
|
22
22
|
REQUIRED_TRACE_FIELDS,
|
|
23
|
+
assertedTraceFields,
|
|
24
|
+
traceHasProductionProvenance,
|
|
23
25
|
rel,
|
|
24
26
|
mk,
|
|
25
27
|
writeReport,
|
|
@@ -42,8 +44,9 @@ for (const scanRoot of roots) {
|
|
|
42
44
|
if (!isInterlockingTest(text, tokens)) continue;
|
|
43
45
|
const m = TRACE_OBJECT.exec(text);
|
|
44
46
|
if (!m) continue; // not a trace-binding test.
|
|
45
|
-
const missing =
|
|
46
|
-
|
|
47
|
+
const missing = assertedTraceFields(text);
|
|
48
|
+
const provenance = traceHasProductionProvenance(text, file, croot);
|
|
49
|
+
if (!missing.length && provenance) continue;
|
|
47
50
|
const line = lineOfIndex(text, m.index);
|
|
48
51
|
violations.push(
|
|
49
52
|
mk(
|
|
@@ -51,9 +54,8 @@ for (const scanRoot of roots) {
|
|
|
51
54
|
rel(file, croot),
|
|
52
55
|
line,
|
|
53
56
|
0,
|
|
54
|
-
`interlocking trace test "${rel(file, croot)}" does not bind
|
|
55
|
-
`trace field(s) ${missing.join(", ")}
|
|
56
|
-
`routeId/selectedTrainId/routeCategory/routeCategoryDigit/guardId/resolutionStrategy/resolutionReason)`,
|
|
57
|
+
`interlocking trace test "${rel(file, croot)}" does not bind a production TrainRunner result to the ` +
|
|
58
|
+
`declared route${missing.length ? `: missing asserted trace field(s) ${missing.join(", ")}` : ""}${!provenance ? "; trace is not derived from InterlockingRunner -> TrainRunner execution" : ""}`,
|
|
57
59
|
lineAt(raw, line),
|
|
58
60
|
),
|
|
59
61
|
);
|
|
@@ -16,22 +16,17 @@
|
|
|
16
16
|
// suite green. The runtime obeyed the new plan; no test looked.
|
|
17
17
|
import {
|
|
18
18
|
parseJsonEnv, readText, findConsumerRoots, interlockingFiles, e2eFiles,
|
|
19
|
-
parseInterlocking,
|
|
19
|
+
parseInterlocking, rel, mk, PLAN_ROOT, unbuiltWagons, hasSequenceMutationProof,
|
|
20
20
|
} from "../_shared/interlocking.mjs";
|
|
21
21
|
import { writeFileSync, readdirSync, statSync } from "node:fs";
|
|
22
22
|
import { join } from "node:path";
|
|
23
23
|
|
|
24
24
|
export const RULE_SEQUENCE = "tester.bun.interlocking-train-sequence-is-exercised";
|
|
25
25
|
|
|
26
|
-
// A line that ASSERTS AN ORDER: names the executed sequence AND compares it.
|
|
27
|
-
// Merely touching the word is not asserting the order — `expect(trace.steps)` alone
|
|
28
|
-
// says nothing about what ran, and counting it would repeat the over-broad trigger
|
|
29
|
-
// that has now been fixed three times in this family.
|
|
30
|
-
const SEQUENCE_ASSERTION =
|
|
31
|
-
/^[^\n]*\b(?:expect|assert)\b[^\n]*\b(?:steps|sequence|wagons)\b[^\n]*(?:==|toEqual|toStrictEqual|deepEqual)/m;
|
|
32
|
-
|
|
33
26
|
export function assertsASequence(text) {
|
|
34
|
-
|
|
27
|
+
// Retained as a narrow API for consumers of this module. The train id is intentionally
|
|
28
|
+
// unavailable here, so it cannot certify coverage; `trainSequenceCovered` below does.
|
|
29
|
+
return /\bexpect\s*\(\s*[A-Za-z_$][\w$]*\.(?:steps|sequence|wagons)\s*\)\s*\.to(?:Equal|StrictEqual)\s*\(/.test(text);
|
|
35
30
|
}
|
|
36
31
|
|
|
37
32
|
export function trainsReachableFromRoutes(records) {
|
|
@@ -45,8 +40,8 @@ export function trainsReachableFromRoutes(records) {
|
|
|
45
40
|
return out;
|
|
46
41
|
}
|
|
47
42
|
|
|
48
|
-
export function trainSequenceCovered(trainId,
|
|
49
|
-
return
|
|
43
|
+
export function trainSequenceCovered(trainId, files, croot) {
|
|
44
|
+
return files.some(({ file, text }) => hasSequenceMutationProof(text, trainId, file, croot));
|
|
50
45
|
}
|
|
51
46
|
|
|
52
47
|
// Does the plan actually DECLARE a wagon sequence for this train?
|
|
@@ -85,13 +80,13 @@ export function scanExecution(scanRoot) {
|
|
|
85
80
|
const records = interlockingFiles(croot).map((f) => parseInterlocking(readText(f))).filter(Boolean);
|
|
86
81
|
if (!records.length) continue;
|
|
87
82
|
const files = e2eFiles(croot).map((f) => ({ file: f, text: readText(f) }));
|
|
88
|
-
|
|
83
|
+
|
|
89
84
|
for (const trainId of [...trainsReachableFromRoutes(records)].sort()) {
|
|
90
85
|
// The finding points at the train's own document, where the sequence a test must assert is declared (FWS #fqUGvWHn25mq).
|
|
91
86
|
const declared = declaresASequence(croot, trainId);
|
|
92
87
|
if (!declared) continue; // nothing declared to exercise
|
|
93
88
|
if (unbuiltWagons(croot, trainId).length) continue; // pending: a wagon on it has no source yet
|
|
94
|
-
if (trainSequenceCovered(trainId,
|
|
89
|
+
if (trainSequenceCovered(trainId, files, croot)) continue;
|
|
95
90
|
violations.push(
|
|
96
91
|
mk(RULE_SEQUENCE, rel(declared, croot), 1, 0,
|
|
97
92
|
`declared train "${trainId}" is selected by a route but no test asserts the wagon ` +
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class InterlockingRunner { resolveTrain() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class TrainRunner { execute() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class InterlockingRunner { resolveTrain() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class TrainRunner { execute() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class InterlockingRunner { resolveTrain() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class TrainRunner { execute() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class InterlockingRunner { resolveTrain() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class TrainRunner { execute() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class InterlockingRunner { resolveTrain() { return {}; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class TrainRunner { execute() { return {}; } }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Phase: SMOKE
|
|
2
|
+
import { expect, test } from "bun:test";
|
|
3
|
+
import { dispatch } from "../../src/server";
|
|
4
|
+
import { InterlockingRunner } from "../../src/trains/interlocking";
|
|
5
|
+
import { TrainRunner } from "../../src/trains/runner";
|
|
6
|
+
|
|
7
|
+
test("inert runner helper does not prove functional Station Master dispatch", async () => {
|
|
8
|
+
const result = await dispatch("resolve_match", { allPlayersVoted: true });
|
|
9
|
+
expect(result.selectedTrainId).toBe("train:match:match-resolution-standard");
|
|
10
|
+
expect(InterlockingRunner).toBeDefined();
|
|
11
|
+
expect(TrainRunner).toBeDefined();
|
|
12
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { InterlockingRunner } from "./trains/interlocking";
|
|
2
|
+
import { TrainRunner } from "./trains/runner";
|
|
3
|
+
|
|
4
|
+
const runner = new InterlockingRunner("plan/_trains/_interlockings/match-resolution.yaml");
|
|
5
|
+
|
|
6
|
+
async function inertProductionHelper(action: string, inputs: object) {
|
|
7
|
+
const resolution = runner.resolveTrain(action, inputs);
|
|
8
|
+
return await new TrainRunner(resolution.trainId).execute({});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function dispatch(_action: string, _inputs: object) {
|
|
12
|
+
return { selectedTrainId: "train:match:match-resolution-standard" };
|
|
13
|
+
}
|