feat: capture state and element evidence

This commit is contained in:
赵义仑
2026-06-16 19:15:52 +08:00
parent 1324f1882c
commit 581724ea42
6 changed files with 431 additions and 4 deletions

View File

@@ -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);