docs: document topology graph v0.2 contract

This commit is contained in:
赵义仑
2026-06-16 21:20:35 +08:00
parent 1ea4c52ad1
commit a7458265c0
4 changed files with 213 additions and 44 deletions

View File

@@ -10,6 +10,7 @@ import { shortHash } from "../src/hash.js";
const fixturePath = path.resolve("tests/fixtures/context-risk.html");
const fixtureFileUrl = pathToFileURL(fixturePath).href;
const examplePath = path.resolve("examples/simple.html");
const snapshotDiffEffectTypes = ["actionableInventory", "dom", "url", "visibleText"];
async function withTempOutput<T>(name: string, callback: (out: string) => Promise<T>): Promise<T> {
const dir = await mkdtemp(path.join(tmpdir(), "action-topology-engine-test-"));
@@ -238,6 +239,72 @@ test("explore records edge effects and opened-by state relationships", async ()
assert.equal(openSettingsEdge.effects.some((effect) => effect.description === "Actionable inventory changed after action" && effect.type === "dom"), false);
});
test("explore artifact preserves referential integrity and snapshot-diff effect records", async () => {
const graph = await withTempOutput("explore-integrity", (out) => exploreUrl({ url: fixturePath, maxActions: 12, out }));
const stateIds = new Set(graph.states.map((state) => state.stateId));
const elementIds = new Set(graph.elements.map((element) => element.elementId));
const edgeIds = new Set(graph.edges.map((edge) => edge.edgeId));
const locatorIds = new Set<string>();
const skippedActionIds = new Set(graph.skippedActions.map((skip) => skip.skippedActionId));
const failedObservationIds = new Set(graph.failedObservations.map((failure) => failure.failedObservationId));
const elementById = new Map(graph.elements.map((element) => [element.elementId, element]));
const observedEffectTypes = new Set<string>();
assert.equal(stateIds.size, graph.states.length);
assert.equal(elementIds.size, graph.elements.length);
assert.equal(edgeIds.size, graph.edges.length);
assert.equal(skippedActionIds.size, graph.skippedActions.length);
assert.equal(failedObservationIds.size, graph.failedObservations.length);
for (const element of graph.elements) {
assert.equal(stateIds.has(element.stateId), true);
for (const locator of element.locators) {
assert.equal(locator.elementId, element.elementId);
assert.equal(locatorIds.has(locator.locatorId), false);
locatorIds.add(locator.locatorId);
}
}
for (const edge of graph.edges) {
const targetElement = elementById.get(edge.elementId);
assert.equal(stateIds.has(edge.fromStateId), true);
if (edge.toStateId !== null) assert.equal(stateIds.has(edge.toStateId), true);
assert.ok(targetElement);
assert.equal(targetElement.stateId, edge.fromStateId);
for (const effect of edge.effects) {
assert.equal(snapshotDiffEffectTypes.includes(effect.type), true);
assert.equal(typeof effect.before, "string");
assert.equal(typeof effect.after, "string");
observedEffectTypes.add(effect.type);
}
}
assert.deepEqual([...observedEffectTypes].sort(), snapshotDiffEffectTypes);
for (const state of graph.states) {
if (state.openedByEdgeId === null) continue;
const openingEdge = graph.edges.find((edge) => edge.edgeId === state.openedByEdgeId);
assert.ok(openingEdge);
assert.equal(edgeIds.has(state.openedByEdgeId), true);
assert.equal(openingEdge.toStateId, state.stateId);
}
for (const skip of graph.skippedActions) {
const element = graph.elements.find((item) => item.elementId === skip.elementId);
assert.equal(stateIds.has(skip.stateId), true);
assert.ok(element);
assert.equal(skip.stateId, element.stateId);
}
for (const failure of graph.failedObservations) {
assert.equal(stateIds.has(failure.stateId), true);
if (failure.elementId === null) continue;
const element = graph.elements.find((item) => item.elementId === failure.elementId);
assert.ok(element);
assert.equal(failure.stateId, element.stateId);
}
});
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];