Files
reverse-engineering-anything/src/application/apiContract/ApiContractService.test.ts
T
morlutoandCursor Agent 654feeef9c fix(api-contract): retain partial OpenAPI evidence (#9908)
* fix(api-contract): retain partial OpenAPI evidence

* docs(api-contract): describe partial OpenAPI Evidence on interruption

Early failures still keep the file snapshot hash. Cancellation or the
retained-facts limit after a row now returns authenticated Evidence, and
the guide explains how to reuse that partial inventory.

* fix(api-contract): count finished paths and keep earlier diagnostics

A path counts only after its item is read, so a path refused before
traversal does not increase path_entries_examined. A paths diagnostic
that exhausts the retained-facts allowance still keeps diagnostics
already stored, including invalid info.

* test(api-contract): keep invalid info when paths exhaust the allowance

The paths diagnostic can fail after invalid info is already stored. The
partial report must still carry that diagnostic and count no path entries.

* docs(api-contract): name every retained row and the view source

The hash remains only when no operation, reference, or diagnostic row was
kept. Partial Evidence is reread through inspect_analysis_view's source
and view, not as separate Evidence fields.

* test(api-contract): verify interrupted inventory evidence

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-11 17:27:00 +08:00

481 lines
17 KiB
TypeScript

import { createHash } from "node:crypto";
import { mkdir, readFile, symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it, vi } from "vitest";
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import {
API_CONTRACT_FACT_BYTES,
apiContractSchema,
} from "../../domain/apiContract/apiContract.js";
import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js";
import { parseEvidence } from "../../domain/evidence.js";
import { projectAnalysisView } from "../../domain/analysisView/analysisView.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { inspectApiContractEvidence } from "./ApiContractService.js";
import * as readOwner from "./ApiContractRead.js";
import * as parser from "../../domain/apiContract/parseApiContract.js";
const fixture = new URL(
"../../../tests/fixtures/openapi/petstore.json.txt",
import.meta.url,
);
const inputFile = async (bytes: Uint8Array | string) => {
const root = await createTestTempDirectory("rea-api-contract-");
const path = join(root, "contract.json");
await writeFile(path, bytes);
return { root, path };
};
it("binds the published source bytes to complete Evidence independently of reply selection", async () => {
const bytes = await readFile(fixture);
const { path } = await inputFile(bytes);
const resources = new ArtifactResourceScope();
try {
const results = await Promise.all([
inspectApiContractEvidence({ path }, resources),
inspectApiContractEvidence({ path, detail: "complete" }, resources),
inspectApiContractEvidence(
{
path,
view: {
kind: "api-contract",
collection: "operations",
fields: [],
limit: 1,
},
},
resources,
),
]);
for (const result of results) expect(result.ok).toBe(true);
const [summary, complete, page] = results;
if (!summary?.ok || !complete?.ok || !page?.ok)
throw new Error("Expected verified imports");
expect(summary.value.evidence_id).toBe(complete.value.evidence_id);
expect(page.value.evidence_id).toBe(complete.value.evidence_id);
const value = apiContractSchema.parse(complete.value.normalized_result);
expect(value.artifact).toEqual({
path,
bytes: bytes.length,
sha256: createHash("sha256").update(bytes).digest("hex"),
});
expect(value.summary.declared).toBe(19);
expect(complete.value.operation).toBe("inspect_api_contract");
expect(complete.value.parameters).not.toHaveProperty("detail");
expect(complete.value.parameters).not.toHaveProperty("view");
expect((await readFile(path)).equals(bytes)).toBe(true);
} finally {
await resources.close();
}
});
it.each([
[Buffer.from([255]), "not valid UTF-8"],
[Buffer.from("{broken"), "JSON"],
[Buffer.from("\uFEFF{}"), "byte-order mark"],
])(
"reports malformed UTF-8/JSON without rewriting source %#",
async (bytes, reason) => {
const { path } = await inputFile(bytes);
const resources = new ArtifactResourceScope();
try {
const result = await inspectApiContractEvidence({ path }, resources);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error._tag).toBe("AnalysisInputError");
expect(JSON.stringify(result.error)).toContain(reason);
expect(result.error.partialObservation).toEqual({
kind: "file-snapshot-hash",
path,
bytes: bytes.length,
sha256: createHash("sha256").update(bytes).digest("hex"),
});
}
expect((await readFile(path)).equals(bytes)).toBe(true);
} finally {
await resources.close();
}
},
);
it("rejects the selected physical byte budget before parsing", async () => {
const { path } = await inputFile("{bad json}");
const resources = new ArtifactResourceScope();
try {
const result = await inspectApiContractEvidence(
{ path, max_file_bytes: 4 },
resources,
);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
if (result.error instanceof AnalysisResourceConstraintError)
expect(result.error.operation).toBe("inspect_api_contract");
}
} finally {
await resources.close();
}
});
it("bounds nesting even in schema regions omitted from the inventory", async () => {
let nested: unknown = {};
for (let depth = 0; depth < 1001; depth++) nested = { child: nested };
const { path } = await inputFile(
JSON.stringify({
openapi: "3.1.2",
info: { title: "Nested", version: "1" },
paths: {},
components: nested,
}),
);
const resources = new ArtifactResourceScope();
try {
const result = await inspectApiContractEvidence({ path }, resources);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error._tag).toBe("AnalysisInputError");
expect(JSON.stringify(result.error)).toContain("maximum nesting depth");
expect(result.error.partialObservation).toMatchObject({
kind: "file-snapshot-hash",
path,
});
}
} finally {
await resources.close();
}
});
it("rejects relative, missing and non-regular selections at the input owner", async () => {
const { root } = await inputFile("{}");
const directory = join(root, "directory");
await mkdir(directory);
const resources = new ArtifactResourceScope();
try {
for (const path of [
"relative.json",
join(root, "missing.json"),
directory,
]) {
const result = await inspectApiContractEvidence({ path }, resources);
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error._tag).toBe("AnalysisInputError");
}
} finally {
await resources.close();
}
});
it.skipIf(process.platform === "win32")(
"preserves an actionable regular-file retry for a selected symlink",
async () => {
const { root, path } = await inputFile("{}");
const link = join(root, "linked.json");
await symlink(path, link);
const resources = new ArtifactResourceScope();
try {
const result = await inspectApiContractEvidence(
{ path: link },
resources,
);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error._tag).toBe("AnalysisInputError");
expect(JSON.stringify(result.error)).toContain(path);
}
} finally {
await resources.close();
}
},
);
it("honors pre-cancellation with an opaque caller reason", async () => {
const controller = new AbortController();
const reason = { _tag: "ArtifactReaderFailure", reason: "integrity" };
controller.abort(reason);
const resources = new ArtifactResourceScope();
try {
const result = await inspectApiContractEvidence(
{ path: "/unused.json" },
resources,
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
expect(result.error.partialObservation).toBeUndefined();
}
} finally {
await resources.close();
}
});
it("keeps native JSON source excerpts internal while preserving the failure cause", async () => {
const bytes = '{"$ref":"https://u:p@h","":NaN}';
const { path } = await inputFile(bytes);
const resources = new ArtifactResourceScope();
try {
const result = await inspectApiContractEvidence({ path }, resources);
if (result.ok) throw new Error("Expected a syntax refusal");
expect(result.error.cause).toBeInstanceOf(SyntaxError);
expect(JSON.stringify(result.error)).not.toContain("u:p@h");
expect(result.error.partialObservation).toEqual({
kind: "file-snapshot-hash",
path,
bytes: Buffer.byteLength(bytes),
sha256: createHash("sha256").update(bytes).digest("hex"),
});
} finally {
await resources.close();
}
});
it.each([
[{ openapi: "2.0" }, "AnalysisUnsupportedTargetError"],
[
{
openapi: "3.1.2",
info: { title: "a".repeat(3 * 1024 * 1024), version: "1" },
},
"AnalysisResourceConstraintError",
],
])(
"keeps a completed parser failure ahead of queued cancellation %#",
async (document, tag) => {
const bytes = JSON.stringify(document);
const { path } = await inputFile(bytes);
const resources = new ArtifactResourceScope();
const controller = new AbortController();
const realParse = parser.parseApiContractSteps;
const scheduled = vi
.spyOn(parser, "parseApiContractSteps")
.mockImplementationOnce(function* (...args) {
const result = yield* realParse(...args);
expect(result.ok).toBe(false);
setImmediate(() => controller.abort("after-parser-failure"));
return result;
});
try {
const result = await inspectApiContractEvidence({ path }, resources, {
signal: controller.signal,
});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(controller.signal.aborted).toBe(true);
if (result.ok) throw new Error("Expected the parser failure");
expect(result.error._tag).toBe(tag);
expect(result.error.partialObservation).toEqual({
kind: "file-snapshot-hash",
path,
bytes: Buffer.byteLength(bytes),
sha256: createHash("sha256").update(bytes).digest("hex"),
});
} finally {
scheduled.mockRestore();
await resources.close();
}
},
);
it("cancels a cooperative document traversal after the actual verified file read", async () => {
const paths = Object.fromEntries(
Array.from({ length: 3000 }, (_, index) => [
`/items/${index}`,
{ get: { operationId: String(index) } },
]),
);
const bytes = JSON.stringify({
openapi: "3.1.2",
info: { title: "Cancellation", version: "1" },
paths,
});
const { path } = await inputFile(bytes);
const controller = new AbortController();
const reason = { stage: "after-source-read" };
const realRead = readOwner.readApiContract;
const scheduled = vi
.spyOn(readOwner, "readApiContract")
.mockImplementationOnce(async (...args) => {
const loaded = await realRead(...args);
expect(loaded.ok).toBe(true);
setImmediate(() => controller.abort(reason));
return loaded;
});
const resources = new ArtifactResourceScope();
try {
const result = await inspectApiContractEvidence({ path }, resources, {
signal: controller.signal,
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
expect(result.error.partialObservation).toEqual({
kind: "file-snapshot-hash",
path,
bytes: Buffer.byteLength(bytes),
sha256: createHash("sha256").update(bytes).digest("hex"),
});
}
expect(await readFile(path, "utf8")).toBe(bytes);
} finally {
scheduled.mockRestore();
await resources.close();
}
});
it.each(["between-paths", "after-projection"] as const)(
"preserves authenticated OpenAPI facts on cancellation %s",
async (stage) => {
const bytes = JSON.stringify({
openapi: "3.1.2",
info: { title: "Cancellation", version: "1" },
paths: { "/first": { get: {} }, "/second": { post: {} } },
});
const { path } = await inputFile(bytes);
const resources = new ArtifactResourceScope();
const controller = new AbortController();
const realParse = parser.parseApiContractSteps;
try {
const baseline = await inspectApiContractEvidence({ path }, resources);
if (!baseline.ok) throw baseline.error;
const complete = apiContractSchema.parse(
baseline.value.normalized_result,
);
const scheduled = vi
.spyOn(parser, "parseApiContractSteps")
.mockImplementationOnce(function* (...args) {
const steps = realParse(...args);
if (stage === "between-paths") {
expect(steps.next().done).toBe(false);
expect(steps.next().done).toBe(false);
setImmediate(() => controller.abort(stage));
return yield* steps;
}
const result = yield* steps;
setImmediate(() => controller.abort(stage));
return result;
});
try {
const result = await inspectApiContractEvidence({ path }, resources, {
signal: controller.signal,
});
if (result.ok) throw new Error("Expected cancellation");
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(stage);
const evidence = parseEvidence(result.error.partialObservation);
const retained = apiContractSchema.parse(evidence.normalized_result);
expect(evidence.subject).toEqual(baseline.value.subject);
expect(evidence.parameters).toEqual(baseline.value.parameters);
expect(retained.artifact).toEqual(complete.artifact);
expect(retained.contract_id).toBe(complete.contract_id);
if (stage === "between-paths") {
expect(retained.operations).toEqual(complete.operations.slice(0, 1));
expect(retained.source_coverage.status).toBe("partial");
expect(evidence.evidence_id).not.toBe(baseline.value.evidence_id);
const page = projectAnalysisView(
{
evidenceId: evidence.evidence_id,
operation: evidence.operation,
predicateType: evidence.predicate_type,
normalizedResult: evidence.normalized_result,
limitations: evidence.limitations,
},
{
kind: "api-contract",
collection: "operations",
offset: 0,
limit: 1,
},
);
expect(page).toMatchObject({
ok: true,
value: {
parent_evidence_id: evidence.evidence_id,
items: [{ path: "/first", state: "declared" }],
coverage: { exhausted: true },
source_coverage: { status: "partial" },
},
});
} else {
expect(retained).toEqual(complete);
expect(evidence.evidence_id).toBe(baseline.value.evidence_id);
}
} finally {
scheduled.mockRestore();
}
} finally {
await resources.close();
}
},
);
it.each(["path-admission", "invalid-paths"] as const)(
"retains authenticated partial facts when %s exhausts the facts allowance",
async (stage) => {
const { path } = await inputFile("{}");
// Leave enough allowance for invalid-info, but not a second diagnostic.
const titleLength = Math.floor(
(API_CONTRACT_FACT_BYTES -
"3.0.4".length * 12 -
4096 -
path.length * 12 -
(1024 + "/info".length * 12) -
500) /
12,
);
const document =
stage === "invalid-paths"
? {
openapi: "3.0.4",
info: { title: "a".repeat(titleLength) },
paths: null,
}
: {
openapi: "3.0.4",
info: { title: "Capacity", version: "1" },
paths: {
"/retained": { get: {} },
[`/${"a".repeat(Math.ceil(API_CONTRACT_FACT_BYTES / 16))}`]: {},
},
};
const bytes = JSON.stringify(document);
await writeFile(path, bytes);
const resources = new ArtifactResourceScope();
try {
const result = await inspectApiContractEvidence({ path }, resources);
if (result.ok) throw new Error("Expected a retained-facts refusal");
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
const evidence = parseEvidence(result.error.partialObservation);
const retained = apiContractSchema.parse(evidence.normalized_result);
expect(retained.artifact).toEqual({
path,
bytes: Buffer.byteLength(bytes),
sha256: createHash("sha256").update(bytes).digest("hex"),
});
expect(retained.source_coverage).toMatchObject({
status: "partial",
paths_state: stage === "invalid-paths" ? "malformed" : "object",
path_entries_examined: stage === "invalid-paths" ? 0 : 1,
});
if (stage === "invalid-paths") {
expect(retained.info).toEqual({
title: document.info.title,
version: null,
});
expect(retained.diagnostics.map((row) => row.code)).toEqual([
"invalid-info",
]);
expect(retained.operations).toEqual([]);
} else {
expect(retained.operations.map((row) => row.path)).toEqual([
"/retained",
]);
expect(retained.diagnostics).toEqual([]);
}
expect(await readFile(path, "utf8")).toBe(bytes);
} finally {
await resources.close();
}
},
);