Files
arah2/web/test/failure-state-matrix.test.mjs
araharah-executor b577bc4687 test(07-02): FM1-FM7 failure-state matrix — named vectors pin the designed surfaces (SC2)
- FAILURE_VECTORS as data: 7 classes (empty/error/no-geometry/off-road/
  location-timeout/old-client/route-timeout), each with trigger + expected surface
- FM2 drives the REAL buildRoutePendingCard (text, no undefined, CTA wired)
- FM3 rejecting + never-settling sources both stay off the draw path (200ms guard)
- FM4 RED: a <2-point geometry currently reaches the draw path — the pending
  card must replace the half-drawn line (T-07-12)
- FM5 pure-reducer excursion: one reroute-requested, storm guard holds, CALM slot
- FM6 old-client chrome builds with zero capability calls (07-01 owns fullscreen)
- FM7 location chain lands designed denied/awaiting-pin states
2026-10-25 04:00:41 +08:00

563 lines
24 KiB
JavaScript
Raw Permalink 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
/*
* failure-state-matrix.test.mjs — FM1..FM7, the SC2 failure-state matrix
* (phase 7, 07-02; T-07-05/T-07-12).
* --------------------------------------------------------------------------
* Every failure class renders a DESIGNED surface, never a blank screen.
* The vectors are DATA (07-RESEARCH Pattern 3): each class is a named
* entry in FAILURE_VECTORS with its trigger and expected surface token,
* and every FM case asserts one vector's designed outcome. The behaviours
* already exist (phases 3–6) — this suite PINS them; the one gap it found
* (a <2-point geometry reaching the consult draw path) is closed in
* route.ts, the pending card instead of a half-drawn line.
*
* The FM prefix follows the HS/GS/RS suite convention. Styles follow the
* house law: behavioral cases drive the REAL modules through recording
* fakes + the minimal document fake (installed per case, restored in
* finally — the homescreen.test.mjs law); the named-vector table is the
* first-class data. No real network, no real DOM, no timer > 200 ms.
*
* Cases:
* FM1 FAILURE_VECTORS — exactly 7 named entries, ids pinned in order
* FM2 empty route → the REAL pending card renders designed text
* FM3 route error AND route timeout → the same pending surface;
* nothing drawn, state never "route", 200 ms timeout guard
* FM4 no-geometry: a polyline6 decoding to < 2 points is refused
* BEFORE the draw path — pending card, never a half-drawn line
* FM5 off-road: exactly ONE reroute-requested, storm guard holds the
* immediate second excursion, REROUTING renders the CALM slot
* FM6 old client (Bot API < 8.0, methods absent): the guidance chrome
* still builds, zero capability calls attempted (chrome-level
* vector — fullscreen.ts's own suite is 07-01's)
* FM7 location timeout: the chain lands "denied" (the designed
* failure kinds exist beside "awaiting-pin"), never a blank
*/
import { strict as assert } from "node:assert";
import { register } from "node:module";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { test } from "./harness.mjs";
/* This suite sorts BEFORE guidance-surface in the runner — it registers the
* ?raw glyph hook itself before the banner/glyph imports resolve. */
register(new URL("./glyph-raw-loader.mjs", import.meta.url));
const HERE = path.dirname(fileURLToPath(new URL(import.meta.url)));
const routing = await import("@araharah/routing/src/index.ts");
const { decodePolyline6, encodePolyline6, deriveCumulativeDistanceM } = routing;
const { strings } = await import("../src/i18n/strings.ts");
const { buildGuidanceBanner, buildGuidanceCard } = await import("../src/guidance/banner.ts");
const { buildRoutePendingCard } = await import("../src/route/route-card.ts");
const {
createGuidanceSession,
stepGuidance,
guidanceView,
OFF_ROUTE_CONSECUTIVE_FIXES,
OFF_ROUTE_GRACE_MS,
STORM_GUARD_MS,
} = await import("../src/guidance/guidance.ts");
const { installRouteLayers } = await import("../src/route/route-layers.ts");
const { createRouteController } = await import("../src/route/route.ts");
const { acquireFix } = await import("../src/location/chain.ts");
/* ————— the matrix as DATA (RESEARCH Pattern 3) ————— */
const FAILURE_VECTORS = [
{ id: "empty-route", trigger: "route options=[]", expect: "pending-card" },
{ id: "route-error", trigger: "route source throws", expect: "pending-card" },
{ id: "no-geometry", trigger: "polyline decodes to <2 points", expect: "pending-card" },
{ id: "off-road", trigger: "3 fixes beyond the accuracy-aware threshold", expect: "rerouting-or-no-reroute" },
{ id: "location-timeout", trigger: "LocationManager returns null after timeout", expect: "denied-or-awaiting-card" },
{ id: "old-client", trigger: "WebApp version < 8.0 / methods absent", expect: "guidance-usable-without-fullscreen" },
{ id: "route-timeout", trigger: "route source never resolves within the budget", expect: "pending-card" },
];
/* ————— shared helpers ————— */
/** Minimal element fake — exactly the surface the builders compose with
* (the guidance-surface pattern, copied). */
function makeEl(tag) {
const el = {
tagName: tag.toUpperCase(),
children: [],
attributes: {},
dataset: {},
style: {},
className: "",
id: "",
type: "",
textContent: "",
handlers: {},
appendChild(child) {
el.children.push(child);
return child;
},
append(...cs) {
for (const c of cs) el.appendChild(c);
},
replaceChildren(...cs) {
el.children = [...cs];
},
setAttribute(name, value) {
el.attributes[name] = String(value);
},
getAttribute(name) {
return el.attributes[name];
},
addEventListener(type, cb) {
(el.handlers[type] ??= []).push(cb);
},
click() {
for (const cb of el.handlers.click ?? []) cb();
},
};
if (tag === "template") {
let content = { firstElementChild: null };
Object.defineProperty(el, "innerHTML", {
set(html) {
el.rawHTML = html;
const m = /<svg\b([^>]*)>/.exec(html);
const attrs = {};
if (m) for (const a of m[1].matchAll(/([\w-]+)="([^"]*)"/g)) attrs[a[1]] = a[2];
const svg = makeEl("svg");
svg.attributes = attrs;
content = { firstElementChild: svg };
},
get() {
return el.rawHTML ?? "";
},
});
Object.defineProperty(el, "content", {
get() {
return content;
},
});
}
return el;
}
const realDocument = globalThis.document;
/** Install the document fake for the duration of `body` (sync or async),
* restored in finally — the homescreen.test.mjs law. */
function withDocument(body) {
globalThis.document = { createElement: (tag) => makeEl(tag) };
try {
return body();
} finally {
globalThis.document = realDocument;
}
}
/** Depth-first search by className (space-separated token match). */
function findByClass(root, cls) {
if (root === null || root === undefined) return null;
if (typeof root.className === "string" && root.className.split(/\s+/).includes(cls)) return root;
for (const child of root.children ?? []) {
const hit = findByClass(child, cls);
if (hit !== null) return hit;
}
return null;
}
/** Collected leaf text of a fake element tree (parents never accumulate). */
function textOf(el) {
if (el === null || el === undefined) return "";
let out = typeof el.textContent === "string" ? el.textContent : "";
for (const child of el.children ?? []) out += textOf(child);
return out;
}
/** Walk every element of the fake tree. */
function walkAll(root, acc = []) {
acc.push(root);
for (const child of root.children ?? []) walkAll(child, acc);
return acc;
}
/** Capture console.log around an async body (restored even on failure). */
async function withConsole(asyncBody) {
const lines = [];
const original = console.log;
console.log = (...args) => lines.push(args.map(String).join(" "));
try {
return { result: await asyncBody(), lines };
} finally {
console.log = original;
}
}
/** A recording MapLike fake (the route-surface shape): setRouteDrawn's
* setData swaps and fitBounds land here — the nothing-drawn assertions. */
function fakeMap() {
const sources = new Map();
const handlers = {};
const recorded = { addSource: [], addLayer: [], setData: [], fitBounds: [], flyTo: [] };
return {
recorded,
addSource(id, data) {
recorded.addSource.push({ id, data });
sources.set(id, {
setData(d) {
recorded.setData.push({ id, data: d });
},
});
},
addLayer(layer) {
recorded.addLayer.push(layer);
},
getSource(id) {
return sources.get(id);
},
fitBounds(bounds, options) {
recorded.fitBounds.push({ bounds, options });
},
flyTo(options) {
recorded.flyTo.push(options);
},
getBearing() {
return 42;
},
on(event, cb) {
(handlers[event] ??= []).push(cb);
},
emit(event, e) {
for (const cb of handlers[event] ?? []) cb(e);
},
};
}
/** Full fake dep set around the REAL controller (the RS4 fakeDeps shape,
* trimmed to the consult-path surfaces the failure vectors touch). */
function fakeDeps({ result } = {}) {
const map = fakeMap();
installRouteLayers(map); // production installs the sources at map load — before any consult
const recorded = { shown: [], states: [], cards: [] };
const deps = {
map,
source: { name: "fixture", route: () => Promise.resolve(result ?? null) },
locale: "en",
buildRouteCard: (locale, destinationName, options, selectedIndex) => {
recorded.cards.push({ kind: "route", destinationName, options, selectedIndex });
return { className: "card", textContent: destinationName };
},
buildPendingCard: (locale) => {
recorded.cards.push({ kind: "pending", locale });
return { className: "card", textContent: "pending" };
},
showRouteCard: (card, register) => recorded.shown.push({ card, register }),
showDestinationCard: () => {},
follow: { follow() {} },
getLatestFix: () => null,
attachEndpoints: () => {},
removeEndpoints: () => {},
onStateChange: (state) => recorded.states.push(state),
__recorded: recorded,
__map: map,
};
return deps;
}
const DEST = { id: "suria-klcc", name: "Suria KLCC", aliases: [], lat: 3.157, lng: 101.712, category: "mall" };
const FIX = { lat: 3.15, lng: 101.7, accuracyM: 8, headingDeg: null, ts: 1_760_000_000_000, source: "webview" };
/* ————— FM1 — the matrix is data, exactly seven named vectors ————— */
test("FM1 FAILURE_VECTORS — exactly 7 entries, ids pinned in order (the SC2 classes)", () => {
assert.equal(FAILURE_VECTORS.length, 7, `the matrix holds 7 classes (got ${FAILURE_VECTORS.length})`);
assert.deepEqual(
FAILURE_VECTORS.map((v) => v.id),
["empty-route", "route-error", "no-geometry", "off-road", "location-timeout", "old-client", "route-timeout"],
"the named ids, in order",
);
for (const v of FAILURE_VECTORS) {
assert.ok(typeof v.trigger === "string" && v.trigger.length > 0, `${v.id}: trigger stated`);
assert.ok(typeof v.expect === "string" && v.expect.length > 0, `${v.id}: expected surface token stated`);
assert.ok(!v.expect.includes("blank") && !v.expect.includes("crash"), `${v.id}: never a blank/crash surface`);
}
});
/* ————— FM2 — empty route: the REAL pending card renders designed text ————— */
test("FM2 empty route — the real buildRoutePendingCard renders designed text, no blank, no undefined", () => {
withDocument(() => {
let searchedAgain = 0;
const card = buildRoutePendingCard("en", { onSearchAgain: () => { searchedAgain += 1; } });
assert.equal(card.className.includes("card"), true, "the same card anatomy as the route card");
const text = textOf(card);
assert.ok(text.length > 0, "the card carries text — never a blank surface");
assert.ok(!text.includes("undefined"), "no bare 'undefined' leaked into the surface");
assert.ok(text.includes(strings.en.routePendingHeadline), "the designed headline renders");
assert.ok(text.includes(strings.en.routePendingBody), "the designed body renders");
assert.ok(text.includes(strings.en.ctaSearchAgain), "the SEARCH AGAIN affordance renders");
for (const el of walkAll(card)) {
assert.ok(typeof el.className === "string" && el.className.length > 0, "no element with an empty class");
for (const value of Object.values(el.dataset)) {
assert.ok(value !== undefined, "no undefined dataset state on any element");
}
}
const button = findByClass(card, "btn-secondary");
assert.ok(button, "the secondary CTA exists");
button.click();
assert.equal(searchedAgain, 1, "SEARCH AGAIN fires the walk-back action");
});
});
/* ————— FM3 — route error AND route timeout land on the pending surface ————— */
test("FM3 route error — a REJECTING source renders the pending card, nothing drawn (T-05-04)", async () => {
const deps = fakeDeps();
deps.source = { name: "fixture", route: () => Promise.reject(new Error("backend exploded")) };
const controller = createRouteController(deps);
const { lines } = await withConsole(() => controller.consult(FIX, DEST));
assert.equal(controller.state, "pending", "an exception lands on the pending surface — never a blank");
assert.equal(deps.__recorded.states.at(-1), "pending");
assert.equal(deps.__recorded.cards.at(-1).kind, "pending", "the designed pending card");
assert.equal(deps.__recorded.shown.at(-1).register, "center", "the pending card is a center-register surface");
assert.equal(deps.__map.recorded.setData.length, 0, "NOTHING drawn on the route sources");
assert.equal(deps.__map.recorded.fitBounds.length, 0, "no camera move on failure");
assert.ok(lines.includes("ARAHARAH-ROUTE state=pending"), "the state token is logged");
});
test("FM3 route timeout — a NEVER-SETTLING source draws nothing; state stays idle/pending; 200 ms guard", async () => {
const deps = fakeDeps();
deps.source = { name: "fixture", route: () => new Promise(() => {}) }; // never settles
const controller = createRouteController(deps);
const consult = controller.consult(FIX, DEST);
const raced = await Promise.race([
consult.then(() => "consult-settled"),
new Promise((r) => setTimeout(() => r("timeout"), 200)),
]);
assert.equal(raced, "timeout", "the 200 ms guard resolved — the case cannot hang the runner");
assert.notEqual(controller.state, "route", "the state never reaches route on a hanging source");
assert.equal(deps.__map.recorded.setData.length, 0, "NOTHING drawn while the source hangs");
assert.equal(deps.__map.recorded.fitBounds.length, 0, "no camera move while the source hangs");
assert.equal(deps.__recorded.cards.some((c) => c.kind === "route"), false, "no route card was built");
});
/* ————— FM4 — no-geometry: refused before the draw path ————— */
test("FM4 no-geometry — a polyline6 decoding to < 2 points is refused; the pending card renders, never a half-drawn line", async () => {
// the vector: a single-point geometry (encode → decode round-trip proves
// the codec's honest output; a real malformed string reads the same)
const single = encodePolyline6([{ lat: 3.15, lng: 101.7 }]);
const decoded = decodePolyline6(single);
assert.ok(decoded.length < 2, `the vector's decoded point count is < 2 (got ${decoded.length})`);
// control: the committed-corpus shape is drawable
const pairGeometry = decodePolyline6(
JSON.parse(readFileSync(path.join(HERE, "..", "..", "routing", "src", "data", "route-fixtures.json"), "utf8")).pairs[0].routes[0].geometry,
);
assert.ok(pairGeometry.length >= 2, "control: a real route decodes to >= 2 points");
// the controller must treat such a geometry as null (the pending card),
// NOT as a drawable route
const deps = fakeDeps();
deps.source = {
name: "fixture",
route: () =>
Promise.resolve({
destinationId: DEST.id,
origin: { lat: FIX.lat, lng: FIX.lng },
backend: "engine-a",
options: [
{
geometry: { points: [{ lat: 3.15, lng: 101.7 }], cumulativeDistanceM: [0] },
summary: { distanceM: 0, durationS: 0 },
steps: [],
},
],
}),
};
const controller = createRouteController(deps);
await withConsole(() => controller.consult(FIX, DEST));
assert.equal(controller.state, "pending", "a <2-point geometry renders the pending card, never a drawable route");
assert.equal(deps.__recorded.cards.at(-1).kind, "pending");
assert.equal(deps.__map.recorded.setData.length, 0, "the half-line never reaches the map sources");
assert.equal(deps.__map.recorded.fitBounds.length, 0, "no camera move for a refused geometry");
assert.equal(deps.__recorded.shown.at(-1).register, "center");
});
/* ————— FM5 — off-road: one request, storm guard holds, calm slot ————— */
/** A synthetic 4 km straight line east of KL center (the R9 storm probe's
* shape — pure excursion mechanics, no fixture coupling). */
function straightModel(lengthM, segM = 39) {
const n = Math.ceil(lengthM / segM);
const lat0 = 3.15;
const lng0 = 101.69;
const dLng = segM / (111194.93 * Math.cos(lat0 * (Math.PI / 180)));
const points = Array.from({ length: n + 1 }, (_, i) => ({ lat: lat0, lng: lng0 + i * dLng }));
const cumulative = deriveCumulativeDistanceM(points);
return {
points,
cumulative,
steps: [
{ kind: "depart", distanceM: lengthM },
{ kind: "arrive", distanceM: 0 },
],
destinationId: "storm-probe",
destinationName: "Storm Probe",
};
}
/** A fix `offM` meters north of the line point (pure-latitude offset reads
* exactly offM through the same haversineM the machine uses). */
function offsetFix(p, offM, t) {
const M_PER_DEG = 111194.93;
return {
lat: p.lat + offM / M_PER_DEG,
lng: p.lng,
accuracyM: 10,
headingDeg: 90,
ts: 1_760_000_000_000 + t * 1000,
source: "webview",
};
}
test("FM5 off-road — exactly ONE reroute-requested; the storm guard holds the immediate second excursion; the machine lands rerouting with the CALM slot", async () => {
const model = straightModel(4000);
const base = model.points[0];
// on-line fixes t=0..5 (start gate), then a permanent 80 m offset from t=6
let snap = createGuidanceSession(model, offsetFix(base, 0, 0), 1_760_000_000_000);
const requests = [];
const allEffects = [];
for (let t = 1; t <= 19; t++) {
const fix = offsetFix(model.points[Math.min(t, model.points.length - 1)], t < 6 ? 0 : 80, t);
const out = stepGuidance(snap, { type: "fix", fix }, fix.ts);
snap = out.snap;
allEffects.push(...out.effects);
for (const e of out.effects) {
if (e.type === "reroute-requested") requests.push({ t, ts: fix.ts });
}
}
// exactly ONE request: 3 consecutive off-threshold fixes + the 8 s grace
assert.equal(requests.length, 1, `excursion confirmed once (got ${requests.length} requests)`);
assert.ok(requests[0].t >= 6 + OFF_ROUTE_CONSECUTIVE_FIXES, "the request waited for 3 consecutive fixes");
const elapsed = requests[0].ts - (1_760_000_000_000 + 6 * 1000);
assert.ok(elapsed >= OFF_ROUTE_GRACE_MS, `the request honored the grace (>= ${OFF_ROUTE_GRACE_MS} ms)`);
// the IMMEDIATE second excursion (t=15..19, inside the storm guard) stays
// silent — the calm law, never a reroute storm
assert.ok(
1_760_000_000_000 + 19 * 1000 - requests[0].ts < STORM_GUARD_MS,
"the second excursion is inside the storm guard window",
);
assert.ok(
allEffects.filter((e) => e.type === "reroute-requested").length === 1,
"the storm guard held — no second request fired",
);
// the controller answers: reroute-started -> the machine lands rerouting
const stepped = stepGuidance(snap, { type: "reroute-started" }, 1_760_000_000_000 + 20 * 1000);
assert.equal(stepped.snap.state, "rerouting", "rerouting — not off_route hanging, not a throw");
// the banner's REROUTING slot is the CALM one, built by the REAL builder
await withDocument(async () => {
const view = guidanceView(stepped.snap, false);
const banner = buildGuidanceBanner(
"en",
view.banner,
{ onToggle() {} },
);
const text = textOf(banner);
assert.ok(text.length > 0, "the banner renders designed copy — never a blank");
const register = findByClass(banner, "guide-register");
assert.equal(register.textContent, `${strings.en.guideRegister} // REROUTING`, "the REROUTING register word");
const street = findByClass(banner, "guide-street");
assert.equal(street.textContent, strings.en.guideReroutingLine, "the calm copy, no blame");
const BLAME_RE = /you (went|drove|missed)|wrong turn|went off|missed the turn|off the route|recalculating/i;
assert.ok(!BLAME_RE.test(text), `the calm slot blames nothing (banner text: ${JSON.stringify(text.slice(0, 120))})`);
assert.ok(findByClass(banner, "guide-gantry") !== null, "the REROUTING gantry (the machine-blink wait state) renders");
// the guidance card renders non-empty too — the full designed surface
const card = buildGuidanceCard(
"en",
{
...view.card,
minutesRemaining: 9,
kmRemaining: 1.4,
etaClock: "18:42",
},
{ onEndTrip() {} },
);
assert.ok(textOf(card).length > 0, "the guidance card renders designed text");
assert.equal(findByClass(card, "text-headline-md").textContent, "Storm Probe", "the destination headline holds");
});
});
/* ————— FM6 — old client: the chrome builds, zero capability calls ————— */
test("FM6 old client (Bot API 7.0, no 8.0 members) — the guidance chrome still builds; no capability call is attempted", () => {
// the old-client fake: version 7.0, isVersionAtLeast present, the 8.0
// fullscreen/orientation members ABSENT (the SC2.e shape). A recording
// wrapper proves the chrome BUILD path calls nothing.
const calls = [];
const wa = {
initData: "",
version: "7.0",
isVersionAtLeast: (v) => {
calls.push(`isVersionAtLeast:${v}`);
const [aMaj, aMin] = v.split(".").map(Number);
return 7 > aMaj || (7 === aMaj && 0 >= aMin);
},
// NO requestFullscreen / exitFullscreen / isFullscreen / lockOrientation members
};
assert.equal(wa.requestFullscreen, undefined, "the old-client shape: the 8.0 method is absent");
withDocument(() => {
const banner = buildGuidanceBanner(
"en",
{ state: "on_route", glyph: "left", distanceM: 250, street: "Jalan Sultan Ismail", listOpen: false },
{ onToggle() {} },
);
assert.ok(textOf(banner).length > 0, "the banner builds — non-empty element");
assert.ok(findByClass(banner, "guide-plate") !== null, "the arrow plate mounts");
const card = buildGuidanceCard(
"en",
{ destinationName: "Suria KLCC", minutesRemaining: 9, kmRemaining: 1.4, etaClock: "18:42", progressPct: 35, showNoReroute: false },
{ onEndTrip() {} },
);
assert.ok(textOf(card).length > 0, "the guidance card builds — non-empty element");
assert.equal(findByClass(card, "text-headline-md").textContent, "Suria KLCC");
assert.equal(findByClass(banner, "guide-register").textContent, `${strings.en.guideRegister} // NEXT`);
// no capability call was attempted by the chrome build path
assert.deepEqual(calls, [], `the builders touched no WebApp capability (got ${JSON.stringify(calls)})`);
});
// this suite pins the CHROME's old-client survival; fullscreen.ts's own
// suite lives in 07-01 — the boundary stays (no fullscreen import here)
const self = readFileSync(path.join(HERE, "failure-state-matrix.test.mjs"), "utf8");
assert.ok(!self.includes("telegram/" + "fullscreen"), "FM6 does not import the fullscreen module (07-01 owns it)");
});
/* ————— FM7 — location timeout: denied / awaiting-pin, never a blank ————— */
test("FM7 location timeout — the chain lands a DESIGNED failure state; 'denied' + 'awaiting-pin' exist in the state vocabulary", async () => {
// the state vocabulary carries BOTH designed failure surfaces (types.ts)
const typesSrc = readFileSync(path.join(HERE, "..", "src", "location", "types.ts"), "utf8");
assert.ok(typesSrc.includes('kind: "denied"'), "the LocationState union carries denied");
assert.ok(typesSrc.includes('kind: "awaiting-pin"'), "the LocationState union carries awaiting-pin");
assert.ok(typesSrc.includes('"timeout"'), "denied's reason set carries timeout");
// behavioral: LINK 1 unsupported (plain browser), LINK 2 times out (null),
// LINK 3 has no token -> the chain returns a NON-NULL state object whose
// kind is a designed failure kind
const noFix = () => Promise.resolve(null);
const outcome = await acquireFix(
{ wa: null, getToken: () => null, readWebview: noFix, pollPin: noFix },
null,
);
assert.notEqual(outcome.state, null, "the failure branch returns a NON-NULL state object");
assert.equal(outcome.fix, null);
assert.equal(outcome.state.kind, "denied", "the designed denied state");
assert.equal(outcome.state.reason, "no-permission");
// the timeout reason: the pin poll expires -> denied/timeout (LINK 3)
const withToken = await acquireFix(
{ wa: null, getToken: () => "tok", readWebview: noFix, pollPin: noFix },
null,
);
assert.equal(withToken.state.kind, "denied", "the designed denied state");
assert.equal(withToken.state.reason, "timeout", "the timeout reason is carried, not swallowed");
});