feat: capture state and element evidence
This commit is contained in:
@@ -3,9 +3,12 @@ import { test } from "node:test";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { exploreUrl, scanUrl } from "../src/engine.js";
|
||||
import { shortHash } from "../src/hash.js";
|
||||
|
||||
const fixturePath = path.resolve("tests/fixtures/context-risk.html");
|
||||
const fixtureFileUrl = pathToFileURL(fixturePath).href;
|
||||
|
||||
async function withTempOutput<T>(name: string, callback: (out: string) => Promise<T>): Promise<T> {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "action-topology-engine-test-"));
|
||||
@@ -51,6 +54,131 @@ test("graph metadata records generation context and policy versions", async () =
|
||||
assert.equal(typeof graph.metadata.inputFingerprint, "string");
|
||||
});
|
||||
|
||||
test("scan records state lifecycle, language, context anchors, and parameter surfaces", async () => {
|
||||
const graph = await withTempOutput("evidence", (out) =>
|
||||
scanUrl({ url: `${fixtureFileUrl}?utm_source=codex&gclid=ad-click&keep=1#customers`, out }),
|
||||
);
|
||||
const state = graph.states[0];
|
||||
assert.equal(state.lifecycle, "fingerprinted");
|
||||
assert.equal(state.language, "en");
|
||||
assert.equal(state.viewport.width, 1440);
|
||||
assert.equal(state.urlCanonicalKey, `${fixtureFileUrl}?keep=1`);
|
||||
assert.equal(state.evidence.some((item) => item.level === "observed" && item.source === "browser.location"), true);
|
||||
assert.equal(state.evidence.some((item) => item.level === "derived" && item.source === "snapshot.hash"), true);
|
||||
|
||||
const query = graph.elements.find((element) => element.accessibleName === "Query");
|
||||
assert.ok(query);
|
||||
assert.equal(query.parameterSurface?.name, "q");
|
||||
assert.equal(query.parameterSurface?.required, true);
|
||||
assert.equal(query.parameterSurface?.maxLength, 40);
|
||||
assert.equal(query.context.form?.name, "Search customers");
|
||||
assert.equal(query.evidence.some((item) => item.level === "observed"), true);
|
||||
|
||||
const dialogProbe = graph.elements.find((element) => element.accessibleName === "Dialog probe");
|
||||
assert.ok(dialogProbe);
|
||||
assert.equal(dialogProbe.context.dialog?.name, null);
|
||||
assert.equal(dialogProbe.context.dialog?.text?.includes("noisy descendant copy"), true);
|
||||
|
||||
const shortUnnamedDialog = graph.elements.find(
|
||||
(element) => element.tag === "section" && element.role === "dialog" && element.text?.includes("Short volatile copy"),
|
||||
);
|
||||
assert.ok(shortUnnamedDialog);
|
||||
assert.equal(shortUnnamedDialog.context.dialog?.name, null);
|
||||
assert.equal(shortUnnamedDialog.context.dialog?.text?.includes("Short volatile copy"), true);
|
||||
const shortUnnamedDialogIndex = graph.elements.findIndex((element) => element === shortUnnamedDialog);
|
||||
assert.notEqual(shortUnnamedDialogIndex, -1);
|
||||
assert.equal(
|
||||
shortUnnamedDialog.elementId,
|
||||
`el_${shortHash([
|
||||
state.stateId,
|
||||
shortUnnamedDialogIndex,
|
||||
shortUnnamedDialog.tag,
|
||||
shortUnnamedDialog.role,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
shortUnnamedDialog.context.dialog?.name,
|
||||
shortUnnamedDialog.context.form?.name,
|
||||
shortUnnamedDialog.context.section?.name,
|
||||
shortUnnamedDialog.context.landmark?.name,
|
||||
shortUnnamedDialog.context.listitem?.name,
|
||||
null,
|
||||
])}`,
|
||||
);
|
||||
|
||||
const unnamedDialog = graph.elements.find(
|
||||
(element) => element.tag === "section" && element.role === "dialog" && element.text?.includes("noisy descendant copy"),
|
||||
);
|
||||
assert.ok(unnamedDialog);
|
||||
const unnamedDialogIndex = graph.elements.findIndex((element) => element === unnamedDialog);
|
||||
assert.notEqual(unnamedDialogIndex, -1);
|
||||
assert.equal(
|
||||
unnamedDialog.elementId,
|
||||
`el_${shortHash([
|
||||
state.stateId,
|
||||
unnamedDialogIndex,
|
||||
unnamedDialog.tag,
|
||||
unnamedDialog.role,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
unnamedDialog.context.dialog?.name,
|
||||
unnamedDialog.context.form?.name,
|
||||
unnamedDialog.context.section?.name,
|
||||
unnamedDialog.context.landmark?.name,
|
||||
unnamedDialog.context.listitem?.name,
|
||||
null,
|
||||
])}`,
|
||||
);
|
||||
|
||||
const duplicates = graph.elements.filter((element) => element.accessibleName === "Duplicate");
|
||||
assert.equal(duplicates.length, 2);
|
||||
assert.notEqual(duplicates[0]?.elementId, duplicates[1]?.elementId);
|
||||
|
||||
const unboundedProbe = graph.elements.find((element) => element.accessibleName === "Unbounded Probe");
|
||||
assert.ok(unboundedProbe);
|
||||
assert.equal(unboundedProbe.context.form?.name, null);
|
||||
assert.equal(unboundedProbe.context.form?.text?.includes("long body of descendant text"), true);
|
||||
|
||||
const mainAction = graph.elements.find((element) => element.accessibleName === "Main action");
|
||||
assert.ok(mainAction);
|
||||
assert.equal(mainAction.context.section, null);
|
||||
assert.equal(mainAction.context.landmark?.name, "Operations");
|
||||
|
||||
const edit = graph.elements.find((element) => element.accessibleName === "Edit");
|
||||
assert.ok(edit);
|
||||
assert.equal(edit.context.row?.text.includes("Acme Co") || edit.context.row?.text.includes("Beta LLC"), true);
|
||||
const edits = graph.elements.filter((element) => element.accessibleName === "Edit");
|
||||
assert.equal(edits.length, 2);
|
||||
assert.notEqual(edits[0]?.elementId, edits[1]?.elementId);
|
||||
assert.notEqual(edits[0]?.context.row?.text, edits[1]?.context.row?.text);
|
||||
for (const item of edits) {
|
||||
const index = graph.elements.findIndex((element) => element === item);
|
||||
assert.notEqual(index, -1);
|
||||
assert.equal(
|
||||
item.elementId,
|
||||
`el_${shortHash([
|
||||
state.stateId,
|
||||
index,
|
||||
item.tag,
|
||||
item.role,
|
||||
item.attributes["data-testid"] || item.attributes["data-test-id"] || item.attributes["data-test"] || null,
|
||||
item.attributes.id || null,
|
||||
item.attributes.name || null,
|
||||
item.accessibleName,
|
||||
item.context.dialog?.name,
|
||||
item.context.form?.name,
|
||||
item.context.section?.name,
|
||||
item.context.landmark?.name,
|
||||
item.context.listitem?.name,
|
||||
null,
|
||||
])}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("explore records low-risk edges and keeps high-risk submit actions out of default execution", async () => {
|
||||
const graph = await withTempOutput("explore", (out) => exploreUrl({ url: fixturePath, maxActions: 8, out }));
|
||||
assert.equal(graph.edges.some((edge) => edge.riskLevel === "low" && edge.toStateId), true);
|
||||
|
||||
Reference in New Issue
Block a user