Files
explore/test/node/source-navigation-schema.test.mjs
Brent Vatne 284ae6a780 Local simulators and emulators, capture targets, faster and sturdier runs (#7)
Capture on this machine (local iOS simulators and Android emulators, with agent-device or Argent) as well as on EAS Simulator; name several capture targets per config (a phone, a foldable closed and open, Android) and review them in parallel with shared builds; repack locally with a local simulator; and make captures faster and harder to break: controller fault containment, an Argent fold recovery, upright captures of an opened iPhone Duo, a settle after each root reset, a route retry after an adb drop, emulator port reservation, a startup sweep of what interrupted captures leave running, and agent-device 0.21.28.
2026-10-17 14:55:21 -07:00

103 lines
6.1 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import Ajv2020 from "ajv/dist/2020.js";
const { tsImport } = await import("tsx/esm/api");
const { ExploreStore, validateMap } = await tsImport("../../src/map/store.ts", import.meta.url);
const schema = JSON.parse(await readFile(new URL("../../schema/screen-map.v1.schema.json", import.meta.url), "utf8"));
const validate = new Ajv2020({ strict: false, validateFormats: false }).compile(schema);
const timestamp = "2026-09-15T00:00:00.000Z";
const graph = {
contract: "explore.navigation",
edges: [{ from: "start", to: "end", kind: "push", sourceFile: "src/app/index.tsx", provenance: "source" }],
modalSurfaceIds: ["end"], unresolved: [],
};
const fixture = () => ({
contract: "expo.explore.local-map.v1", revision: 1,
run: { id: "run", name: "Source navigation", platform: "ios", controller: "agent-device", status: "draft",
createdAt: timestamp, updatedAt: timestamp, build: {}, environment: {},
limits: { maxScreens: 2, maxActions: 1, maxDurationMinutes: 1 } },
artifacts: [], screens: [], variants: [], observations: [], elements: [],
actionAttempts: [], transitions: [], journeys: [], frontier: [], gaps: [],
});
test("map schema accepts optional independent current and baseline source graphs", () => {
assert.equal(validate(fixture()), true, JSON.stringify(validate.errors));
const map = { ...fixture(), sourceNavigation: structuredClone(graph), baselineSourceNavigation: structuredClone(graph) };
assert.equal(validate(map), true, JSON.stringify(validate.errors));
assert.deepEqual(map.transitions, []);
assert.deepEqual(map.actionAttempts, []);
});
test("structural journeys validate source membership without claiming observed replay evidence", async () => {
const map = fixture();
map.screens = ["start", "end"].map(id => ({ id, title: id, start: false, terminal: false, depth: 0,
firstSeenAt: timestamp, lastSeenAt: timestamp, visits: 0, variantIds: [], observationIds: [], elementIds: [] }));
const source = { kind: "structural", flowId: "flow-feature", revision: "current" };
map.journeys = [{ id: "flow-feature", title: "Feature", screenIds: ["start", "end"], transitionIds: [], attemptIds: [], source }];
const store = new ExploreStore("/unused-structural-validation");
assert.equal(validate(map), true, JSON.stringify(validate.errors));
assert.deepEqual(await validateMap(map, store, { checkArtifacts: false }), []);
for (const invalid of [{ ...source, kind: "observed" }, { ...source, flowId: "../secret" },
{ ...source, revision: "future" }, { ...source, confidence: 1 }]) {
assert.equal(validate({ ...map, journeys: [{ ...map.journeys[0], source: invalid }] }), false);
}
const unknown = structuredClone(map); unknown.journeys[0].screenIds.push("missing");
assert.ok((await validateMap(unknown, store, { checkArtifacts: false })).some(error => /unknown screen/.test(error)));
const invented = structuredClone(map); invented.journeys[0].transitionIds.push("invented");
assert.ok((await validateMap(invented, store, { checkArtifacts: false })).some(error => /cannot claim observed/.test(error)));
const observed = structuredClone(map); delete observed.journeys[0].source;
assert.ok((await validateMap(observed, store, { checkArtifacts: false })).some(error => /one transition per screen pair/.test(error)));
});
test("map schema rejects invented observed metadata and unsupported source links", async t => {
for (const change of [
value => { value.edges[0].provenance = "observed"; },
value => { value.edges[0].attemptIds = ["fake-attempt"]; },
value => { value.edges[0].kind = "back"; },
value => { value.contract = "expo.explore.observed-navigation.v1"; },
value => { delete value.modalSurfaceIds; },
]) await t.test(change.toString(), () => {
const invalid = structuredClone(graph);
change(invalid);
assert.equal(validate({ ...fixture(), sourceNavigation: invalid }), false);
assert.equal(validate({ ...fixture(), baselineSourceNavigation: invalid }), false);
});
});
test("map semantic validation rejects dangling, unsafe, and duplicate source links on either revision", async t => {
const map = fixture();
map.screens = ["start", "end"].map(id => ({
id, title: id, start: id === "start", terminal: id === "end", depth: 0,
firstSeenAt: timestamp, lastSeenAt: timestamp, visits: 0,
variantIds: [], observationIds: [], elementIds: [],
}));
const store = new ExploreStore("/unused-navigation-validation");
assert.deepEqual(await validateMap({ ...map, sourceNavigation: graph, baselineSourceNavigation: graph }, store, { checkArtifacts: false }), []);
for (const field of ["sourceNavigation", "baselineSourceNavigation"]) {
for (const [name, change] of [
["dangling target", value => { value.edges[0].to = "missing-screen"; }],
["unsafe file", value => { value.edges[0].sourceFile = "../private-file.tsx"; }],
["duplicate edge", value => { value.edges.push({ ...value.edges[0] }); }],
["dangling modal", value => { value.modalSurfaceIds = ["missing-screen"]; }],
]) await t.test(`${field}: ${name}`, async () => {
const invalid = structuredClone(graph);
change(invalid);
const errors = await validateMap({ ...map, [field]: invalid }, store, { checkArtifacts: false });
assert.ok(errors.some(error => error.startsWith(`${field}:`)), errors.join("\n"));
});
}
});
test("map schema accepts a run's capture target and its sibling targets", () => {
const map = fixture();
const targets = [{ id: "iphone", platform: "ios", label: "iPhone 17" }, { id: "duo-open", platform: "ios", label: "iPhone Duo (open)" }];
map.run = { ...map.run, target: targets[1], targets };
assert.equal(validate(map), true, JSON.stringify(validate.errors));
for (const target of [{ id: "Duo", platform: "ios", label: "x" }, { id: "duo", platform: "web", label: "x" }, { id: "duo", platform: "ios" }]) {
assert.equal(validate({ ...map, run: { ...map.run, target } }), false, JSON.stringify(target));
}
assert.equal(validate({ ...map, run: { ...map.run, targets: [targets[0]] } }), false, "one target is no sibling");
});