mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
9dde3fd6a4
* fix(artifacts): retain caller selection and applicable publication recovery * fix(artifacts): retain DMG producer failures and bound integrity recovery claims * fix(apk): classify malformed supported Zip64 records as input failures * fix(browser): compare script and tool facts without metadata identity churn * fix(environment): retain path segments counters and parent diagnostics * fix(discovery): retain Frida optional provider failure dispatch * test(native): share Swift context in vtable fixture admission * fix(native): narrow capability refusal before retaining its reason * fix(javascript): retain private receiver and iterative disposal effects * fix(native): preserve negative Mach-O admission identity * fix(javascript): bind Next page admission to source context * fix: preserve process filesystem scope and admission failures * fix(evidence): reject runtime locations for managed static facts * fix(hopper): bind ordinary executable opens to selected bytes * fix(browser): admit legacy capture uncertainty at retained input boundaries * fix(unknowns): distinguish rejected creation from committed mutation delivery * fix(evidence): preserve authentication failures and diagnose prior commitments * fix(cli): preserve explicit Evidence record pairing in bundle comparison * fix(artifacts): publish addressable inspection sources through both adapters * docs(evidence): define authentication and committed delivery recovery * fix(javascript): own reference roles and invocation result identities * fix(hopper): pass private X authority cookie through stdin * fix(ghidra): preserve catalog and metadata admission failures * fix(jeb): retain selected inputs and completed primary replies * fix(native): preserve literal environment map members * test: retain direct hash cleanup retainer contract * test: distinguish corrected port normalization commitment * fix: recheck attached worker policy after final artifacts * fix(javascript): share iteration and equality invocation facts * fix(artifacts): preserve typed resource limit diagnoses * fix(javascript): preserve semantic query identity and source coverage * docs(readme): align translated setup and analysis facts * fix(cli): reject surplus positional arguments before execution * fix(kuna): retain setting-specific doctor recovery * fix(setup): preserve semantic configuration refusal during uninstall * fix(setup): retain publication operation failures and recovery facts * fix(setup): distinguish Hopper inspection and archive staging failures * docs(managed): distinguish oracle conformance from host verification * docs(skill): keep one setup prerequisite policy * fix(firmware): preserve admission and extraction coverage outcomes * fix(contracts): preserve safe defaults and client literal guidance * fix(javascript): retain worker storage and projection failures * fix(evidence): preserve valid output paths near filesystem pathname limits * fix(cli): accept named native API selectors beginning with a dash * fix(artifacts): retain DMG carrier observations when child traversal is unavailable * fix(artifacts): retain dylib enumeration readers through failed cleanup * fix(browser): resolve empty storage-world origins from exact frame evidence * fix(browser): retain unknown popup storage initialization coverage * test(reconstruction): isolate derived surface ownership in fixtures * test(filesystem): probe actual permission denial before host assertions * test(managed): assert large issue retention without full-array diffs * fix(javascript): preserve source facts across graph projections * fix(javascript): preserve copied values and export publication lifetimes * fix(javascript): preserve per-field publication and provenance coverage * fix(javascript): retain initialized class self binding at body reads * fix(artifacts): retain permission omissions through JavaScript lookups * test(graph): narrow summary owner and complete PE fixture facts * test(javascript): supply source graph to native handoff checks * fix(sourcemaps): bind cleanup status to remaining codec owners * fix(browser): retain excluded source-map declaration coverage * test(javascript): preserve exact empty property keys in declarations * fix(browser): keep code absence unknown without worker inventory * fix(browser): preserve WebMCP optional fields and replay coverage gaps * fix(browser): publish executable examples with truthful capture coverage * fix(browser): preserve observed document invalidation and scope recovery * fix(native): share exact-name xref ambiguity resolution * fix(hopper): own bookmark API capability checks * fix(native): preserve procedure selector coordinate intent * test(discovery): select the declared GDB console contract * fix(ghidra): bind session refusal to retained cleanup owner * fix(apple): retain selected dylib identity after path removal * fix(apple): preserve asset failures and authored resource coverage * fix(apple): retain binary UID identity through archive projection * fix: retain completed historical replies through finalization * fix: retain unresolved browser secret declarations as input errors * test(browser): resolve network fixture secrets through typed input contract * fix(environment): distinguish destination refusal and write access failures * fix(artifacts): preserve exact ZIP and DMG caller cancellation reasons * fix(artifacts): require regular source-map target occurrences * fix(reference): separate local filesystem diagnostics from graph commitments * fix(setup): retain proven PATH aliases in installed registrations * test(package): join owned updater after registry fixture failure * fix(javascript): preserve recovery acquisition and cancellation failures * fix(session): retain cleanup blockers and snapshot request identity * fix(evidence): preserve format-owned JSON recovery diagnostics * test(evidence): cover BOM recovery across bundle commands * fix(tracing): retain caller reasons at traversal checkpoints * fix(inventory): require released cleanup before capability fallback * fix(hopper): drain retained publishers after current close failure * fix(native): preserve unknown Swift extension contexts * fix(apple): qualify unexamined nib content coverage * fix(frida): preserve session refusal and close execution effects * fix(rizin): retain cancellation and recover cleanup correlation * fix: retain Electron file admission facts across adapters * fix: preserve selected Electron display authorization * fix(javascript): retain unavailable HTML reference scopes * fix(javascript): preserve capture timing and detached receiver authority * fix(ida): preserve producer failure and caller abort outcomes * fix(cli): retain nested facts in Markdown tables * fix(cli): present argument guidance without validator cause dumps * docs(setup): define optional native update verification limits * docs(clients): distinguish discovery and schema recovery boundaries * docs(setup): explain npm workspace launcher failures * fix(verification): bind Copilot schema profile to observed inputs * fix(javascript): scope Next package admission by container identity * fix(apple): align asset allowance and category fixture types * fix(protocols): honor runtime queue and Frida identity types * test: parse Electron listing Evidence before comparison * fix(evidence): omit absent format recovery metadata * fix(contracts): validate historical source examples as exact JSON values * fix(javascript): consume per-field CommonJS publication facts * fix(gdb): correlate raw-output completion and retain read-only TTY proof * fix(native): retain completed LLDB facts across output-close cancellation * fix(hopper): retain failed private-display probe cleanup authority * style(lifecycle): format traversal and fallback edits * fix(apple): own ordered nib collections and registry parentage * fix(process): attribute trace omissions without changing shared successors * fix(cli): share builtin argument ownership with MCP routing * fix(cli): count emitted documents and normalize token windows * fix(inspector): retain captures through authorization cleanup failures * fix(artifacts): retain declared ASAR companion occurrence facts * fix(javascript): separate endpoint syntax correspondence from target identity * fix: distinguish native process exit from sampling failure * fix(javascript): retain selected-root access and filesystem failure facts * fix(browser): retain truthful cleanup facts and document request ownership * fix(native): share exact Objective-C class symbol interpretation * fix(go): retain completed artifact facts through finalization * fix(native): preserve seed analysis failures in value traces * fix(native): classify observed dSYM inspection refusal * test(apple): assert observed collection edge contract * fix(javascript): retain primary interruption through worker cleanup * fix(cutter): retain completion-read failures during cleanup retry * fix(cli): validate output windows and finish pending writes * fix(mcp): require absolute JavaScript application inputs * fix(capabilities): report missing navigation targets * fix(cli): support system Bash completion registration * fix(cli): retain narrowed token-count format handling * test(ida): preserve exact optional resource method shape * test(javascript): retain valid unknown endpoint coverage shape * fix(ghidra): try keg JDKs after failed PATH launchers * fix(browser): retain capacity refusal through terminal progress * fix(reference): require exact optional JSON diagnostics at the source schema * fix(apple): retain reader outcomes and cleanup ownership * fix(elf): qualify exports without dynamic symbol declarations * fix(mips): retain unexamined RELR declaration coverage * fix(metadata): preserve canonical PowerPC subject identity * fix(native): preserve LLDB discovery failure classifications * fix(native): report absent launch process before cleanup classification * fix(rizin): preserve primary outcomes across final cleanup * fix(browser): preserve admission failures and retain policy transport cleanup * fix(objdump): preserve input issues and provider launch failures * test(apple): attribute omitted edges to their node capacity limit * chore(metadata): synchronize packaged skill version * fix(cli): consume canonical output validation state * fix(views): preserve owning layout field diagnostics * test(pyinstaller): distinguish path and metadata allowance failures * fix(native): compose the selected command cause once * fix(setup): require HTTPS throughout Hopper download redirects * fix(ghidra): narrow fallback probe failures before projection * fix(native): require observed symbol origin for named identity * fix(javascript): retain unavailable source processing scopes * fix(cli): share declared output-error redaction * docs(windows): explain MCP registration launcher limits * fix(session): preserve binding commitments during restoration * fix(environment): retain primary facts through server cleanup failure * fix(pe): distinguish unsupported overlapping header layouts * fix(cli): keep positional admission placeholders omittable * test(cli): expose JavaScript progress workflow failure output * test(browser): match retained subframe limitations structurally * fix(javascript): preserve capture and property-key effect ownership * fix(ghidra): attribute import policy conflicts to settings * fix(native): validate Mach-O command framing before admission * docs(mcp): state plist inspection session prerequisite * fix(electron): separate script frame admission from declared source labels * test(environment): read asynchronous owner observations through contracts * test(javascript): distinguish resolver URLs from existing module targets * fix(ghidra): retain neutral recovery for observed SIGKILL * fix(hopper): preserve unknown relationship completeness * docs(native): clarify Darwin runtime breakpoint names * fix(browser): preserve unknown script ownership through reconciliation * fix(electron): keep optional provenance fields within JSON evidence values * fix(native): retain Mach-O architecture discriminant narrowing * test: give oversized registration groups semantic owners * test: group boundary registration by observed behavior * fix: normalize retained terminal observations with sampled identities * fix: identify missing Electron lineage ownership markers * fix(ida): retain registration constraints and identity refusal guidance * fix(kuna): retain published exports through staging cleanup failures * fix(nativeaot): charge only examined candidate table entries * docs: clarify batch decompilation retention limits * fix(javascript): distinguish deferred bootstrap installer writes * fix(native)!: preserve UI scalar facts and selection failures BREAKING CHANGE: Accessibility node value can also be a JSON number or boolean. Strings and null retain their prior meaning. * fix(javascript): retain activation order for captured scope exits * test(pyinstaller): use an absolute drive path for refusal coverage * fix(javascript): preserve route selection owners and uncertainty * fix(javascript): prove native URL constructor paths * refactor(tests): separate fixtures and registration groups * test(native): distinguish qualified symbol identity controls * fix(ida): preserve busy details across timeout attribution * fix(cli): unify builtin and global completion scope * test(ghidra): group exit-signal evidence controls * fix: retain narrowed terminal observation values * fix(cli): bind completion input to public registry type * fix(flutter): qualify unsupported unified snapshot profiles * test(native): bind origin comparison to containing entries * fix(cli): remove completion markers on system Bash * test(ida): preserve physical workspace traversal controls * fix(native): retain unknown UI executable identity * fix(native): unify LLDB discovery preparation failures * fix(evidence): preserve primary semantics during pending cleanup * fix(evidence): retain owner finalization reports across cleanup envelopes * test(browser): inspect provenance in owned graph observations * test(flutter): assert canonical unsupported profile error code * fix(javascript): capture primitive return values at evaluation * fix(mcp): own cancellation and raw input throughout startup * test(ghidra): construct exact import policy targets * fix(gdb): preserve configured executable launch recovery * test(ghidra): own fixed import target fixtures * docs(readme): align localized guide link occurrences * test(evidence): retain structured input issues through cleanup owners * test(mcp): await observed EOF before releasing startup * fix(electron): retain bounded source frame claims * fix(elf): validate declared file extents before provider admission * fix(elf): align metadata and admission fixture error types * test(wasm): place parsed interface comparison at provider boundary * test(ida): place capability composition at provider boundary * test(browser): place PNG contract execution at provider boundary * perf(test): share SDK catalog compilation across deferred clients * fix(cli): trim repeated selector discovery guidance * revert(test): remove unproven SDK compiler sharing This reverts commit 56eca648d. The equivalent completed two-client measurement increased elapsed and CPU time, so compiler sharing has not established a benefit. * refactor(managed): retain decoded metadata token facts * test(managed): scope cleanup failures to snapshot reads * test(cli): bound selector rejection cases by command * test(mips): distinguish damaged ELF admission from partial ABI evidence * test(reconstruction): declare authored function symbol identity * test(rizin): register terminal admission at provider boundary * test(process): align root admission fixture with observer allocation ownership * test(mips): project inspected ABI facts without undefined overrides * refactor(browser): remove obsolete web script reconciliation * refactor(javascript): remove obsolete in-process projection runner * refactor(managed): retain TypeDef row ownership through projections * test(process): assert canonical admission failure through cleanup envelopes * fix(managed): select types from retained definition records --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
716 lines
25 KiB
TypeScript
716 lines
25 KiB
TypeScript
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
import { Cli, z } from "incur";
|
|
import { resolve } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { createCli } from "../../../src/cli.js";
|
|
import { createCliInventory } from "../../../scripts/lib/catalog-cli.mjs";
|
|
import {
|
|
apktoolInputSchemas,
|
|
MAX_SELECTED_LOCALES,
|
|
} from "../../../src/domain/apktool/apktoolResourceAnalysis.js";
|
|
import { presentOmittableDefaults } from "../../../src/cli/omittableDefaults.js";
|
|
|
|
type CliInstance = ReturnType<typeof createCli>;
|
|
type CommandMap = NonNullable<ReturnType<typeof Cli.toCommands.get>>;
|
|
|
|
const INPUT_GROUPS = ["args", "options"] as const;
|
|
const registeredCommands: WeakMap<object, CommandMap> = Cli.toCommands;
|
|
|
|
const advertisedGroupSchema = z
|
|
.object({
|
|
properties: z.record(z.string(), z.unknown()),
|
|
required: z.array(z.string()).optional(),
|
|
})
|
|
.loose();
|
|
const advertisedInputSchema = z.object({
|
|
args: advertisedGroupSchema.optional(),
|
|
options: advertisedGroupSchema.optional(),
|
|
});
|
|
|
|
const serve = async (
|
|
cli: CliInstance,
|
|
argv: readonly string[],
|
|
): Promise<{ readonly stdout: string; readonly exitCode: number }> => {
|
|
let stdout = "";
|
|
let exitCode = 0;
|
|
await cli.serve([...argv], {
|
|
env: {},
|
|
exit: (code) => {
|
|
exitCode = code;
|
|
},
|
|
stdout: (text) => {
|
|
stdout += text;
|
|
},
|
|
});
|
|
return { stdout, exitCode };
|
|
};
|
|
|
|
const advertisedSchema = async (cli: CliInstance, command: string) => {
|
|
const result = await serve(cli, [command, "--schema", "--json"]);
|
|
expect(result.exitCode, command).toBe(0);
|
|
return advertisedInputSchema.parse(JSON.parse(result.stdout));
|
|
};
|
|
|
|
const parserSchema = (
|
|
cli: CliInstance,
|
|
command: string,
|
|
group: (typeof INPUT_GROUPS)[number],
|
|
): z.ZodObject | undefined => {
|
|
const entry = registeredCommands.get(cli)?.get(command);
|
|
if (entry === undefined || !(group in entry)) return undefined;
|
|
const schema: unknown = Reflect.get(entry, group);
|
|
return schema instanceof z.ZodObject ? schema : undefined;
|
|
};
|
|
|
|
/**
|
|
* Reject a key only when an object parse that omits it fails.
|
|
* `exactOptional()` rejects explicit `undefined` and still accepts omission.
|
|
*/
|
|
const omissionRejected = (schema: z.ZodObject | undefined): string[] =>
|
|
Object.entries(schema?.shape ?? {})
|
|
.filter(([key, field]) => !acceptsOmittedKey(key, field))
|
|
.map(([key]) => key)
|
|
.sort();
|
|
|
|
const acceptsOmittedKey = (key: string, field: z.ZodType): boolean =>
|
|
z.object({ [key]: field }).safeParse({}).success;
|
|
|
|
const registerParserAdvertisementInventoryTests = (): void => {
|
|
it("require exactly the inputs whose omission the parser rejects", async () => {
|
|
const cli = createCli({});
|
|
const mismatches: string[] = [];
|
|
for (const command of createCliInventory(cli).primary) {
|
|
const advertised = await advertisedSchema(cli, command);
|
|
for (const group of INPUT_GROUPS) {
|
|
const parser = parserSchema(cli, command, group);
|
|
const advertisedGroup = advertised[group];
|
|
if (parser === undefined) {
|
|
if (advertisedGroup !== undefined)
|
|
mismatches.push(`${command} ${group}: advertised without parser`);
|
|
continue;
|
|
}
|
|
if (advertisedGroup === undefined) {
|
|
mismatches.push(`${command} ${group}: parser group not advertised`);
|
|
continue;
|
|
}
|
|
const parserProperties = Object.keys(parser.shape).toSorted();
|
|
const advertisedProperties = Object.keys(
|
|
advertisedGroup.properties,
|
|
).toSorted();
|
|
if (advertisedProperties.join() !== parserProperties.join())
|
|
mismatches.push(
|
|
`${command} ${group}: advertised properties [${advertisedProperties.join()}], parser properties [${parserProperties.join()}]`,
|
|
);
|
|
const required = (advertisedGroup.required ?? []).toSorted();
|
|
const rejected = omissionRejected(parser);
|
|
if (required.join() !== rejected.join())
|
|
mismatches.push(
|
|
`${command} ${group}: advertised [${required.join()}], parser rejects [${rejected.join()}]`,
|
|
);
|
|
}
|
|
}
|
|
expect(mismatches).toEqual([]);
|
|
});
|
|
};
|
|
|
|
describe("CLI advertised input schemas", () => {
|
|
registerParserAdvertisementInventoryTests();
|
|
|
|
it("preserves client choices, selected path bounds and required decoded steps", async () => {
|
|
const cli = createCli({});
|
|
const ajv = new Ajv2020({ strict: false });
|
|
for (const command of ["setup", "doctor"]) {
|
|
const schema = await advertisedSchema(cli, command);
|
|
const validate = ajv.compile(schema.options ?? {});
|
|
for (const client of ["codex", "unsupported-client"]) {
|
|
const input = { client: [client] };
|
|
expect(validate(input)).toBe(client === "codex");
|
|
expect(
|
|
parserSchema(cli, command, "options")?.safeParse(input).success,
|
|
).toBe(client === "codex");
|
|
}
|
|
}
|
|
const setup = await advertisedSchema(cli, "setup");
|
|
const validateSkill = ajv.compile(setup.options ?? {});
|
|
for (const skillClient of ["shared", "codex", "unsupported-client"]) {
|
|
const input = { skillClient: [skillClient] };
|
|
expect(validateSkill(input)).toBe(skillClient !== "unsupported-client");
|
|
expect(
|
|
parserSchema(cli, "setup", "options")?.safeParse(input).success,
|
|
).toBe(skillClient !== "unsupported-client");
|
|
}
|
|
for (const [command, field] of [
|
|
["inspect-wasm-artifact", "glue_paths"],
|
|
["inspect-wasm-artifact", "candidate_paths"],
|
|
["recover-javascript-sources", "additionalPath"],
|
|
] as const) {
|
|
const schema = await advertisedSchema(cli, command);
|
|
const validate = ajv.compile(schema.options ?? {});
|
|
for (const count of [16, 17]) {
|
|
const input = {
|
|
[field]: Array.from({ length: count }, () => "./selected"),
|
|
};
|
|
expect(validate(input)).toBe(count === 16);
|
|
expect(
|
|
parserSchema(cli, command, "options")?.safeParse(input).success,
|
|
).toBe(count === 16);
|
|
}
|
|
}
|
|
const scenario = await advertisedSchema(cli, "capture-native-ui-scenario");
|
|
expect(scenario.options?.required).toContain("steps");
|
|
expect(scenario.options?.properties.steps).toMatchObject({
|
|
description: expect.stringContaining("one or more"),
|
|
});
|
|
const desktop = await advertisedSchema(cli, "inspect-desktop-application");
|
|
const validate = ajv.compile(desktop.args ?? {});
|
|
for (const path of ["", ".", "./selected"]) {
|
|
expect(validate({ path })).toBe(path !== "");
|
|
expect(
|
|
parserSchema(cli, "inspect-desktop-application", "args")?.safeParse({
|
|
path,
|
|
}).success,
|
|
).toBe(path !== "");
|
|
}
|
|
});
|
|
|
|
it("allows an omitted WASM timeout while validating explicitly supplied values", async () => {
|
|
const cli = createCli({});
|
|
const advertised = await advertisedSchema(cli, "inspect-wasm-artifact");
|
|
const options = parserSchema(cli, "inspect-wasm-artifact", "options");
|
|
expect(options).toBeDefined();
|
|
const validate = new Ajv2020({ strict: false }).compile(
|
|
advertised.options ?? {},
|
|
);
|
|
|
|
expect(options?.safeParse({}).success).toBe(true);
|
|
expect(validate({})).toBe(true);
|
|
expect(options?.safeParse({ commandTimeoutMs: undefined }).success).toBe(
|
|
false,
|
|
);
|
|
for (const commandTimeoutMs of [1, 30_000, 2_147_483_647]) {
|
|
expect(options?.safeParse({ commandTimeoutMs }).success).toBe(true);
|
|
expect(validate({ commandTimeoutMs })).toBe(true);
|
|
}
|
|
for (const commandTimeoutMs of [0, 1.5, 2_147_483_648]) {
|
|
expect(options?.safeParse({ commandTimeoutMs }).success).toBe(false);
|
|
expect(validate({ commandTimeoutMs })).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("accepts the one-argument keyed-archive call and keeps its constraints", async () => {
|
|
const cli = createCli({});
|
|
const advertised = await advertisedSchema(cli, "inspect-keyed-archive");
|
|
const ajv = new Ajv2020({ strict: false });
|
|
const args = ajv.compile(advertised.args ?? {});
|
|
const options = ajv.compile(advertised.options ?? {});
|
|
|
|
expect(args({ path: "./keyed.plist" })).toBe(true);
|
|
expect(args({})).toBe(false);
|
|
expect(options({})).toBe(true);
|
|
expect(options({ limit: 0 })).toBe(false);
|
|
expect(advertised.args?.properties?.["archive"]).toMatchObject({
|
|
default: ".",
|
|
});
|
|
expect(advertised.options?.properties?.["limit"]).toMatchObject({
|
|
default: 20_000,
|
|
minimum: 1,
|
|
});
|
|
expect(
|
|
parserSchema(cli, "inspect-keyed-archive", "args")?.parse({ path: "x" }),
|
|
).toEqual({ path: "x", archive: "." });
|
|
|
|
const help = await serve(cli, ["inspect-keyed-archive", "--help"]);
|
|
expect(help.stdout).toContain(
|
|
"Usage: rea inspect-keyed-archive <path> [archive] [options]",
|
|
);
|
|
expect(help.stdout).toContain(
|
|
"archive Relative archive path when the target is a bundle",
|
|
);
|
|
});
|
|
|
|
it("advertises defaults as omittable on a mounted group root", async () => {
|
|
const group = Cli.create("group", {
|
|
args: z.object({ scope: z.enum(["all", "selected"]).default("all") }),
|
|
run: () => ({ ok: true }),
|
|
}).command("child", { run: () => ({ ok: true }) });
|
|
const cli = Cli.create("group-root-test", { sync: false }).command(group);
|
|
|
|
presentOmittableDefaults(cli as unknown as CliInstance);
|
|
|
|
const advertised = await advertisedSchema(
|
|
cli as unknown as CliInstance,
|
|
"group",
|
|
);
|
|
expect(advertised.args?.required ?? []).not.toContain("scope");
|
|
});
|
|
});
|
|
|
|
describe("firmware CLI advertised input schema", () => {
|
|
it("keeps limits and defaults from its shared input contract", async () => {
|
|
const cli = createCli({});
|
|
const advertised = await advertisedSchema(cli, "extract-firmware");
|
|
const ajv = new Ajv2020({ strict: false });
|
|
const args = ajv.compile(advertised.args ?? {});
|
|
const options = ajv.compile(advertised.options ?? {});
|
|
|
|
expect(args({ path: "firmware.bin", outputDirectory: "output" })).toBe(
|
|
true,
|
|
);
|
|
expect(args({ path: "", outputDirectory: "output" })).toBe(false);
|
|
expect(args({ path: "firmware.bin", outputDirectory: "" })).toBe(false);
|
|
expect(options({})).toBe(true);
|
|
expect(options({ maxDepth: 0 })).toBe(false);
|
|
expect(options({ maxDepth: 11 })).toBe(false);
|
|
expect(options({ maxOutputBytes: 0 })).toBe(false);
|
|
expect(options({ maxOutputBytes: 512 * 1024 * 1024 + 1 })).toBe(false);
|
|
expect(options({ maxOutputFiles: 100_001 })).toBe(false);
|
|
expect(advertised.options?.properties?.["maxDepth"]).toMatchObject({
|
|
default: 3,
|
|
});
|
|
expect(advertised.options?.properties?.["maxOutputBytes"]).toMatchObject({
|
|
default: 256 * 1024 * 1024,
|
|
});
|
|
expect(advertised.options?.properties?.["maxOutputFiles"]).toMatchObject({
|
|
default: 10_000,
|
|
});
|
|
|
|
const maxOffset = Number.MAX_SAFE_INTEGER;
|
|
expect(options({ offset: maxOffset + 1 })).toBe(false);
|
|
expect(options({ length: maxOffset + 1 })).toBe(false);
|
|
|
|
expect(
|
|
parserSchema(cli, "extract-firmware", "options")?.parse({}),
|
|
).toMatchObject({
|
|
maxDepth: 3,
|
|
maxOutputBytes: 256 * 1024 * 1024,
|
|
maxOutputFiles: 10_000,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("web module CLI advertised input schema", () => {
|
|
it("advertises the shared path and URL constraints", async () => {
|
|
const cli = createCli({});
|
|
const advertised = await advertisedSchema(cli, "trace-web-module-imports");
|
|
const ajv = new Ajv2020({ strict: false });
|
|
const args = ajv.compile(advertised.args ?? {});
|
|
const options = ajv.compile(advertised.options ?? {});
|
|
|
|
expect(
|
|
args({ manifestPath: resolve("manifest.json"), scriptIndex: 0 }),
|
|
).toBe(true);
|
|
expect(args({ manifestPath: "", scriptIndex: 0 })).toBe(false);
|
|
expect(args({ manifestPath: "manifest\u0000.json", scriptIndex: 0 })).toBe(
|
|
false,
|
|
);
|
|
expect(args({ manifestPath: "./manifest.json", scriptIndex: -1 })).toBe(
|
|
false,
|
|
);
|
|
expect(options({})).toBe(true);
|
|
expect(options({ importerUrl: "" })).toBe(false);
|
|
expect(options({ importMapPath: "" })).toBe(false);
|
|
expect(options({ importMapPath: "map\u0000.json" })).toBe(false);
|
|
expect(options({ importMapBaseUrl: "" })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("Apktool locale selection input schema", () => {
|
|
it("preserves the owner's array bounds through CLI advertisement and parsing", async () => {
|
|
const cli = createCli({});
|
|
const advertised = await advertisedSchema(cli, "decode-android-resources");
|
|
expect(advertised.options?.properties?.locales).toMatchObject({
|
|
type: "array",
|
|
minItems: 1,
|
|
maxItems: MAX_SELECTED_LOCALES,
|
|
});
|
|
const parser = parserSchema(cli, "decode-android-resources", "options");
|
|
const ajv = new Ajv2020({ strict: false });
|
|
const validate = ajv.compile({
|
|
...advertised.options,
|
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
});
|
|
for (const locales of [
|
|
undefined,
|
|
[],
|
|
["de"],
|
|
Array<string>(MAX_SELECTED_LOCALES).fill("de"),
|
|
Array<string>(MAX_SELECTED_LOCALES + 1).fill("de"),
|
|
["not a locale"],
|
|
]) {
|
|
const input = locales === undefined ? {} : { locales };
|
|
const expected = apktoolInputSchemas.decode_android_resources.safeParse({
|
|
path: "/targets/app.apk",
|
|
...input,
|
|
}).success;
|
|
expect(parser?.safeParse(input).success).toBe(expected);
|
|
expect(validate(input)).toBe(expected);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("reference-source outline-depth input schema", () => {
|
|
it("aligns advertised bounds with the parser's accepted values", async () => {
|
|
const cli = createCli({});
|
|
const advertised = await advertisedSchema(cli, "import-reference-source");
|
|
const outlineDepth = advertised.options?.properties?.outlineDepth;
|
|
expect(outlineDepth).toMatchObject({ type: "integer", minimum: 0 });
|
|
|
|
const parser = parserSchema(cli, "import-reference-source", "options");
|
|
expect(parser?.safeParse({ outlineDepth: -1 }).success).toBe(false);
|
|
expect(parser?.safeParse({ outlineDepth: 0 }).success).toBe(true);
|
|
expect(parser?.safeParse({ outlineDepth: 2 }).success).toBe(true);
|
|
|
|
const ajv = new Ajv2020({ strict: false });
|
|
const validate = ajv.compile({
|
|
...advertised.options,
|
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
});
|
|
expect(validate({ outlineDepth: -1 })).toBe(false);
|
|
expect(validate({ outlineDepth: 0 })).toBe(true);
|
|
expect(validate({ outlineDepth: 2 })).toBe(true);
|
|
});
|
|
});
|
|
|
|
const atLeastOneSelector = (alternatives: string): string =>
|
|
`Supply at least one of ${alternatives}.`;
|
|
const ADDRESS_OR_PROCEDURE = atLeastOneSelector(
|
|
"the positional address or --procedure",
|
|
);
|
|
const DECOMPILE_SELECTORS = `${ADDRESS_OR_PROCEDURE} Alternatively, use --file-offset alone.`;
|
|
const NAMED_ADDRESS = atLeastOneSelector("the positional address or --address");
|
|
const QUERY_SELECTOR = atLeastOneSelector("the positional query or --query");
|
|
const PATTERN_SELECTOR = atLeastOneSelector(
|
|
"the positional pattern or --pattern",
|
|
);
|
|
const EXACTLY_ONE_SCRIPT = "Provide exactly one of --script or --script-file.";
|
|
const TARGET = "target.bin";
|
|
|
|
interface SelectorRequest {
|
|
readonly args: Record<string, unknown>;
|
|
readonly options: Record<string, unknown>;
|
|
}
|
|
|
|
interface SelectorField {
|
|
readonly group: "args" | "options";
|
|
readonly name: string;
|
|
readonly preserved?: readonly string[];
|
|
}
|
|
|
|
interface SelectorDiscovery {
|
|
readonly command: string;
|
|
readonly guidance: string;
|
|
readonly choice: "at least one" | "exactly one";
|
|
readonly fields: readonly SelectorField[];
|
|
/** Each side is valid alone, including when the other group omits its selector. */
|
|
readonly representable: readonly SelectorRequest[];
|
|
/** Cross-group omission stays valid because the schemas are separate. */
|
|
readonly omitted: SelectorRequest;
|
|
readonly rejected: readonly {
|
|
readonly args?: Record<string, unknown>;
|
|
readonly options?: Record<string, unknown>;
|
|
}[];
|
|
}
|
|
|
|
const selectorDiscovery: readonly SelectorDiscovery[] = [
|
|
{
|
|
command: "inspect-native-api",
|
|
guidance: ADDRESS_OR_PROCEDURE,
|
|
choice: "at least one",
|
|
fields: [
|
|
{ group: "args", name: "address" },
|
|
{
|
|
group: "options",
|
|
name: "procedure",
|
|
preserved: ["symbols beginning with a dash"],
|
|
},
|
|
],
|
|
representable: [
|
|
{ args: { path: TARGET, address: "main" }, options: {} },
|
|
{ args: { path: TARGET }, options: { procedure: "--help" } },
|
|
{
|
|
args: { path: TARGET, address: "main" },
|
|
options: { procedure: "main" },
|
|
},
|
|
],
|
|
omitted: { args: { path: TARGET }, options: {} },
|
|
rejected: [
|
|
{ args: { path: TARGET, address: 1 } },
|
|
{ options: { procedure: 1 } },
|
|
],
|
|
},
|
|
{
|
|
command: "function",
|
|
guidance: ADDRESS_OR_PROCEDURE,
|
|
choice: "at least one",
|
|
fields: [
|
|
{ group: "args", name: "address" },
|
|
{
|
|
group: "options",
|
|
name: "procedure",
|
|
preserved: ["use --procedure=<value> for leading dashes"],
|
|
},
|
|
],
|
|
representable: [
|
|
{ args: { path: TARGET, address: "main" }, options: {} },
|
|
{ args: { path: TARGET }, options: { procedure: "--help" } },
|
|
{
|
|
args: { path: TARGET, address: "main" },
|
|
options: { procedure: "main" },
|
|
},
|
|
],
|
|
omitted: { args: { path: TARGET }, options: {} },
|
|
rejected: [
|
|
{ args: { path: TARGET, address: "" } },
|
|
{ options: { procedure: "" } },
|
|
],
|
|
},
|
|
{
|
|
command: "instructions",
|
|
guidance: ADDRESS_OR_PROCEDURE,
|
|
choice: "at least one",
|
|
fields: [
|
|
{ group: "args", name: "address" },
|
|
{
|
|
group: "options",
|
|
name: "procedure",
|
|
preserved: ["use --procedure=<value> for leading dashes"],
|
|
},
|
|
],
|
|
representable: [
|
|
{ args: { path: TARGET, address: "main" }, options: {} },
|
|
{ args: { path: TARGET }, options: { procedure: "--help" } },
|
|
{
|
|
args: { path: TARGET, address: "main" },
|
|
options: { procedure: "main" },
|
|
},
|
|
],
|
|
omitted: { args: { path: TARGET }, options: {} },
|
|
rejected: [
|
|
{ args: { path: TARGET, address: "" } },
|
|
{ options: { procedure: "" } },
|
|
],
|
|
},
|
|
{
|
|
command: "decompile",
|
|
guidance: DECOMPILE_SELECTORS,
|
|
choice: "at least one",
|
|
fields: [
|
|
{ group: "args", name: "address" },
|
|
{
|
|
group: "options",
|
|
name: "procedure",
|
|
preserved: ["use --procedure=<value> for leading dashes"],
|
|
},
|
|
{
|
|
group: "options",
|
|
name: "file-offset",
|
|
preserved: [
|
|
"Ghidra source-file byte offset",
|
|
"mapped internally to an analysis address",
|
|
],
|
|
},
|
|
],
|
|
representable: [
|
|
{ args: { path: TARGET, address: "0x140001010" }, options: {} },
|
|
{ args: { path: TARGET }, options: { procedure: "--help" } },
|
|
{ args: { path: TARGET }, options: { "file-offset": "0x210" } },
|
|
{
|
|
args: { path: TARGET, address: "0x140001010" },
|
|
options: { procedure: "0x140001010" },
|
|
},
|
|
],
|
|
omitted: { args: { path: TARGET }, options: {} },
|
|
rejected: [
|
|
{ args: { path: TARGET, address: "" } },
|
|
{ options: { procedure: "" } },
|
|
{ options: { "file-offset": "210" } },
|
|
{ options: { "file-offset": "0x" } },
|
|
],
|
|
},
|
|
{
|
|
command: "xrefs",
|
|
guidance: NAMED_ADDRESS,
|
|
choice: "at least one",
|
|
fields: [
|
|
{ group: "args", name: "address" },
|
|
{
|
|
group: "options",
|
|
name: "address",
|
|
preserved: ["use --address=<value> for leading dashes"],
|
|
},
|
|
],
|
|
representable: [
|
|
{ args: { path: TARGET, address: "main" }, options: {} },
|
|
{ args: { path: TARGET }, options: { address: "--help" } },
|
|
{
|
|
args: { path: TARGET, address: "main" },
|
|
options: { address: "main" },
|
|
},
|
|
],
|
|
omitted: { args: { path: TARGET }, options: {} },
|
|
rejected: [
|
|
{ args: { path: TARGET, address: "" } },
|
|
{ options: { address: "" } },
|
|
],
|
|
},
|
|
{
|
|
command: "trace",
|
|
guidance: QUERY_SELECTOR,
|
|
choice: "at least one",
|
|
fields: [
|
|
{ group: "args", name: "query" },
|
|
{
|
|
group: "options",
|
|
name: "query",
|
|
preserved: ["use --query=<value> for leading dashes"],
|
|
},
|
|
],
|
|
representable: [
|
|
{ args: { path: TARGET, query: "feature" }, options: {} },
|
|
{ args: { path: TARGET }, options: { query: "--help" } },
|
|
{
|
|
args: { path: TARGET, query: "feature" },
|
|
options: { query: "feature" },
|
|
},
|
|
],
|
|
omitted: { args: { path: TARGET }, options: {} },
|
|
rejected: [
|
|
{ args: { path: TARGET, query: "" } },
|
|
{ options: { query: "" } },
|
|
],
|
|
},
|
|
{
|
|
command: "search",
|
|
guidance: PATTERN_SELECTOR,
|
|
choice: "at least one",
|
|
fields: [
|
|
{ group: "args", name: "pattern" },
|
|
{
|
|
group: "options",
|
|
name: "pattern",
|
|
preserved: ["use --pattern=<value> for leading dashes"],
|
|
},
|
|
],
|
|
representable: [
|
|
{ args: { path: TARGET, pattern: "text" }, options: {} },
|
|
{ args: { path: TARGET }, options: { pattern: "--help" } },
|
|
{
|
|
args: { path: TARGET, pattern: "text" },
|
|
options: { pattern: "text" },
|
|
},
|
|
],
|
|
omitted: { args: { path: TARGET }, options: {} },
|
|
rejected: [
|
|
{ args: { path: TARGET, pattern: "" } },
|
|
{ options: { pattern: "" } },
|
|
],
|
|
},
|
|
{
|
|
command: "instrument-with-frida",
|
|
guidance: EXACTLY_ONE_SCRIPT,
|
|
choice: "exactly one",
|
|
fields: [
|
|
{ group: "options", name: "script" },
|
|
{ group: "options", name: "scriptFile" },
|
|
],
|
|
representable: [
|
|
{
|
|
args: { mode: "attach", target: "71" },
|
|
options: { script: "send(1)" },
|
|
},
|
|
{
|
|
args: { mode: "attach", target: "71" },
|
|
options: { scriptFile: "source.js" },
|
|
},
|
|
{
|
|
args: { mode: "attach", target: "71" },
|
|
options: { script: "send(1)", scriptFile: "source.js" },
|
|
},
|
|
],
|
|
omitted: { args: { mode: "attach", target: "71" }, options: {} },
|
|
rejected: [
|
|
{ args: { mode: "attach", target: "" } },
|
|
{ args: { mode: "debug", target: "71" } },
|
|
{ options: { scriptFile: "" } },
|
|
],
|
|
},
|
|
];
|
|
|
|
const propertyDescription = (property: unknown, label: string): string => {
|
|
expect(property, label).toMatchObject({
|
|
description: expect.any(String),
|
|
});
|
|
if (
|
|
typeof property !== "object" ||
|
|
property === null ||
|
|
!("description" in property) ||
|
|
typeof property.description !== "string"
|
|
)
|
|
throw new Error(`${label} description was not on the schema property`);
|
|
return property.description;
|
|
};
|
|
|
|
describe("required CLI selector discovery", () => {
|
|
it.each(selectorDiscovery)(
|
|
"$command names every permitted selector alternative in schema, help, and the full LLM manifest",
|
|
async (entry) => {
|
|
const cli = createCli({});
|
|
const advertised = await advertisedSchema(cli, entry.command);
|
|
const otherChoice =
|
|
entry.choice === "at least one" ? "exactly one" : "at least one";
|
|
for (const field of entry.fields) {
|
|
const label = `${entry.command} ${field.group}.${field.name}`;
|
|
const description = propertyDescription(
|
|
advertised[field.group]?.properties[field.name],
|
|
label,
|
|
);
|
|
expect(description, label).toContain(entry.guidance);
|
|
expect(description, label).toContain(entry.choice);
|
|
expect(description, label).not.toContain(otherChoice);
|
|
for (const preserved of field.preserved ?? [])
|
|
expect(description, label).toContain(preserved);
|
|
expect(advertised[field.group]?.required ?? [], label).not.toContain(
|
|
field.name,
|
|
);
|
|
}
|
|
|
|
const help = await serve(cli, [entry.command, "--help"]);
|
|
const manifest = await serve(cli, [entry.command, "--llms-full"]);
|
|
expect(help.exitCode, entry.command).toBe(0);
|
|
expect(manifest.exitCode, entry.command).toBe(0);
|
|
for (const surface of [help.stdout, manifest.stdout]) {
|
|
expect(surface).toContain(entry.guidance);
|
|
expect(surface).toContain(entry.choice);
|
|
expect(surface).not.toContain(otherChoice);
|
|
for (const field of entry.fields)
|
|
for (const preserved of field.preserved ?? [])
|
|
expect(surface).toContain(preserved);
|
|
}
|
|
|
|
const ajv = new Ajv2020({ strict: false });
|
|
expect(advertised.args, entry.command).toBeDefined();
|
|
expect(advertised.options, entry.command).toBeDefined();
|
|
const validateArgs = ajv.compile(advertised.args ?? {});
|
|
const validateOptions = ajv.compile(advertised.options ?? {});
|
|
expect(validateArgs({}), entry.command).toBe(false);
|
|
for (const request of [...entry.representable, entry.omitted]) {
|
|
expect(
|
|
validateArgs(request.args),
|
|
`${entry.command} args ${JSON.stringify(validateArgs.errors)}`,
|
|
).toBe(true);
|
|
expect(
|
|
validateOptions(request.options),
|
|
`${entry.command} options ${JSON.stringify(validateOptions.errors)}`,
|
|
).toBe(true);
|
|
}
|
|
for (const request of entry.rejected) {
|
|
if (request.args !== undefined)
|
|
expect(validateArgs(request.args), entry.command).toBe(false);
|
|
if (request.options !== undefined)
|
|
expect(validateOptions(request.options), entry.command).toBe(false);
|
|
}
|
|
},
|
|
);
|
|
});
|