mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 00:36:35 +00:00
* test: focus assertions on behavioral evidence Prune redundant and wording-coupled checks while retaining diagnostic, parser, recovery, and cleanup coverage. * refactor: remove test-only wrappers and duplicate checks Exercise production operations through test fixtures, remove unused adapters, and keep managed-member uncertainty guidance under its existing owner. * test: focus process validation on diagnostic facts * refactor: remove test-only runtime helpers Move semantic evidence fixture setup out of runtime and remove unused Windows settle recovery helpers while checking configured defaults independently. * test: focus artifact diagnostics on format facts * test: verify retained cleanup guidance delivery
445 lines
15 KiB
TypeScript
445 lines
15 KiB
TypeScript
import { chmod, mkdir, readFile, truncate, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { buildBinary } from "plist";
|
|
import { describe, expect } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { thinMach } from "../../../src/domain/binaryTarget.fixture.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
const environment = {
|
|
REA_LOG_LEVEL: "silent",
|
|
REA_ANALYSIS_PROVIDER: "auto",
|
|
} as const;
|
|
|
|
const capturedDiagnostic = (value: unknown): string => {
|
|
const { stdout, stderr } = z
|
|
.object({
|
|
details: z.object({
|
|
captured_output: z.object({ stdout: z.string(), stderr: z.string() }),
|
|
}),
|
|
})
|
|
.parse(value).details.captured_output;
|
|
return `${stdout}\n${stderr}`;
|
|
};
|
|
|
|
/** An app bundle whose Contents holds a directory and a non-plist file. */
|
|
const fixtureApp = async () => {
|
|
const app = join(await createTestTempDirectory("rea-plist-fail-"), "A.app");
|
|
const contents = join(app, "Contents");
|
|
await mkdir(join(contents, "MacOS"), { recursive: true });
|
|
await mkdir(join(contents, "Folder.plist"));
|
|
await writeFile(join(contents, "MacOS/App"), thinMach(0xfeedfacf, 0x100000c));
|
|
await writeFile(
|
|
join(contents, "Info.plist"),
|
|
"<plist><dict><key>CFBundleExecutable</key><string>App</string></dict></plist>",
|
|
);
|
|
await writeFile(join(contents, "Text.plist"), "not a property list\n");
|
|
return app;
|
|
};
|
|
|
|
describe.skipIf(process.platform !== "darwin")(
|
|
"native plist CLI selection failures",
|
|
() => {
|
|
cliTest.for([
|
|
["Contents/Missing.plist", "no file exists there."],
|
|
["Contents/Folder.plist", "it is not a regular file."],
|
|
["Contents/Text.plist", "plutil could not decode it as a property list"],
|
|
] as const)(
|
|
"reports why the selected %s cannot be inspected",
|
|
async ([selected, reason], { cli }) => {
|
|
const app = await fixtureApp();
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"inspect-plist",
|
|
app,
|
|
"--relative-path",
|
|
selected,
|
|
"--json",
|
|
],
|
|
environment,
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.json).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
operation: "inspect_plist",
|
|
issues: [
|
|
{
|
|
path: ["path"],
|
|
reason: "invalid_value",
|
|
message: expect.stringContaining(
|
|
`Cannot inspect ${join(app, selected)}: ${reason}`,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
});
|
|
},
|
|
);
|
|
|
|
// Root bypasses file modes, so the denial is only observable otherwise.
|
|
cliTest.skipIf(process.getuid?.() === 0)(
|
|
"reports a permission denial instead of a malformed plist",
|
|
async ({ cli, onTestFinished }) => {
|
|
const app = await fixtureApp();
|
|
const locked = join(app, "Contents/Locked.plist");
|
|
await writeFile(locked, "<plist><dict/></plist>");
|
|
await chmod(locked, 0o000);
|
|
onTestFinished(async () => {
|
|
await chmod(locked, 0o600);
|
|
});
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"inspect-plist",
|
|
app,
|
|
"--relative-path",
|
|
"Contents/Locked.plist",
|
|
"--json",
|
|
],
|
|
environment,
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.json).toMatchObject({
|
|
code: "access_denied",
|
|
details: {
|
|
path: locked,
|
|
reason: "permission denied while reading plist (EACCES)",
|
|
system_code: "EACCES",
|
|
},
|
|
});
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"reports a missing default Info.plist as unavailable for the target",
|
|
async ({ cli }) => {
|
|
const directory = await createTestTempDirectory("rea-plist-bare-");
|
|
const program = join(directory, "tool");
|
|
await writeFile(program, thinMach(0xfeedfacf, 0x100000c));
|
|
const result = await cli.run({
|
|
arguments: ["inspect-plist", program, "--json"],
|
|
environment,
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.json).toMatchObject({
|
|
code: "capability_unavailable",
|
|
message: expect.stringContaining("(CLI: --relative-path)"),
|
|
remediation: {
|
|
action: expect.stringMatching(
|
|
/select another plist.*path input.*--relative-path/iu,
|
|
),
|
|
},
|
|
details: {
|
|
operation: "inspect_plist",
|
|
reason: expect.stringContaining(
|
|
`no readable default plist at ${join(directory, "Contents/Info.plist")}: no file exists there.`,
|
|
),
|
|
},
|
|
});
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
describe.skipIf(process.platform !== "darwin")(
|
|
"native plist CLI decode diagnostics",
|
|
() => {
|
|
cliTest.for([
|
|
["not a property list", "Property List error:"],
|
|
[
|
|
"<plist><dict><key>value</key><integer> 42 </integer></dict></plist>",
|
|
"Unknown character",
|
|
],
|
|
[
|
|
"<plist><dict><key>value</key><true>x</true></dict></plist>",
|
|
"Encountered non-empty <true>",
|
|
],
|
|
] as const)(
|
|
"preserves native decode diagnostics for %s",
|
|
async ([source, diagnostic], { cli }) => {
|
|
const app = await fixtureApp();
|
|
const malformed = join(app, "Contents/Broken.plist");
|
|
await writeFile(malformed, source);
|
|
const selected = await cli.run({
|
|
arguments: [
|
|
"inspect-plist",
|
|
app,
|
|
"--relative-path",
|
|
"Contents/Broken.plist",
|
|
"--json",
|
|
],
|
|
environment,
|
|
});
|
|
expect(selected.exitCode).toBe(1);
|
|
expect(selected.json).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: [{ message: expect.stringContaining(diagnostic) }],
|
|
captured_output: { exit_code: 1 },
|
|
},
|
|
});
|
|
expect(capturedDiagnostic(selected.json)).toContain(diagnostic);
|
|
const directory = await createTestTempDirectory(
|
|
"rea-plist-default-diagnostic-",
|
|
);
|
|
const program = join(directory, "program");
|
|
await writeFile(program, thinMach(0xfeedfacf, 0x100000c));
|
|
await mkdir(join(directory, "Contents"));
|
|
const defaultPlist = join(directory, "Contents/Info.plist");
|
|
await writeFile(defaultPlist, source);
|
|
const fallback = await cli.run({
|
|
arguments: ["inspect-plist", program, "--json"],
|
|
environment,
|
|
});
|
|
expect(fallback.exitCode).toBe(1);
|
|
expect(fallback.json).toMatchObject({
|
|
code: "capability_unavailable",
|
|
details: {
|
|
reason: expect.stringContaining(diagnostic),
|
|
captured_output: { exit_code: 1 },
|
|
},
|
|
});
|
|
expect(capturedDiagnostic(fallback.json)).toContain(diagnostic);
|
|
expect(await readFile(malformed, "utf8")).toBe(source);
|
|
expect(await readFile(defaultPlist, "utf8")).toBe(source);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
describe.skipIf(process.platform !== "darwin")(
|
|
"native plist CLI direct target selection",
|
|
() => {
|
|
cliTest.for([
|
|
[
|
|
"<plist><dict><key>value</key><integer> 42 </integer></dict></plist>",
|
|
"Unknown character",
|
|
],
|
|
[
|
|
"<plist><dict><key>value</key><true>x</true></dict></plist>",
|
|
"Encountered non-empty <true>",
|
|
],
|
|
[
|
|
"<plist><dict><key>__proto__</key><true>x</true></dict></plist>",
|
|
"Encountered non-empty <true>",
|
|
],
|
|
["<plist><null/></plist>", "Encountered unknown tag null"],
|
|
["<plist/>", "Encountered empty plist tag"],
|
|
[
|
|
"<plist><integer>1</integer><integer>2</integer></plist>",
|
|
"plist can only include one object",
|
|
],
|
|
] as const)(
|
|
"classifies the selected malformed plist as input: %s",
|
|
async ([source, diagnostic], { cli }) => {
|
|
const root = await createTestTempDirectory("rea-plist-target-input-");
|
|
const path = join(root, "chosen.plist");
|
|
await writeFile(path, source);
|
|
for (const args of [[], ["--relative-path", path]]) {
|
|
const result = await cli.run({
|
|
arguments: ["inspect-plist", path, ...args, "--json"],
|
|
environment,
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.json).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: [
|
|
{
|
|
path: ["path"],
|
|
message: expect.stringContaining(diagnostic),
|
|
},
|
|
],
|
|
captured_output: { exit_code: 1 },
|
|
},
|
|
});
|
|
expect(capturedDiagnostic(result.json)).toContain(diagnostic);
|
|
}
|
|
expect(await readFile(path, "utf8")).toBe(source);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
describe.skipIf(process.platform !== "darwin")(
|
|
"native plist CLI source admission",
|
|
() => {
|
|
cliTest(
|
|
"accepts native nested plist syntax without imposing an XML grammar allowlist",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory(
|
|
"rea-plist-nested-admission-",
|
|
);
|
|
const path = join(root, "chosen.plist");
|
|
const source = "<plist><plist><integer>1</integer></plist></plist>";
|
|
await writeFile(path, source);
|
|
const result = await cli.run({
|
|
arguments: ["inspect-plist", path, "--json"],
|
|
environment,
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
expect(result.json).toMatchObject({
|
|
normalized_result: {
|
|
source_path: path,
|
|
provenance: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
tool: "plutil",
|
|
command: ["/usr/bin/plutil", "-lint", "--", "$PLIST"],
|
|
}),
|
|
]),
|
|
},
|
|
});
|
|
expect(await readFile(path, "utf8")).toBe(source);
|
|
},
|
|
);
|
|
cliTest(
|
|
"retains a valid prototype key after native validation",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-plist-valid-key-");
|
|
const path = join(root, "chosen.plist");
|
|
const source =
|
|
"<plist><dict><key>__proto__</key><integer>42</integer><key>kept</key><string>value</string></dict></plist>";
|
|
await writeFile(path, source);
|
|
const result = await cli.run({
|
|
arguments: ["inspect-plist", path, "--json"],
|
|
environment,
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
expect(result.json).toMatchObject({
|
|
normalized_result: {
|
|
value: { ["__proto__"]: 42, kept: "value" },
|
|
provenance: [
|
|
expect.objectContaining({ tool: "file" }),
|
|
expect.objectContaining({
|
|
tool: "plutil",
|
|
command: ["/usr/bin/plutil", "-lint", "--", "$PLIST"],
|
|
}),
|
|
],
|
|
},
|
|
});
|
|
expect(JSON.stringify(result.json)).toContain('"__proto__":42');
|
|
expect(JSON.stringify(result.json)).not.toContain(
|
|
"keyed __proto__ was omitted",
|
|
);
|
|
expect(await readFile(path, "utf8")).toBe(source);
|
|
},
|
|
);
|
|
cliTest(
|
|
"retains original unknown scalar facts after successful native validation",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory(
|
|
"rea-plist-source-admission-",
|
|
);
|
|
const path = join(root, "chosen.plist");
|
|
const source =
|
|
"<plist><dict><key>value</key><real>1e999</real></dict></plist>";
|
|
await writeFile(path, source);
|
|
const result = await cli.run({
|
|
arguments: ["inspect-plist", path, "--json"],
|
|
environment,
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
expect(result.json).toMatchObject({
|
|
normalized_result: {
|
|
format: "xml",
|
|
source_path: path,
|
|
value: {
|
|
value: {
|
|
$plist_type: "unknown",
|
|
element: "real",
|
|
literal: "1e999",
|
|
reason: "unrepresentable",
|
|
},
|
|
},
|
|
provenance: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
tool: "plutil",
|
|
command: ["/usr/bin/plutil", "-lint", "--", "$PLIST"],
|
|
}),
|
|
]),
|
|
},
|
|
});
|
|
expect(await readFile(path, "utf8")).toBe(source);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
describe.skipIf(process.platform !== "darwin")(
|
|
"native plist CLI fixed allowances",
|
|
() => {
|
|
const hostMemoryAdvice = /available host memory|worker file-size limits/u;
|
|
|
|
cliTest(
|
|
"names the binary plist decode allowance instead of host memory",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-plist-decode-limit-");
|
|
const path = join(root, "large.plist");
|
|
// The decode estimate exceeds 64 MiB below the 64 MiB input ceiling.
|
|
const source = buildBinary({
|
|
data: Buffer.alloc(22 * 1024 * 1024, 0x78),
|
|
});
|
|
await writeFile(path, source);
|
|
const runs = [];
|
|
for (let attempt = 0; attempt < 2; attempt += 1)
|
|
runs.push(
|
|
await cli.run({
|
|
arguments: ["inspect-plist", path, "--json"],
|
|
environment,
|
|
}),
|
|
);
|
|
const [first, second] = runs;
|
|
expect(first?.exitCode).toBe(1);
|
|
expect(second?.json).toEqual(first?.json);
|
|
expect(first?.json).toMatchObject({
|
|
code: "resource_constraint",
|
|
retryable: false,
|
|
remediation: {
|
|
action: expect.stringMatching(
|
|
/64 MiB.*67108864.byte.*decode allowance.*no runtime override.*do not raise.*complete input.*unchanged request fails/iu,
|
|
),
|
|
},
|
|
details: {
|
|
operation: "inspect_plist",
|
|
resource: "memory",
|
|
reported_limits: { maximum_decoded_bytes: 64 * 1024 * 1024 },
|
|
},
|
|
});
|
|
expect(Buffer.compare(await readFile(path), source)).toBe(0);
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"names the binary plist input allowance instead of worker file-size limits",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-plist-input-limit-");
|
|
const path = join(root, "huge.plist");
|
|
await writeFile(path, "bplist00");
|
|
await truncate(path, 64 * 1024 * 1024 + 1);
|
|
const result = await cli.run({
|
|
arguments: ["inspect-plist", path, "--json"],
|
|
environment,
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.json).toMatchObject({
|
|
code: "resource_constraint",
|
|
retryable: false,
|
|
remediation: {
|
|
action: expect.stringMatching(
|
|
/complete artifact.*64 MiB.*67108864.byte.*input allowance.*no runtime override/iu,
|
|
),
|
|
},
|
|
details: {
|
|
resource: "file-size",
|
|
reported_limits: { maximum_input_bytes: 64 * 1024 * 1024 },
|
|
},
|
|
});
|
|
expect(JSON.stringify(result.json)).not.toMatch(hostMemoryAdvice);
|
|
},
|
|
);
|
|
},
|
|
);
|