mirror of
https://github.com/expo/explore.git
synced 2026-11-15 03:48:15 -10:00
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.
103 lines
6.1 KiB
JavaScript
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");
|
|
});
|