test(06-05): failing capability-guard suite GS13-GS14 — haptic cue routing + layered close protection under version gates (RED)
This commit is contained in:
@@ -50,6 +50,8 @@ const {
|
||||
buildTripEndCard,
|
||||
MANEUVER_LIST_ID,
|
||||
} = 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 ————— */
|
||||
|
||||
@@ -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-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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user