Files
arah2/bot/test/guidance-envelope.test.mjs
T
araharah-executor 0b8671e9ca test(05-03): add failing guidance-envelope types-only gates (GE1-GE7) (RED)
- GE1 TTL constant === 7200 (~2h, D-06)
- GE2 field freeze: envelope 4 fields / snapshot 6 fields (SessionClaim law)
- GE3 inline snapshot: typed inline, zero snapshotRef tokens in bot/src (D-07)
- GE4 types-only: zero behavior tokens comment-stripped (T-05-08)
- GE5-GE7 negative-space guards: one kv namespace, no Env field, no consumer
  (green by construction — redden only on drift; 05-01 C4/C5/C6 precedent)
2026-10-24 05:08:52 +08:00

189 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/*
* guidance-envelope.test.mjs — GE1–GE7 types-only gates for the frozen
* guidance_sessions contract (D-06/D-07).
* ---------------------------------------------------------------------
* The envelope is a TYPES-ONLY frozen contract this phase (D-06): code
* types + the ~2h TTL constant + the key-scheme doc. NO KV namespace, NO
* service, NO runtime writes — Phase 6 builds the backgrounded turn-card
* service on the frozen shape. These gates pin that boundary MECHANICALLY:
*
* GE1 TTL constant — GUIDANCE_SESSION_TTL_SECONDS === 7200 (the
* ~2h law; every future write carries it)
* GE2 field freeze — GuidanceSessionEnvelope has EXACTLY
* [id, chat_id, route_snapshot, created_at] and
* GuidanceRouteSnapshot has EXACTLY
* [destinationId, destinationName, distanceM,
* durationS, geometry, startedAtSec] (the
* SessionClaim "a 5th property means drift" law)
* GE3 inline snapshot — route_snapshot is TYPED INLINE
* (GuidanceRouteSnapshot, not a reference) and
* the tokens snapshotRef / snapshot_ref appear
* NOWHERE in bot/src (D-07: one key, one TTL)
* GE4 types-only — comments stripped, the module contains zero
* behavior tokens (no KV binding type, no
* put/get/delete, no function export, no import)
* GE5 no new namespace — wrangler.jsonc carries EXACTLY ONE kv
* namespace (chat_sessions)
* GE6 no Env field — config.ts gains no guidance field this phase
* GE7 no premature — no bot/src module imports the contract; only
* consumer the test does (Phase 6 builds the consumer)
*
* RED-shape note: GE1–GE4 fail while the module is absent. GE5–GE7 are
* negative-space boundary guards over files that must NOT change — they
* are green by construction and redden only on drift (the 05-01 C4/C5/C6
* zero-laws precedent).
*
* Phase 6 note: when the turn-card service legitimately adds the kv
* namespace binding, the Env field, and the consumer, updating GE4–GE7 is
* THAT plan's responsibility — these gates redden until then (T-05-08:
* the contract cannot acquire a runtime shadow this phase).
*/
import { strict as assert } from "node:assert";
import { readdirSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { test } from "./harness.mjs";
const here = path.dirname(fileURLToPath(import.meta.url)); // bot/test
const srcDir = path.join(here, "..", "src"); // bot/src
const MODULE = path.join(srcDir, "kv", "guidance-sessions.ts");
const CONFIG = path.join(srcDir, "config.ts");
const WRANGLER = path.join(here, "..", "wrangler.jsonc");
/* Comments stripped first — banned-token scans read CODE, not prose
* (routing/test/contract.test.mjs C4 / web no-storage pattern). */
function stripComments(src) {
return src
.replace(/\/\*[\s\S]*?\*\//g, " ") // block comments
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1"); // line comments (benign over-stripping)
}
function tsFilesUnder(dir) {
const out = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, entry.name);
if (entry.isDirectory()) out.push(...tsFilesUnder(p));
else if (entry.name.endsWith(".ts")) out.push(p);
}
return out;
}
/* ————— GE1 — the ~2h TTL constant exists as a documented value ————— */
test("GE1 guidance TTL constant — GUIDANCE_SESSION_TTL_SECONDS === 7200 (~2h, D-06)", async () => {
const mod = await import("../src/kv/guidance-sessions.ts");
assert.equal(
mod.GUIDANCE_SESSION_TTL_SECONDS,
7200,
"the ~2h TTL must exist as the documented constant awaiting its Phase 6 consumer",
);
});
/* ————— GE2 — the field freeze (SessionClaim law) ————— */
/** Field names of `interface <name> { ... }` — lines shaped `^\s{2}\w+:`. */
function interfaceFields(source, name) {
const decl = source.indexOf(`interface ${name} {`);
assert.notEqual(decl, -1, `interface ${name} not found in the contract module`);
const bodyStart = source.indexOf("{", decl) + 1;
const bodyEnd = source.indexOf("\n}", bodyStart);
assert.notEqual(bodyEnd, -1, `interface ${name} body never closes`);
return source
.slice(bodyStart, bodyEnd)
.split("\n")
.map((line) => line.match(/^\s{2}(\w+):/))
.filter(Boolean)
.map((m) => m[1]);
}
test("GE2 field freeze — envelope has EXACTLY [id, chat_id, route_snapshot, created_at]; snapshot EXACTLY its six", () => {
const source = readFileSync(MODULE, "utf8");
assert.deepEqual(
interfaceFields(source, "GuidanceSessionEnvelope"),
["id", "chat_id", "route_snapshot", "created_at"],
"the envelope grew/shrank a field — a 5th property means drift (the SessionClaim law)",
);
assert.deepEqual(
interfaceFields(source, "GuidanceRouteSnapshot"),
["destinationId", "destinationName", "distanceM", "durationS", "geometry", "startedAtSec"],
"the route snapshot surface drifted (GUID-05/06 scoped fields)",
);
});
/* ————— GE3 — the snapshot is INLINE (D-07: one key, one TTL) ————— */
test("GE3 inline snapshot — route_snapshot is typed inline; zero snapshot-ref tokens in bot/src", () => {
const source = readFileSync(MODULE, "utf8");
assert.ok(
source.includes("route_snapshot: GuidanceRouteSnapshot"),
"route_snapshot must be typed INLINE (GuidanceRouteSnapshot), never a string/number reference",
);
for (const file of tsFilesUnder(srcDir)) {
assert.doesNotMatch(
readFileSync(file, "utf8"),
/snapshotRef|snapshot_ref/,
`${file} — snapshot-by-reference token found (D-07: one key, one TTL, nothing reconstructable after expiry)`,
);
}
});
/* ————— GE4 — the module ships NO behavior (T-05-08) ————— */
test("GE4 types-only — comments stripped, zero behavior tokens in guidance-sessions.ts", () => {
const code = stripComments(readFileSync(MODULE, "utf8"));
const banned = [
"KvNamespace",
".put(",
".get(",
".delete(",
"export function",
"export async function",
"import ",
];
for (const token of banned) {
assert.ok(
!code.includes(token),
`types-only violation: behavior token "${token}" in guidance-sessions.ts (T-05-08: premature persistence path)`,
);
}
});
/* ————— GE5 — exactly one kv namespace (chat_sessions) ————— */
test("GE5 no new namespace — wrangler.jsonc carries EXACTLY ONE kv namespace: chat_sessions", () => {
const wrangler = JSON.parse(stripComments(readFileSync(WRANGLER, "utf8")));
const namespaces = wrangler.kv_namespaces;
assert.equal(Array.isArray(namespaces), true, "kv_namespaces must remain an array");
assert.equal(
namespaces.length,
1,
"a second kv namespace appeared — the guidance envelope is TYPES-ONLY this phase (D-06)",
);
assert.equal(namespaces[0].binding, "chat_sessions", "the single namespace must remain chat_sessions");
});
/* ————— GE6 — the Env contract gains nothing this phase ————— */
test("GE6 no Env field — config.ts contains no guidance token", () => {
const config = readFileSync(CONFIG, "utf8");
assert.ok(
!config.includes("guidance"),
"config.ts gained a guidance field (D-06: Env gains NOTHING this phase — Phase 6's plan owns that change)",
);
});
/* ————— GE7 — no premature consumer ————— */
test("GE7 no premature consumer — zero bot/src imports of guidance-sessions (only the test imports it)", () => {
const IMPORT_RE = /\bimport\b[^;\n"']*["'][^"']*guidance-sessions[^"']*["']/;
for (const file of tsFilesUnder(srcDir)) {
const code = stripComments(readFileSync(file, "utf8"));
assert.doesNotMatch(
code,
IMPORT_RE,
`${file} imports the guidance contract (D-06: no consumer until Phase 6 builds the service)`,
);
}
});