test(06-05): failing capability-guard suite GS13-GS14 — haptic cue routing + layered close protection under version gates (RED)

This commit is contained in:
araharah-executor
2026-10-24 14:19:44 +08:00
parent 79836f0a31
commit 47b1524472
+98
View File
@@ -50,6 +50,8 @@ const {
buildTripEndCard, buildTripEndCard,
MANEUVER_LIST_ID, MANEUVER_LIST_ID,
} = await import("../src/guidance/banner.ts"); } = await import("../src/guidance/banner.ts");
const { fireHaptic } = await import("../src/guidance/haptics.ts");
const { engageCloseProtection, releaseCloseProtection } = await import("../src/guidance/close-protection.ts");
/* ————— shared helpers ————— */ /* ————— shared helpers ————— */
@@ -625,3 +627,99 @@ test("GS12 banner.ts places .btn-primary exactly once — the confirm card's YES
assert.equal((src.match(/btn-primary/g) ?? []).length, 1, "only the confirm card builds a primary CTA"); assert.equal((src.match(/btn-primary/g) ?? []).length, 1, "only the confirm card builds a primary CTA");
assert.equal((src.match(/btn-secondary/g) ?? []).length, 1, "every other action rides the shared secondary-button helper"); assert.equal((src.match(/btn-secondary/g) ?? []).length, 1, "every other action rides the shared secondary-button helper");
}); });
/* ————— GS13/GS14 — the capability guards (D-08 haptics, PX-03 close
protection): recording fakes prove every call is gated BEFORE it happens
and degrades to an honest no-op (T-06-06) ————— */
/** Bot-API semver compare ("7.7" vs "8.0"). */
function atLeast(actual, wanted) {
const [aMaj, aMin] = actual.split(".").map(Number);
const [wMaj, wMin] = wanted.split(".").map(Number);
return aMaj > wMaj || (aMaj === wMaj && aMin >= wMin);
}
/** A recording TelegramWebAppLike fake — every guard-relevant call lands in
* `rec`; members can be deleted to prove the no-op paths. */
function fakeWa({ version = "8.0", haptics = true, versionGate = true } = {}) {
const rec = { haptic: [], close: [], versionChecks: [] };
const wa = {
initData: "",
HapticFeedback: haptics
? {
selectionChanged() { rec.haptic.push("selection"); },
impactOccurred(style) { rec.haptic.push(`impact:${style}`); },
notificationOccurred(type) { rec.haptic.push(`notify:${type}`); },
}
: undefined,
disableVerticalSwipes() { rec.close.push("disableVerticalSwipes"); },
enableVerticalSwipes() { rec.close.push("enableVerticalSwipes"); },
enableClosingConfirmation() { rec.close.push("enableClosingConfirmation"); },
disableClosingConfirmation() { rec.close.push("disableClosingConfirmation"); },
};
if (versionGate) {
wa.isVersionAtLeast = (v) => {
rec.versionChecks.push(v);
return atLeast(version, v);
};
}
return { wa, rec };
}
test("GS13 fireHaptic routes the three cues — selectionChanged / impactOccurred('medium') / notificationOccurred('success')", () => {
const { wa, rec } = fakeWa();
fireHaptic(wa, "approach");
fireHaptic(wa, "maneuver");
fireHaptic(wa, "arrive");
assert.deepEqual(rec.haptic, ["selection", "impact:medium", "notify:success"], "the UI-SPEC haptics table, verbatim");
});
test("GS13b fireHaptic degrades silently — no HapticFeedback member and a null wa are no-ops, never throws (D-08)", () => {
const bare = fakeWa({ haptics: false });
fireHaptic(bare.wa, "approach");
fireHaptic(bare.wa, "maneuver");
fireHaptic(bare.wa, "arrive");
assert.deepEqual(bare.rec.haptic, [], "nothing recorded, nothing thrown");
fireHaptic(null, "arrive"); // plain browser — must not throw
// the no-haptic law is documented in the module header (D-08 + no blame)
const src = readFileSync(path.join(GUIDANCE_DIR, "haptics.ts"), "utf8");
assert.ok(src.includes("D-08"), "the header cites D-08");
assert.ok(src.toLowerCase().includes("reroute"), "the header carries the no-haptic-on-reroute law");
});
test("GS14 engageCloseProtection — body-swipe off at 7.7+, native confirm at 6.2+, each BEFORE its call, layered in order", () => {
const modern = fakeWa({ version: "8.0" });
engageCloseProtection(modern.wa);
assert.deepEqual(modern.rec.close, ["disableVerticalSwipes", "enableClosingConfirmation"], "both layers engage in order");
assert.deepEqual(modern.rec.versionChecks, ["7.7", "6.2"], "each gate checked before its own call");
const mid = fakeWa({ version: "7.0" });
engageCloseProtection(mid.wa);
assert.deepEqual(mid.rec.close, ["enableClosingConfirmation"], "7.0: the 7.7 layer stays a no-op");
const old = fakeWa({ version: "6.1" });
engageCloseProtection(old.wa);
assert.deepEqual(old.rec.close, [], "6.1: nothing engages — silent no-op");
const gateless = fakeWa({ versionGate: false });
engageCloseProtection(gateless.wa);
assert.deepEqual(gateless.rec.close, [], "no version gate → no calls, no throw (Pitfall 5)");
engageCloseProtection(null); // must not throw
});
test("GS14b releaseCloseProtection inverts both layers under the same gates", () => {
const modern = fakeWa({ version: "8.0" });
releaseCloseProtection(modern.wa);
assert.deepEqual(modern.rec.close, ["enableVerticalSwipes", "disableClosingConfirmation"], "trip end: both layers release");
const mid = fakeWa({ version: "6.5" });
releaseCloseProtection(mid.wa);
assert.deepEqual(mid.rec.close, ["disableClosingConfirmation"], "6.5: only the 6.2 layer releases");
const old = fakeWa({ version: "6.1" });
releaseCloseProtection(old.wa);
assert.deepEqual(old.rec.close, []);
releaseCloseProtection(null); // must not throw
});
test("GS14c the version literals are pinned — isVersionAtLeast(\"7.7\") and isVersionAtLeast(\"6.2\") in close-protection.ts", () => {
const src = readFileSync(path.join(GUIDANCE_DIR, "close-protection.ts"), "utf8");
assert.ok(src.includes('isVersionAtLeast("7.7")'), "the 7.7 body-swipe gate");
assert.ok(src.includes('isVersionAtLeast("6.2")'), "the 6.2 native-confirm gate");
assert.ok(src.includes("?.") || src.includes("?."), "optional-member chains throughout");
});