2026-10-17 00:15:58 +08:00
|
|
|
import { writeFile } from "node:fs/promises";
|
|
|
|
|
import { join } from "node:path";
|
|
|
|
|
|
|
|
|
|
import { expect } from "vitest";
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
ANDROID_APPLICATION_GRAPH_EXAMPLE,
|
|
|
|
|
APPLE_APPLICATION_GRAPH_EXAMPLE,
|
|
|
|
|
HARMONY_APPLICATION_GRAPH_EXAMPLE,
|
|
|
|
|
MACOS_APPLICATION_GRAPH_EXAMPLE,
|
|
|
|
|
} from "../../../src/contracts/mobileApplicationGraphExamples.js";
|
|
|
|
|
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
|
|
|
|
import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js";
|
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
|
|
2026-10-23 06:40:25 +08:00
|
|
|
const graphExamples = [
|
2026-10-17 00:15:58 +08:00
|
|
|
{
|
|
|
|
|
name: "Android",
|
|
|
|
|
command: "project-android-application-graph",
|
|
|
|
|
operation: "project_android_application_graph",
|
|
|
|
|
example: ANDROID_APPLICATION_GRAPH_EXAMPLE,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "IPA",
|
|
|
|
|
command: "project-apple-application-graph",
|
|
|
|
|
operation: "project_apple_application_graph",
|
|
|
|
|
example: APPLE_APPLICATION_GRAPH_EXAMPLE,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "macOS",
|
|
|
|
|
command: "project-apple-application-graph",
|
|
|
|
|
operation: "project_apple_application_graph",
|
|
|
|
|
example: MACOS_APPLICATION_GRAPH_EXAMPLE,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "Harmony",
|
|
|
|
|
command: "project-harmony-application-graph",
|
|
|
|
|
operation: "project_harmony_application_graph",
|
|
|
|
|
example: HARMONY_APPLICATION_GRAPH_EXAMPLE,
|
|
|
|
|
},
|
2026-10-23 06:40:25 +08:00
|
|
|
] as const;
|
|
|
|
|
|
|
|
|
|
cliTest.for(graphExamples)(
|
2026-10-17 00:15:58 +08:00
|
|
|
"CLI executes the canonical $name graph example without target files",
|
|
|
|
|
async ({ command, operation, example }, { cli }) => {
|
|
|
|
|
const root = await createTestTempDirectory("rea-mobile-graph-example-");
|
|
|
|
|
const input = JSON.stringify(example);
|
|
|
|
|
const path = join(root, "example.json");
|
|
|
|
|
await writeFile(path, input);
|
|
|
|
|
for (const argument of [input, path]) {
|
|
|
|
|
const result = await cli.run({
|
|
|
|
|
arguments: [command, argument, "--json"],
|
|
|
|
|
cwd: root,
|
|
|
|
|
});
|
|
|
|
|
expect(result.exitCode, result.stdout).toBe(0);
|
|
|
|
|
const evidence = parseEvidence(result.json);
|
|
|
|
|
expect(evidence.operation).toBe(operation);
|
|
|
|
|
expect(evidence.normalized_result).toMatchObject({
|
|
|
|
|
coverage: { status: "partial", inventory_complete: true },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2026-10-23 06:40:25 +08:00
|
|
|
cliTest.for(graphExamples)(
|
|
|
|
|
"CLI classifies malformed $name source Evidence separately from request errors",
|
|
|
|
|
async ({ command, example }, { cli }) => {
|
|
|
|
|
const source = example.inventory_evidence[0]!;
|
|
|
|
|
for (const { input, code } of [
|
|
|
|
|
{
|
|
|
|
|
input: {
|
|
|
|
|
inventory_evidence: [{ ...source, evidence_id: "not-a-digest" }],
|
|
|
|
|
},
|
|
|
|
|
code: "evidence_integrity_mismatch",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
input: { inventory_evidence: [source], unexpected: true },
|
|
|
|
|
code: "invalid_request",
|
|
|
|
|
},
|
|
|
|
|
{ input: { inventory_evidence: [] }, code: "invalid_request" },
|
|
|
|
|
]) {
|
|
|
|
|
const result = await cli.run({
|
|
|
|
|
arguments: [command, JSON.stringify(input), "--json"],
|
|
|
|
|
});
|
|
|
|
|
expect(result.exitCode).toBe(1);
|
|
|
|
|
expect(result.json).toMatchObject({
|
|
|
|
|
code,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2026-10-17 00:15:58 +08:00
|
|
|
cliTest(
|
|
|
|
|
"CLI still rejects an unsupported inventory coverage status",
|
|
|
|
|
async ({ cli }) => {
|
|
|
|
|
const original = ANDROID_APPLICATION_GRAPH_EXAMPLE.inventory_evidence[0]!;
|
|
|
|
|
const inventory = artifactInventoryResultSchema.parse(
|
|
|
|
|
original.normalized_result,
|
|
|
|
|
);
|
|
|
|
|
const evidence = createEvidence(
|
|
|
|
|
{ path: "Fixture.apk", sha256: "a".repeat(64), format: "apk" },
|
|
|
|
|
original.provider,
|
|
|
|
|
{
|
|
|
|
|
operation: "inventory_artifact",
|
|
|
|
|
parameters: {},
|
|
|
|
|
result: {
|
|
|
|
|
...inventory,
|
|
|
|
|
coverage: { scope: "root", status: "invalid" },
|
|
|
|
|
},
|
|
|
|
|
confidence: "observed",
|
|
|
|
|
authority: "shipped-artifact",
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
const result = await cli.run({
|
|
|
|
|
arguments: [
|
|
|
|
|
"project-android-application-graph",
|
|
|
|
|
JSON.stringify({ inventory_evidence: [evidence] }),
|
|
|
|
|
"--json",
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
expect(result.exitCode).toBe(1);
|
2026-10-23 04:47:39 +08:00
|
|
|
const projected = result.json;
|
|
|
|
|
expect(projected).toMatchObject({
|
|
|
|
|
code: "evidence_integrity_mismatch",
|
|
|
|
|
category: "integrity_mismatch",
|
|
|
|
|
});
|
|
|
|
|
const reason = inventoryIntegrityReason(projected);
|
|
|
|
|
expect(JSON.parse(reason)).toMatchObject([
|
|
|
|
|
{
|
|
|
|
|
code: "invalid_value",
|
|
|
|
|
values: ["complete", "unknown"],
|
|
|
|
|
path: ["coverage", "status"],
|
2026-10-17 00:15:58 +08:00
|
|
|
},
|
2026-10-23 04:47:39 +08:00
|
|
|
]);
|
|
|
|
|
expect(projected).toMatchObject({
|
|
|
|
|
message: reason,
|
|
|
|
|
details: { reason },
|
2026-10-17 00:15:58 +08:00
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
2026-10-23 04:47:39 +08:00
|
|
|
|
|
|
|
|
const inventoryIntegrityReason = (projected: unknown): string => {
|
|
|
|
|
if (
|
|
|
|
|
projected === null ||
|
|
|
|
|
typeof projected !== "object" ||
|
|
|
|
|
!("details" in projected) ||
|
|
|
|
|
projected.details === null ||
|
|
|
|
|
typeof projected.details !== "object" ||
|
|
|
|
|
!("reason" in projected.details) ||
|
|
|
|
|
typeof projected.details.reason !== "string"
|
|
|
|
|
)
|
|
|
|
|
throw new Error("Inventory rejection omitted an integrity reason");
|
|
|
|
|
return projected.details.reason;
|
|
|
|
|
};
|