feat: verify explored edges by replay
This commit is contained in:
8
tests/fixtures/context-risk.html
vendored
8
tests/fixtures/context-risk.html
vendored
@@ -11,11 +11,14 @@
|
||||
button, input, select, a { margin: 4px; padding: 6px 10px; }
|
||||
[role="dialog"] { display: none; border: 1px solid #999; padding: 16px; margin-top: 16px; }
|
||||
[role="dialog"].open { display: block; }
|
||||
#settings-dialog.open { position: fixed; inset: 24px auto auto 24px; background: white; z-index: 10; }
|
||||
#toast { margin-top: 16px; min-height: 24px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Accounts</h1>
|
||||
<button id="storage-leak-marker" hidden>Replay storage leaked</button>
|
||||
<button data-testid="remember-filter" onclick="localStorage.setItem('ateReplayLeak', '1')">Remember filter</button>
|
||||
<nav aria-label="Primary">
|
||||
<a href="#overview" data-testid="overview-link">Overview</a>
|
||||
<a href="#reports" data-testid="reports-link">Reports</a>
|
||||
@@ -78,6 +81,7 @@
|
||||
<section aria-label="Duplicate controls">
|
||||
<button>Duplicate</button>
|
||||
<button>Duplicate</button>
|
||||
<button><span aria-hidden="true"></span></button>
|
||||
</section>
|
||||
|
||||
<section role="dialog" aria-modal="true" class="open">
|
||||
@@ -115,6 +119,10 @@
|
||||
|
||||
<p id="toast" role="status"></p>
|
||||
<script>
|
||||
if (localStorage.getItem("ateReplayLeak")) {
|
||||
document.getElementById("storage-leak-marker").hidden = false;
|
||||
}
|
||||
|
||||
function openEditor(name) {
|
||||
document.getElementById("customer-name").value = name;
|
||||
document.getElementById("editor-title").textContent = "Editing " + name;
|
||||
|
||||
@@ -33,6 +33,24 @@ function hasEdgeForAccessibleName(
|
||||
});
|
||||
}
|
||||
|
||||
function edgeForAccessibleName(
|
||||
graph: {
|
||||
edges: Array<{
|
||||
edgeId: string;
|
||||
elementId: string;
|
||||
toStateId: string | null;
|
||||
effects: Array<{ type: string; before?: string; after?: string; description: string }>;
|
||||
}>;
|
||||
elements: Array<{ elementId: string; accessibleName: string | null }>;
|
||||
},
|
||||
accessibleName: string,
|
||||
) {
|
||||
return graph.edges.find((edge) => {
|
||||
const element = graph.elements.find((item) => item.elementId === edge.elementId);
|
||||
return element?.accessibleName === accessibleName;
|
||||
});
|
||||
}
|
||||
|
||||
test("scan captures a state-local inventory without clicking high-risk actions", async () => {
|
||||
const graph = await withTempOutput("scan", (out) => scanUrl({ url: fixturePath, out }));
|
||||
assert.equal(graph.states.length, 1);
|
||||
@@ -198,8 +216,110 @@ test("explore records low-risk edges and keeps high-risk submit actions out of d
|
||||
assert.equal(graph.states.length >= 2, true);
|
||||
});
|
||||
|
||||
test("explore records edge effects and opened-by state relationships", async () => {
|
||||
const graph = await withTempOutput("explore-effects", (out) => exploreUrl({ url: fixturePath, maxActions: 12, out }));
|
||||
const entryState = graph.states[0];
|
||||
const openSettingsEdge = edgeForAccessibleName(graph, "Open settings");
|
||||
|
||||
assert.ok(entryState);
|
||||
assert.ok(openSettingsEdge);
|
||||
assert.ok(openSettingsEdge.toStateId);
|
||||
|
||||
const openedState = graph.states.find((state) => state.stateId === openSettingsEdge.toStateId);
|
||||
assert.ok(openedState);
|
||||
assert.equal(entryState.openedByEdgeId, null);
|
||||
assert.equal(openedState.openedByEdgeId, openSettingsEdge.edgeId);
|
||||
|
||||
const visibleTextEffect = openSettingsEdge.effects.find((effect) => effect.type === "visibleText");
|
||||
assert.ok(visibleTextEffect);
|
||||
assert.equal(visibleTextEffect.before, entryState.visibleTextHash);
|
||||
assert.equal(visibleTextEffect.after, openedState.visibleTextHash);
|
||||
assert.equal(openSettingsEdge.effects.some((effect) => effect.type === "actionableInventory"), true);
|
||||
assert.equal(openSettingsEdge.effects.some((effect) => effect.description === "Actionable inventory changed after action" && effect.type === "dom"), false);
|
||||
});
|
||||
|
||||
test("explore replays each v0.2.1 edge from the entry state", async () => {
|
||||
const graph = await withTempOutput("explore-entry-replay", (out) => exploreUrl({ url: fixturePath, maxActions: 8, out }));
|
||||
const entryState = graph.states[0];
|
||||
|
||||
assert.ok(entryState);
|
||||
assert.equal(graph.edges.length > 0, true);
|
||||
assert.equal(graph.edges.every((edge) => edge.fromStateId === entryState.stateId), true);
|
||||
});
|
||||
|
||||
test("explore isolates replay contexts for storage-writing actions", async () => {
|
||||
const graph = await withTempOutput("explore-storage-isolation", (out) => exploreUrl({ url: fixturePath, maxActions: 8, out }));
|
||||
const entryState = graph.states[0];
|
||||
const rememberFilterEdge = edgeForAccessibleName(graph, "Remember filter");
|
||||
const leakText = "Replay storage leaked";
|
||||
|
||||
assert.ok(entryState);
|
||||
assert.ok(rememberFilterEdge);
|
||||
assert.equal(graph.edges.every((edge) => edge.fromStateId === entryState.stateId), true);
|
||||
assert.equal(
|
||||
graph.states.some(
|
||||
(state) =>
|
||||
state.title.includes(leakText) ||
|
||||
state.url.includes(leakText) ||
|
||||
state.route.includes(leakText) ||
|
||||
state.evidence.some((item) => item.description.includes(leakText)),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
graph.elements.some(
|
||||
(element) =>
|
||||
element.accessibleName === leakText ||
|
||||
element.text?.includes(leakText) ||
|
||||
element.evidence.some((item) => item.description.includes(leakText)),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
graph.edges.some((edge) =>
|
||||
edge.effects.some(
|
||||
(effect) =>
|
||||
effect.description.includes(leakText) || effect.before?.includes(leakText) || effect.after?.includes(leakText),
|
||||
),
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("explore records locator failures instead of edges or fake effects for unsafe low-risk candidates", async () => {
|
||||
const graph = await withTempOutput("explore-locator-failures", (out) => exploreUrl({ url: fixturePath, maxActions: 10, out }));
|
||||
const entryState = graph.states[0];
|
||||
assert.ok(entryState);
|
||||
|
||||
const duplicates = graph.elements.filter((element) => element.stateId === entryState.stateId && element.accessibleName === "Duplicate");
|
||||
const namelessButton = graph.elements.find((element) => element.stateId === entryState.stateId && element.tag === "button" && element.accessibleName === null);
|
||||
|
||||
assert.equal(duplicates.length, 2);
|
||||
assert.equal(hasEdgeForAccessibleName(graph, "Duplicate"), false);
|
||||
assert.ok(namelessButton);
|
||||
assert.equal(graph.edges.some((edge) => edge.elementId === namelessButton.elementId), false);
|
||||
for (const duplicate of duplicates) {
|
||||
assert.equal(
|
||||
graph.failedObservations.some(
|
||||
(failure) => failure.elementId === duplicate.elementId && failure.reason === "locator_not_unique",
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
graph.failedObservations.some(
|
||||
(failure) => failure.elementId === namelessButton.elementId && failure.reason === "locator_not_found",
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
graph.edges.some((edge) => edge.effects.some((effect) => effect.description.includes("Action failed:"))),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("explore records state-local skipped high-risk actions with evidence", async () => {
|
||||
const graph = await withTempOutput("explore-skips", (out) => exploreUrl({ url: fixturePath, maxActions: 8, out }));
|
||||
const graph = await withTempOutput("explore-skips", (out) => exploreUrl({ url: fixturePath, maxActions: 12, out }));
|
||||
assert.equal(graph.skippedActions.some((skip) => skip.reason.includes("form_submit")), true);
|
||||
|
||||
const deleteElement = graph.elements.find((element) => element.accessibleName === "Delete account");
|
||||
|
||||
Reference in New Issue
Block a user