mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
fix(javascript): report JSON status for imported package manifests
Use the existing JSON-text status checker for explicit package.json imports while retaining separate package-metadata validity. Preserve malformed/unavailable status and partial facts after cancellation. Co-authored-by: Immanuel Tikhonov <[email protected]>
This commit is contained in:
@@ -379,7 +379,9 @@ const relationshipProperties = (
|
||||
resolution_status: target?.status ?? null,
|
||||
target_file_kind: target?.file?.kind ?? null,
|
||||
target_json_status:
|
||||
target?.file?.kind === "json" ? jsonStatus(target.file) : null,
|
||||
target?.file?.kind === "json" || target?.file?.kind === "package-json"
|
||||
? jsonStatus(target.file)
|
||||
: null,
|
||||
});
|
||||
|
||||
const relationshipEvidence = (
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import { expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"reports JSON availability for imported package manifests through %s",
|
||||
async (surface, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-package-json-imports-");
|
||||
const files = {
|
||||
"main.cjs": [
|
||||
'const version = require("./package.json").version;',
|
||||
'const data = require("./data.json");',
|
||||
'const helper = require("./helper.cjs");',
|
||||
'const broken = require("./broken/package.json");',
|
||||
'const nonObject = require("./non-object/package.json");',
|
||||
'const unavailable = require("./unavailable/package.json");',
|
||||
].join("\n"),
|
||||
"package.json": '{"name":"app","version":"1.0.0"}',
|
||||
"data.json": "[]",
|
||||
"helper.cjs": "module.exports = 1;",
|
||||
"broken/package.json": "{",
|
||||
"non-object/package.json": "[]",
|
||||
"unavailable/package.json": Buffer.from([0xff]),
|
||||
};
|
||||
await Promise.all(
|
||||
Object.entries(files).map(async ([path, content]) => {
|
||||
const absolute = join(root, path);
|
||||
await mkdir(dirname(absolute), { recursive: true });
|
||||
await writeFile(absolute, content);
|
||||
}),
|
||||
);
|
||||
const nativeRequire = createRequire(join(root, "main.cjs"));
|
||||
expect(nativeRequire("./package.json").version).toBe("1.0.0");
|
||||
expect(nativeRequire("./non-object/package.json")).toEqual([]);
|
||||
expect(nativeRequire("./data.json")).toEqual([]);
|
||||
|
||||
let document: unknown;
|
||||
if (surface === "CLI") {
|
||||
const response = await cli.run({
|
||||
arguments: ["analyze-javascript-application", root, "--json"],
|
||||
});
|
||||
expect(response.exitCode, response.stderr).toBe(0);
|
||||
document = response.json;
|
||||
} else {
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
const response = await call("analyze_javascript_application", {
|
||||
input_path: root,
|
||||
});
|
||||
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
||||
document = response.structuredContent;
|
||||
}
|
||||
const { graph } = z
|
||||
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
|
||||
.parse(document).normalized_result;
|
||||
const imports = graph.edges.filter(
|
||||
({ properties }) => properties.module_link_kind === "require",
|
||||
);
|
||||
for (const [path, kind, status, metadataStatus] of [
|
||||
["package.json", "package-json", "included", "included"],
|
||||
["data.json", "json", "included", null],
|
||||
["helper.cjs", "javascript", null, null],
|
||||
["broken/package.json", "package-json", "invalid", "invalid"],
|
||||
["non-object/package.json", "package-json", "included", "invalid"],
|
||||
[
|
||||
"unavailable/package.json",
|
||||
"package-json",
|
||||
"unavailable",
|
||||
"unavailable",
|
||||
],
|
||||
]) {
|
||||
expect(imports).toContainEqual(
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
specifier: `./${path}`,
|
||||
resolved_path: path,
|
||||
resolution_status: "resolved",
|
||||
target_file_kind: kind,
|
||||
target_json_status: status,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
if (kind === "package-json")
|
||||
expect(
|
||||
graph.nodes
|
||||
.filter(({ kind }) => kind === "package")
|
||||
.flatMap(({ observations }) => observations),
|
||||
).toContainEqual(
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
path,
|
||||
parse_status: metadataStatus,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1,11 +1,80 @@
|
||||
import { setImmediate } from "node:timers";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
import { analyzeJavaScriptApplication } from "../../support/javascriptApplicationScope.js";
|
||||
|
||||
it("preserves imported manifest status when cancelled before visiting manifests", async () => {
|
||||
const root = await createTestTempDirectory("rea-js-cancel-package-");
|
||||
const manifests = [
|
||||
["valid", '{"version":"1.0.0"}', "included"],
|
||||
["malformed", "{", "invalid"],
|
||||
["non-object", "[]", "included"],
|
||||
["unavailable", Buffer.from([0xff]), "unavailable"],
|
||||
] as const;
|
||||
await Promise.all(
|
||||
manifests.map(async ([name, content]) => {
|
||||
const directory = join(root, "z", name);
|
||||
await mkdir(directory, { recursive: true });
|
||||
await writeFile(join(directory, "package.json"), content);
|
||||
}),
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "a.cjs"),
|
||||
manifests
|
||||
.map(
|
||||
([name], index) =>
|
||||
`const manifest${index} = require("./z/${name}/package.json");`,
|
||||
)
|
||||
.join("\n"),
|
||||
);
|
||||
await writeFile(join(root, "b.cjs"), "module.exports = 1;");
|
||||
const controller = new AbortController();
|
||||
const visited: string[] = [];
|
||||
const result = await analyzeJavaScriptApplication(
|
||||
{ input_path: root, format: "directory" },
|
||||
{
|
||||
signal: controller.signal,
|
||||
progress: {
|
||||
async report(event) {
|
||||
if (event.phase !== "parse_javascript_source") return;
|
||||
visited.push(event.message);
|
||||
if (event.message.includes("b.cjs")) controller.abort();
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
if (result.ok) throw new Error("Cancelled analysis must return a failure");
|
||||
const failure = projectAnalysisError(result.error);
|
||||
expect(failure.code).toBe("cancelled");
|
||||
expect(visited).toHaveLength(2);
|
||||
const { graph } = z
|
||||
.object({
|
||||
details: z.object({
|
||||
partial_observation: z.object({
|
||||
normalized_result: javascriptApplicationAnalysisResultSchema,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.parse(failure).details.partial_observation.normalized_result;
|
||||
for (const [name, , status] of manifests)
|
||||
expect(
|
||||
graph.edges.find(
|
||||
({ properties }) =>
|
||||
properties.module_link_kind === "require" &&
|
||||
properties.specifier === `./z/${name}/package.json`,
|
||||
)?.properties,
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
target_file_kind: "package-json",
|
||||
target_json_status: status,
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript application cancellation", () => {
|
||||
it("interrupts semantic graph commitment before result validation", async () => {
|
||||
const root = await createTestTempDirectory("rea-js-cancel-semantic-");
|
||||
|
||||
Reference in New Issue
Block a user