chore: merge main into feat/pkg-inventory

This commit is contained in:
morluto committed 2026-10-08 21:21:07 +08:00
commit 64d2878882
11 files changed
+333 -20

No files matched your search

+2 -2
View File
@@ -318,8 +318,8 @@ jobs:
- run: npm run verify:asset-catalog
- run: npm run verify:macos-bundle
- run: npm run verify:installer-package
- name: Native compiled NIB hierarchy regressions
run: npm run test:focused -- tests/boundary/filesystem/nibNativeHierarchy.test.ts tests/boundary/filesystem/interfaceBuilderBundle.test.ts
- name: Native installer and compiled NIB hierarchy regressions
run: npm run test:focused -- tests/boundary/filesystem/installScript.test.ts tests/boundary/filesystem/nibNativeHierarchy.test.ts tests/boundary/filesystem/interfaceBuilderBundle.test.ts
- name: Real binary metadata and Interface Builder integration
run: npm run verify:apple-dispatch && npm run verify:interface-builder
- name: Native plist CLI selection regressions
+1 -1
View File
@@ -107,7 +107,7 @@ if [[ "$dry_run" == true ]]; then
fi
printf 'Installing %s@%s...\n' "$PACKAGE" "$version"
npm install --global "${prefix_args[@]}" "$PACKAGE@$version" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
npm install --global ${prefix_args[@]+"${prefix_args[@]}"} "$PACKAGE@$version" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
[[ -x "$install_bin" ]] || fail "npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry."
installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "the installed REA version could not be read. Reinstall the requested version, then retry."
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version. Reinstall the requested version, then retry."
@@ -362,6 +362,35 @@ describe("contextual JavaScript package path resolution", () => {
});
});
describe("generic package entry heuristics", () => {
it.each([
['{"main":"actual.cjs","module":"bundler.mjs"}', "actual.cjs"],
['{"module":"bundler.mjs"}', "bundler.mjs"],
])(
"preserves generic entry heuristics without a module kind for %s",
(metadata, entry) => {
const files = fileMap([
file("app/consumer.js", "root"),
file("app/node_modules/fixture/package.json", "root", metadata),
file("app/node_modules/fixture/actual.cjs", "root"),
file("app/node_modules/fixture/bundler.mjs", "root"),
file("app/node_modules/fixture/index.js", "root"),
]);
expect(
resolve({
declaredPath: "fixture",
sourcePath: "app/consumer.js",
context: "module-specifier",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: `app/node_modules/fixture/${entry}`,
});
},
);
});
describe("directory package entrypoint precedence", () => {
it.each([
["require", '{"main":"actual.cjs"}', "actual.cjs"],
@@ -461,10 +461,12 @@ const packageEntry = (
? { ...exported, source: "exports" }
: exported;
}
const preferred =
moduleKind === "import"
? [Reflect.get(value, "module"), Reflect.get(value, "main")]
: [Reflect.get(value, "main"), Reflect.get(value, "module")];
// Node imports and requires ignore the bundler-only module field. Keep
// that fallback for callers without an explicit module-loading kind.
const preferred = [
Reflect.get(value, "main"),
...(moduleKind === undefined ? [Reflect.get(value, "module")] : []),
];
const entry = preferred.find((candidate) => candidate !== undefined);
if (entry === undefined) return { status: "missing" };
const legacy = packagePathValue(entry);
+9 -4
View File
@@ -37,9 +37,9 @@ export const parseCliJsonInput = async (
/**
* Resolve named string fields of CLI-supplied JSON against the operator
* working directory. Each entry is a key path from the document root; only
* fields holding strings are resolved, everything else passes through
* untouched. Shared MCP contracts reject relative paths, so CLI workflows
* resolve operator-relative values before validation.
* fields holding nonblank strings are resolved, everything else passes through
* untouched so shared schemas can reject blank paths. Shared MCP contracts reject
* relative paths, so CLI workflows resolve operator-relative values before validation.
*/
export const resolveCliJsonPaths = (
value: unknown,
@@ -61,7 +61,12 @@ const resolveJsonPath = (
([key, entry]: readonly [string, unknown]): readonly [string, unknown] => {
if (key !== head) return [key, entry];
if (tail.length === 0)
return [key, typeof entry === "string" ? resolve(entry) : entry];
return [
key,
typeof entry === "string" && entry.trim().length > 0
? resolve(entry)
: entry,
];
return [key, resolveJsonPath(entry, tail)];
},
);
+59
View File
@@ -0,0 +1,59 @@
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { resolveCliJsonPaths } from "./cliJsonInput.js";
import { electronActiveObservationInputSchema } from "./domain/javascript/electronActiveObservation.js";
const paths = [
["executable_path"],
["application_path"],
["application_root"],
] as const;
describe("CLI JSON path normalization", () => {
it.each([
["executable_path", ""],
["executable_path", " \t "],
["application_path", ""],
["application_path", " \t "],
["application_root", ""],
["application_root", " \t "],
])(
"keeps blank %s (%j) invalid at the shared input boundary",
(field, value) => {
const input = {
executable_path: "/synthetic/electron",
application_path: "/synthetic/app",
application_root: "/synthetic",
[field]: value,
};
const parsed = electronActiveObservationInputSchema.safeParse(
resolveCliJsonPaths(input, paths),
);
expect(parsed.success).toBe(false);
if (parsed.success) throw new Error("Expected blank path rejection");
expect(parsed.error.issues).toEqual(
expect.arrayContaining([expect.objectContaining({ path: [field] })]),
);
expect(input).toHaveProperty(field, value);
},
);
it("resolves selected relative paths without trimming names or changing unrelated fields", () => {
const input = {
browser: { executable_path: " browser ", label: "relative-label" },
application_path: "app",
};
expect(
resolveCliJsonPaths(input, [["browser", "executable_path"]]),
).toEqual({
browser: {
executable_path: resolve(" browser "),
label: "relative-label",
},
application_path: "app",
});
expect(input.browser.executable_path).toBe(" browser ");
});
});
+56 -4
View File
@@ -56,10 +56,11 @@ export const toolInputSchemaWithMetadata = <Contract extends ToolContract>(
withAdvertisedJsonSchema(
contract.inputSchema,
"input",
(project) => (options) => ({
...describeProperties(project(options)),
examples: contract.examples.map(({ input }) => input),
}),
(project) => (options) =>
flattenRootUnion({
...describeProperties(project(options)),
examples: contract.examples.map(({ input }) => input),
}),
);
/** Advertise a canonical output schema without reconverting it per listing. */
@@ -113,6 +114,57 @@ const memoizeByTarget = (
};
};
/** Keep MCP tool roots object-shaped for clients that reject root unions. */
const flattenRootUnion = (
value: Record<string, unknown>,
): Record<string, unknown> => {
if (!Array.isArray(value.anyOf)) return value;
const branches = value.anyOf.filter(isObject);
if (branches.length === 0 || branches.length !== value.anyOf.length)
return value;
const properties: Record<string, unknown> = {};
for (const branch of branches) {
if (!isObject(branch.properties)) return value;
for (const [name, schema] of Object.entries(branch.properties)) {
const previous = properties[name];
if (previous === undefined) properties[name] = schema;
else if (JSON.stringify(previous) !== JSON.stringify(schema))
properties[name] = {
anyOf: [previous, schema],
description: fallbackPropertyDescription(name),
};
}
}
// The canonical Zod parser still enforces branch validation at invocation.
const { anyOf: _anyOf, required: _required, ...root } = value;
const requiredByBranch = branches.map((branch) =>
Array.isArray(branch.required)
? branch.required.filter(
(item): item is string => typeof item === "string",
)
: [],
);
const required = requiredByBranch.reduce((common, current) =>
common.filter((name) => current.includes(name)),
);
const minProperties = Math.max(
typeof root.minProperties === "number" ? root.minProperties : 0,
Math.min(...requiredByBranch.map((names) => names.length)),
);
return {
...root,
type: "object",
properties,
...(minProperties > 0 ? { minProperties } : {}),
...(required.length > 0 ? { required } : {}),
...(branches.every((branch) => branch.additionalProperties === false)
? { additionalProperties: false }
: {}),
};
};
const describeProperties = (
value: Readonly<Record<string, unknown>>,
): Record<string, unknown> =>
@@ -0,0 +1,32 @@
import { expect } from "vitest";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest.for(["", " \t "])(
"rejects blank Electron application_root (%j) before filesystem or provider work",
async (applicationRoot, { cli }) => {
const response = await cli.run({
arguments: [
"capture-electron-scenario",
JSON.stringify({
executable_path: "/synthetic/missing-electron",
application_path: "/synthetic/missing-app",
application_root: applicationRoot,
}),
"--json",
],
environment: { REA_LOG_LEVEL: "silent" },
timeoutMs: 15000,
});
expect(response.exitCode).toBe(1);
expect(response.json).toMatchObject({
code: "invalid_request",
details: {
operation: "capture_electron_scenario",
issues: expect.arrayContaining([
{ path: ["application_root"], reason: "out_of_range", minimum: 1 },
]),
},
});
},
);
@@ -172,6 +172,22 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => {
});
});
describe("native macOS installer", { timeout: 20_000 }, () => {
it("installs on Darwin with an empty prefix argument array", async () => {
const fixture = await createFixture();
const result = await runInstaller(fixture, ["--version", "0.3.0"], {
FAKE_PLATFORM: "Darwin",
FAKE_NPM_PREFIX: join(fixture.home, "npm global prefix"),
});
expect(result.stdout).toContain("REA 0.3.0 is installed");
expect(await readFile(fixture.npmLog, "utf8")).toBe(
"prefix --global\ninstall --global rea-agents@0.3.0\n",
);
expect(await readFile(fixture.reaLog, "utf8")).toBe("--version\n");
expect(await readdir(fixture.temporary)).toEqual([]);
});
});
describe("installer semantic version parsing", { timeout: 20_000 }, () => {
it.each(["1.2.3-01"])(
"rejects malformed semantic versions before invoking npm: %s",
@@ -309,6 +325,7 @@ printf '%s\n' "$*" >> "$FAKE_NPM_LOG"
[ "$1" = "prefix" ] && { [ "\${FAKE_NPM_PREFIX_FAIL:-}" = "1" ] && exit 1; printf '%s\n' "$FAKE_NPM_PREFIX"; exit 0; }
[ "\${FAKE_NPM_FAIL:-}" = "1" ] && exit 1
prefix="$HOME/.local"
[ "\${FAKE_PLATFORM:-Linux}" = "Darwin" ] && prefix="$FAKE_NPM_PREFIX"
while [ "$#" -gt 0 ]; do
if [ "$1" = "--prefix" ]; then shift; prefix="$1"; fi
shift
@@ -1,7 +1,11 @@
import { mkdir, writeFile } from "node:fs/promises";
import { execFile } from "node:child_process";
import { mkdir, realpath, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, join, relative, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { expect, it } from "vitest";
import { reconstructJavaScriptArtifact } from "../../../src/application/javascript/JavaScriptArtifactReconstruction.js";
@@ -160,3 +164,108 @@ it.each(cases)(
expect(result.graph.coverage.status).toBe("complete");
},
);
const runNode = promisify(execFile);
it.each([
{
name: "import uses main before bundler module",
kind: "import",
metadata: { main: "actual.cjs", module: "bundler.mjs" },
},
{
name: "import ignores a module-only entry",
kind: "import",
metadata: { module: "bundler.mjs" },
},
{
name: "require ignores a module-only entry",
kind: "require",
metadata: { module: "bundler.mjs" },
},
{
name: "import preserves a main-only entry",
kind: "import",
metadata: { main: "actual.cjs" },
},
{
name: "require preserves main before bundler module",
kind: "require",
metadata: { main: "actual.cjs", module: "bundler.mjs" },
},
{
name: "import preserves declared exports before legacy entries",
kind: "import",
metadata: {
main: "actual.cjs",
module: "bundler.mjs",
exports: { import: "./exported.mjs", require: "./actual.cjs" },
},
},
] as const)(
"matches the native Node loader when $name",
async ({ kind, metadata }) => {
const root = await createTestTempDirectory("rea-node-package-fields-");
const entry = kind === "import" ? "main.mjs" : "main.cjs";
const source =
kind === "import"
? `import value from "fixture"; console.log(JSON.stringify({value, resolved: import.meta.resolve("fixture")}));`
: `const value = require("fixture"); console.log(JSON.stringify({value, resolved: require.resolve("fixture")}));`;
const files = {
"package.json": JSON.stringify({
name: "app",
main: entry,
type: kind === "import" ? "module" : "commonjs",
}),
[entry]: source,
"node_modules/fixture/package.json": JSON.stringify({
name: "fixture",
...metadata,
}),
"node_modules/fixture/actual.cjs": "module.exports = 'actual.cjs';",
"node_modules/fixture/index.js": "module.exports = 'index.js';",
"node_modules/fixture/bundler.mjs": "export default 'bundler.mjs';",
"node_modules/fixture/exported.mjs": "export default 'exported.mjs';",
};
await Promise.all(
Object.entries(files).map(async ([path, text]) => {
await mkdir(dirname(join(root, path)), { recursive: true });
await writeFile(join(root, path), text);
}),
);
const { stdout } = await runNode(process.execPath, [join(root, entry)], {
cwd: root,
});
const oracle: unknown = JSON.parse(stdout);
expect(oracle).toMatchObject({
value: expect.any(String),
resolved: expect.any(String),
});
if (typeof oracle !== "object" || oracle === null)
throw new Error("Node returned no package identity");
const resolved: unknown = Reflect.get(oracle, "resolved");
if (typeof resolved !== "string")
throw new Error("Node returned no resolved path");
const expected = relative(
await realpath(root),
kind === "import" ? fileURLToPath(resolved) : resolved,
)
.split(sep)
.join("/");
expect(Reflect.get(oracle, "value")).toBe(expected.split("/").at(-1));
const result = await reconstructJavaScriptArtifact({
input_path: root,
format: "directory",
});
const edge = result.graph.edges.find(
({ properties }) =>
properties.module_link_kind === kind &&
properties.specifier === "fixture",
);
expect(edge?.properties).toMatchObject({
resolution_status: "resolved",
resolved_path: expected,
});
expect(result.graph.coverage.status).toBe("complete");
},
);
+12 -4
View File
@@ -328,8 +328,10 @@ describe("MCP JSON Schema validity", () => {
await Promise.allSettled([client.close(), server.close()]);
}
});
});
it("advertises the structural request requirements enforced at runtime", async () => {
describe("MCP root input schemas", () => {
it("advertises object roots while enforcing union requirements at runtime", async () => {
const server = new McpServer({ name: "schema-constraints", version: "0" });
const client = new Client({ name: "schema-constraints", version: "0" });
const [clientTransport, serverTransport] =
@@ -353,12 +355,18 @@ describe("MCP JSON Schema validity", () => {
if (graphContract === undefined || graphTool === undefined)
throw new Error("Managed application graph tool was not advertised");
expect(graphContract.inputSchema.safeParse({}).success).toBe(false);
expect(graphTool.inputSchema).toMatchObject({ type: "object" });
expect(ajv.compile(graphTool.inputSchema)({})).toBe(false);
expect(ajv.compile(graphTool.inputSchema)({ unrelated: true })).toBe(
false,
);
for (const contract of TOOL_CONTRACTS) {
const validate = ajv.compile(
advertised.get(contract.name)!.inputSchema,
);
const inputSchema = advertised.get(contract.name)!.inputSchema;
expect(inputSchema.type, contract.name).toBe("object");
expect(inputSchema.properties, contract.name).toBeDefined();
expect(inputSchema.anyOf, contract.name).toBeUndefined();
const validate = ajv.compile(inputSchema);
for (const example of contract.examples)
expect(
validate(example.input),