feat: add topology graph v0.2.2 contract

This commit is contained in:
赵义仑
2026-06-24 09:47:23 +08:00
parent a7458265c0
commit 2f12fe8cda
16 changed files with 1041 additions and 42 deletions

View File

@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { mkdtemp, rm } from "node:fs/promises";
import { createServer, type Server } from "node:http";
import { tmpdir } from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
@@ -21,6 +22,20 @@ async function withTempOutput<T>(name: string, callback: (out: string) => Promis
}
}
async function withServer<T>(handler: Parameters<typeof createServer>[0], callback: (url: string) => Promise<T>): Promise<T> {
const server = createServer(handler);
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
assert.ok(address && typeof address === "object");
try {
return await callback(`http://127.0.0.1:${address.port}/`);
} finally {
await new Promise<void>((resolve, reject) => {
(server as Server).close((error) => (error ? reject(error) : resolve()));
});
}
}
function hasEdgeForAccessibleName(
graph: {
edges: Array<{ elementId: string }>;
@@ -73,7 +88,7 @@ test("duplicated row actions are verified with semantic locator evidence", async
test("graph metadata records generation context and policy versions", async () => {
const graph = await withTempOutput("metadata", (out) => scanUrl({ url: fixturePath, out }));
assert.equal(graph.schemaVersion, "0.2");
assert.equal(graph.metadata.engineVersion, "0.2.0");
assert.equal(graph.metadata.engineVersion, "0.2.2");
assert.equal(graph.metadata.runtimeContext.browserName, "chromium");
assert.equal(graph.metadata.runtimeContext.viewport.width, 1440);
assert.equal(graph.metadata.runtimeContext.viewport.height, 1000);
@@ -297,6 +312,7 @@ test("explore artifact preserves referential integrity and snapshot-diff effect
}
for (const failure of graph.failedObservations) {
if (failure.stateId === null) continue;
assert.equal(stateIds.has(failure.stateId), true);
if (failure.elementId === null) continue;
const element = graph.elements.find((item) => item.elementId === failure.elementId);
@@ -385,6 +401,35 @@ test("explore records locator failures instead of edges or fake effects for unsa
);
});
test("explore records replay navigation timeout as a failed observation", async () => {
let requestCount = 0;
await withServer(
(_request, response) => {
requestCount += 1;
if (requestCount === 1) {
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
response.end("<!doctype html><button>Open panel</button>");
return;
}
// Leave replay navigation hanging so the engine must record a timeout.
},
async (url) => {
const graph = await withTempOutput("explore-navigation-timeout", (out) =>
exploreUrl({ url, maxActions: 1, navigationTimeoutMs: 50, out }),
);
const button = graph.elements.find((element) => element.accessibleName === "Open panel");
assert.ok(button);
assert.equal(graph.edges.length, 0);
assert.equal(
graph.failedObservations.some(
(failure) => failure.elementId === button.elementId && failure.reason === "navigation_timeout",
),
true,
);
},
);
});
test("explore records state-local skipped high-risk actions with evidence", async () => {
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);