mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
* feat(evidence)!: give Evidence bundles a format revision owned by one module `src/domain/evidenceFormat.ts` owns Evidence format evolution. Bundles now carry `evidence_format: 1`; every bundle reader admits only the current revision and refuses an unmarked or other-revision bundle once, with the single recreate-observations guidance, instead of admitting earlier shapes one optional field at a time. The 6.7-era artifact inventory summary identities are no longer rebuilt: compare_artifacts checks the current projection only and reports a mismatch with the owner's guidance. The pre-6.6 identity diagnosis uses the same guidance. SQLite `worker_timeout_ms` and Dart AOT `string_pool.policy`, optional only for historical Evidence, are now required. BREAKING CHANGE: Evidence bundles written by REA 6.x (no `evidence_format`) are refused by import, comparison, reconstruction and analysis-snapshot readers as an integrity mismatch telling the caller to recreate the observations; re-exporting an earlier bundle does not update its format or identities. Bundles constructed by hand must include `"evidence_format": 1`. 6.7-era artifact summaries are refused by compare_artifacts, and SQLite and Dart AOT Evidence without the newly required fields fails validation. * fix(evidence): let the ledger application admit its bundle's format build_reconstruction_obligation_ledger registered the full bundle schema with the SDK, so an unmarked or other-revision bundle was refused as invalid_request before the format owner ran. Register it like coverage: the application parses the request envelope and admits the bundle. * feat(pcap)!: read capture Evidence through the current result schema only (#10879) Delete the pcap read-compatibility schema that defaulted executable identity for historical capture Evidence, and require query_labels_hex (null when unknown) now that every producer writes it. Evidence from an earlier format is refused at the bundle format owner; a record that omits these fields is reported as malformed instead of defaulted. BREAKING CHANGE: inspect_pcap_capture results always carry query_labels_hex, and analysis views no longer default missing tshark/capinfos identity fields. * feat(desktop)!: require the effective view policy in desktop state Evidence (#10886) inspect_desktop_application_state always records the complete policy it used, including sqlite_view_bytes. Drop the optional historical shape and the task draft's absent-policy branch. BREAKING CHANGE: desktop application state results without effective_view_policy, or with a policy missing sqlite_view_bytes, are refused as malformed. * feat(android)!: require component declaration facets and reference aggregates in Android Evidence (#10904) * feat(android)!: require component declaration facets and reference aggregates in Android Evidence The JADX producers always write the manifest component projection with every declaration facet, target_definition and direct_invokes_coverage. Require them, delete the duplicate observed component schema, drop the unknown declaration coverage that only described historical omission, and give manifest view results their explicit facet. BREAKING CHANGE: Android package, component and reference results missing these fields are refused as malformed; component declaration_coverage and its summary counts no longer include unknown; manifest view results always carry facet manifest. * refactor(android): parse package results with the domain schema directly * feat(browser)!: require the DOM length unit and redirect-scoped prior-hop facts (#10897) * feat(browser)!: require the DOM length unit and redirect-scoped prior-hop facts Both CDP producers always declare node_value_length_unit, and the observation session writes redirect_response on every redirect event and no other. Require the unit, drop the diff's absent-unit branch, and admit redirect_response exactly on redirect rows. BREAKING CHANGE: DOM snapshots without node_value_length_unit and redirect timeline rows without redirect_response are refused as malformed, including inline inspections passed to compare_web_captures. * docs(browser): drop the historical redirect_response omission note * feat(native)!: require native Evidence fields that were optional only for historical Evidence (#10946) Mach-O encryption, slice malformed cause, symbol runtime names and import facts; ELF export encoding facts and visibility; value-flow limits; load-image permissions and relocation symbols; Swift type-reference kind and GPU AMD target metadata are required. Legacy enum values (unrecorded, unknown visibility, undecodable, modinfo_field_exceeds_limit) and the retained schemas, view defaults and reinterpretations that served historical Evidence are deleted. BREAKING CHANGE: native records missing these fields are refused as malformed; recreate the observation.
310 lines
9.6 KiB
TypeScript
310 lines
9.6 KiB
TypeScript
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { Cli, z } from "incur";
|
|
import { expect, it } from "vitest";
|
|
|
|
import { createCli } from "../../../src/cli.js";
|
|
import { validateCliArguments } from "../../../src/cli/strictPositionals.js";
|
|
import {
|
|
excludeDeclaredSensitiveArguments,
|
|
explicitSensitiveValueOptions,
|
|
} from "../../../src/cli/explicitSensitiveArguments.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
type CliInstance = ReturnType<typeof Cli.create>;
|
|
const serve = async (cli: CliInstance, argv: string[]) => {
|
|
let stdout = "";
|
|
let exitCode = 0;
|
|
await cli.serve([...argv, "--json"], {
|
|
env: {},
|
|
stdout: (text) => {
|
|
stdout += text;
|
|
},
|
|
exit: (code) => {
|
|
exitCode = code;
|
|
},
|
|
});
|
|
return { stdout, exitCode };
|
|
};
|
|
const installAdmission = (cli: CliInstance) => {
|
|
const strict = validateCliArguments(cli);
|
|
const declared = excludeDeclaredSensitiveArguments(cli);
|
|
const original = cli.serve.bind(cli);
|
|
cli.serve = (argv = [], options = {}) =>
|
|
strict(argv, options, (tokens, selected) =>
|
|
declared(tokens, selected, original),
|
|
);
|
|
};
|
|
|
|
it("rejects surplus public CLI inputs before importing and retains the one-path control", async () => {
|
|
const directory = await createTestTempDirectory(
|
|
"rea-strict-cli-positionals-",
|
|
);
|
|
const path = join(directory, "bundle.json");
|
|
await writeFile(
|
|
path,
|
|
JSON.stringify({
|
|
evidence_format: 1,
|
|
artifacts: [],
|
|
captures: [],
|
|
environments: [],
|
|
providers: [],
|
|
records: [],
|
|
scenarios: [],
|
|
unknowns: [],
|
|
}),
|
|
);
|
|
const cli = createCli({});
|
|
for (const extra of [["ignored"], ["ignored", "second"]]) {
|
|
const result = await serve(cli, ["evidence-import", path, ...extra]);
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.stdout).toContain("Unexpected positional arguments");
|
|
for (const value of extra) expect(result.stdout).toContain(value);
|
|
}
|
|
const missingFile = await serve(cli, [
|
|
"evidence-import",
|
|
join(directory, "absent"),
|
|
"extra",
|
|
]);
|
|
expect(missingFile.exitCode).toBe(1);
|
|
expect(missingFile.stdout).toContain("Unexpected positional arguments");
|
|
const valid = await serve(cli, ["evidence-import", path]);
|
|
expect(valid.exitCode).toBe(0);
|
|
expect(JSON.parse(valid.stdout)).toMatchObject({ imported: 0, total: 0 });
|
|
expect((await serve(cli, ["providers", "ignored"])).exitCode).toBe(1);
|
|
const unknown = await serve(cli, ["providers", "--bogus"]);
|
|
expect(unknown.exitCode).toBe(1);
|
|
expect(unknown.stdout).toContain("Unknown flag: --bogus");
|
|
});
|
|
|
|
it("keeps parser-owned aliases, option values and optional arguments without rerunning transforms", async () => {
|
|
let runs = 0;
|
|
let transforms = 0;
|
|
let defaults = 0;
|
|
const cli = Cli.create("strict-fixture", { sync: false });
|
|
cli.command("scalar", {
|
|
args: z.object({
|
|
first: z.string().transform((value) => {
|
|
transforms++;
|
|
return value;
|
|
}),
|
|
second: z.string().optional(),
|
|
}),
|
|
options: z.object({
|
|
selectedValue: z.string(),
|
|
enabled: z.boolean().default(false),
|
|
tag: z.array(z.string()).default([]),
|
|
supplied: z.string().default(() => {
|
|
defaults++;
|
|
return "default";
|
|
}),
|
|
...explicitSensitiveValueOptions,
|
|
}),
|
|
alias: { selectedValue: "s", enabled: "e" },
|
|
run: ({ args, options }) => {
|
|
runs++;
|
|
return { args, options };
|
|
},
|
|
});
|
|
installAdmission(cli);
|
|
const valid = await serve(cli, [
|
|
"scalar",
|
|
"one",
|
|
"-es",
|
|
"option value",
|
|
"--tag=a",
|
|
"--tag",
|
|
"b",
|
|
]);
|
|
expect(valid.exitCode).toBe(0);
|
|
expect(JSON.parse(valid.stdout)).toMatchObject({
|
|
args: { first: "one" },
|
|
options: { enabled: true, selectedValue: "option value", tag: ["a", "b"] },
|
|
});
|
|
expect({ runs, transforms, defaults }).toEqual({
|
|
runs: 1,
|
|
transforms: 1,
|
|
defaults: 1,
|
|
});
|
|
const invalid = await serve(cli, [
|
|
"scalar",
|
|
"one",
|
|
"two",
|
|
"sensitive-extra",
|
|
"-s",
|
|
"value",
|
|
"--sensitive-value",
|
|
"sensitive-extra",
|
|
]);
|
|
expect(invalid.exitCode).toBe(1);
|
|
expect(invalid.stdout).toContain("Unexpected positional arguments");
|
|
expect(invalid.stdout).not.toContain("sensitive-extra");
|
|
expect({ runs, transforms, defaults }).toEqual({
|
|
runs: 1,
|
|
transforms: 1,
|
|
defaults: 1,
|
|
});
|
|
expect(
|
|
(await serve(cli, ["scalar", "one", "two", "-s", "value"])).exitCode,
|
|
).toBe(0);
|
|
});
|
|
|
|
it("uses resolved group and alias paths and preserves declared variadic inputs", async () => {
|
|
const group = Cli.create("group", { sync: false });
|
|
group.command("scalar", {
|
|
aliases: ["alias"],
|
|
args: z.object({ value: z.string() }),
|
|
run: ({ args }) => args,
|
|
});
|
|
group.command("many", {
|
|
args: z.object({ values: z.array(z.string()) }),
|
|
run: ({ args }) => args,
|
|
});
|
|
const cli = Cli.create("strict-groups", { sync: false }).command(group);
|
|
installAdmission(cli);
|
|
expect((await serve(cli, ["group", "scalar", "one"])).exitCode).toBe(0);
|
|
expect((await serve(cli, ["group", "scalar", "one", "extra"])).exitCode).toBe(
|
|
1,
|
|
);
|
|
expect((await serve(cli, ["group", "alias", "one"])).exitCode).toBe(0);
|
|
expect((await serve(cli, ["group", "alias", "one", "extra"])).exitCode).toBe(
|
|
1,
|
|
);
|
|
const many = await serve(cli, ["group", "many", "one", "two", "three"]);
|
|
expect(many.exitCode).toBe(0);
|
|
expect(JSON.parse(many.stdout)).toMatchObject({
|
|
values: ["one", "two", "three"],
|
|
});
|
|
});
|
|
|
|
it("preserves counted aliases and negated booleans while checking the positional tail", async () => {
|
|
let runs = 0;
|
|
const cli = Cli.create("strict-counted-flags", { sync: false });
|
|
cli.command("capture", {
|
|
args: z.object({ path: z.string() }),
|
|
options: z.object({
|
|
enabled: z.boolean().default(true),
|
|
verbosity: z.number().default(0).meta({ count: true }),
|
|
}),
|
|
alias: { verbosity: "v" },
|
|
run: ({ args, options }) => {
|
|
runs++;
|
|
return { args, options };
|
|
},
|
|
});
|
|
installAdmission(cli);
|
|
const valid = await serve(cli, ["capture", "one", "--no-enabled", "-vvv"]);
|
|
expect(valid.exitCode).toBe(0);
|
|
expect(JSON.parse(valid.stdout)).toMatchObject({
|
|
args: { path: "one" },
|
|
options: { enabled: false, verbosity: 3 },
|
|
});
|
|
const surplus = await serve(cli, [
|
|
"capture",
|
|
"one",
|
|
"--no-enabled",
|
|
"-vvv",
|
|
"extra",
|
|
]);
|
|
expect(surplus.exitCode).toBe(1);
|
|
expect(surplus.stdout).toContain("Unexpected positional arguments");
|
|
expect(runs).toBe(1);
|
|
});
|
|
|
|
it("validates every dry-run literal before lossy coercion while retaining bare and negated controls", async () => {
|
|
let runs = 0;
|
|
let defaults = 0;
|
|
const cli = Cli.create("dry-run-fixture", { sync: false });
|
|
cli.command("setup", {
|
|
aliases: ["plan"],
|
|
options: z.object({
|
|
dryRun: z.boolean().default(false),
|
|
selected: z.string().default(() => {
|
|
defaults++;
|
|
return "default";
|
|
}),
|
|
}),
|
|
run: ({ options }) => {
|
|
runs++;
|
|
return options;
|
|
},
|
|
});
|
|
installAdmission(cli);
|
|
for (const literal of ["", "abc", "1", "True", "ab\ncd"]) {
|
|
const result = await serve(cli, [
|
|
"plan",
|
|
`--dry-run=${literal}`,
|
|
"--dryRun=false",
|
|
]);
|
|
expect(result.exitCode).toBe(1);
|
|
expect(JSON.parse(result.stdout)).toMatchObject({ code: "PARSE_ERROR" });
|
|
}
|
|
expect({ runs, defaults }).toEqual({ runs: 0, defaults: 0 });
|
|
for (const [tokens, expected] of [
|
|
[["--dry-run"], true],
|
|
[["--dryRun=true"], true],
|
|
[["--dry-run=false"], false],
|
|
[["--no-dry-run"], false],
|
|
[["--no-dryRun", "--dry-run"], true],
|
|
] as const) {
|
|
const result = await serve(cli, ["setup", ...tokens]);
|
|
expect(result.exitCode, result.stdout).toBe(0);
|
|
expect(JSON.parse(result.stdout)).toMatchObject({ dryRun: expected });
|
|
}
|
|
expect({ runs, defaults }).toEqual({ runs: 5, defaults: 5 });
|
|
});
|
|
|
|
it("keeps flag-shaped option data untouched while validating only bound dry-run values", async () => {
|
|
const cli = Cli.create("dry-run-data", { sync: false });
|
|
cli.command("setup", {
|
|
options: z.object({
|
|
dryRun: z.boolean().default(false),
|
|
selected: z.string(),
|
|
}),
|
|
alias: { selected: "s" },
|
|
run: ({ options }) => options,
|
|
});
|
|
cli.command("text", {
|
|
options: z.object({ dryRun: z.string() }),
|
|
run: ({ options }) => options,
|
|
});
|
|
installAdmission(cli);
|
|
for (const value of ["--dry-run=abc", "--dry-run", "--no-dryRun"]) {
|
|
const result = await serve(cli, ["setup", "-s", value, "--dry-run=true"]);
|
|
expect(result.exitCode, result.stdout).toBe(0);
|
|
expect(JSON.parse(result.stdout)).toEqual({
|
|
dryRun: true,
|
|
selected: value,
|
|
});
|
|
}
|
|
const text = await serve(cli, ["text", "--dry-run=abc"]);
|
|
expect(text.exitCode).toBe(0);
|
|
expect(JSON.parse(text.stdout)).toEqual({ dryRun: "abc" });
|
|
});
|
|
|
|
it("validates dry-run on variadic commands without losing their declared tail", async () => {
|
|
let runs = 0;
|
|
const cli = Cli.create("dry-run-tail", { sync: false });
|
|
cli.command("many", {
|
|
args: z.object({
|
|
values: z.array(z.string()),
|
|
}),
|
|
options: z.object({ dryRun: z.boolean().default(false) }),
|
|
run: ({ args, options }) => {
|
|
runs++;
|
|
return { args, options };
|
|
},
|
|
});
|
|
installAdmission(cli);
|
|
const rejected = await serve(cli, ["many", "one", "two", "--dry-run=abc"]);
|
|
expect(rejected.exitCode).toBe(1);
|
|
expect(runs).toBe(0);
|
|
const accepted = await serve(cli, ["many", "one", "two", "--no-dry-run"]);
|
|
expect(accepted.exitCode, accepted.stdout).toBe(0);
|
|
expect(JSON.parse(accepted.stdout)).toMatchObject({
|
|
args: { values: ["one", "two"] },
|
|
options: { dryRun: false },
|
|
});
|
|
expect(runs).toBe(1);
|
|
});
|