Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f6f1600c5 | ||
|
|
092a303b0f |
No files matched your search
@@ -13,6 +13,8 @@ Prioritize:
|
|||||||
|
|
||||||
Installers must not install or upgrade Homebrew, Node.js, npm, or other unrelated software. `rea setup` must print its planned changes and require approval before writing files or installing Hopper.
|
Installers must not install or upgrade Homebrew, Node.js, npm, or other unrelated software. `rea setup` must print its planned changes and require approval before writing files or installing Hopper.
|
||||||
|
|
||||||
|
REA is a local-only tool; do not sanitize actionable local diagnostics such as artifact paths, digests, mismatch locations, or analysis metadata, while continuing to redact genuine secrets such as credentials and authorization headers.
|
||||||
|
|
||||||
## Project Structure & Module Organization
|
## Project Structure & Module Organization
|
||||||
|
|
||||||
REA is a layered ESM TypeScript application. Dependencies flow inward: `domain` ← `contracts` ← `hopper` ← `application` ← `server` ← adapters.
|
REA is a layered ESM TypeScript application. Dependencies flow inward: `domain` ← `contracts` ← `hopper` ← `application` ← `server` ← adapters.
|
||||||
|
|||||||
@@ -634,10 +634,17 @@ def _dispatch(method, params):
|
|||||||
return {"shutdown": True, "analysis_stopped": True, "document_closed": True}
|
return {"shutdown": True, "analysis_stopped": True, "document_closed": True}
|
||||||
if document.backgroundProcessActive():
|
if document.backgroundProcessActive():
|
||||||
document.requestBackgroundProcessStop()
|
document.requestBackgroundProcessStop()
|
||||||
|
if REA_OWNS_PROCESS_LIFETIME:
|
||||||
|
return {
|
||||||
|
"shutdown": True,
|
||||||
|
"analysis_stopped": not document.backgroundProcessActive(),
|
||||||
|
"document_closed": False,
|
||||||
|
"cleanup_required": True,
|
||||||
|
}
|
||||||
if document.backgroundProcessActive():
|
if document.backgroundProcessActive():
|
||||||
document.waitForBackgroundProcessToEnd()
|
document.waitForBackgroundProcessToEnd()
|
||||||
analysis_stopped = not document.backgroundProcessActive()
|
|
||||||
document.closeDocument()
|
document.closeDocument()
|
||||||
|
analysis_stopped = not document.backgroundProcessActive()
|
||||||
document_closed = _session_document() is None
|
document_closed = _session_document() is None
|
||||||
return {
|
return {
|
||||||
"shutdown": True,
|
"shutdown": True,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Cli, z } from "incur";
|
|||||||
import { runCrossVersionInvestigation } from "./application/CrossVersionInvestigation.js";
|
import { runCrossVersionInvestigation } from "./application/CrossVersionInvestigation.js";
|
||||||
import { logCliCommand } from "./cliLogging.js";
|
import { logCliCommand } from "./cliLogging.js";
|
||||||
import { parseConfig } from "./config.js";
|
import { parseConfig } from "./config.js";
|
||||||
|
import { projectAnalysisError, type AnalysisError } from "./domain/errors.js";
|
||||||
import type { Logger } from "./logger.js";
|
import type { Logger } from "./logger.js";
|
||||||
|
|
||||||
const investigationOptionsSchema = z.object({
|
const investigationOptionsSchema = z.object({
|
||||||
@@ -74,8 +75,7 @@ const runInvestigation = async (
|
|||||||
message: "Persistent workspace writes require explicit --yes approval.",
|
message: "Persistent workspace writes require explicit --yes approval.",
|
||||||
};
|
};
|
||||||
const config = parseConfig(process.env);
|
const config = parseConfig(process.env);
|
||||||
if (!config.ok)
|
if (!config.ok) return cliError(config.error);
|
||||||
return { error: config.error._tag, message: config.error.message };
|
|
||||||
const investigated = await runCrossVersionInvestigation(
|
const investigated = await runCrossVersionInvestigation(
|
||||||
{
|
{
|
||||||
approved: true,
|
approved: true,
|
||||||
@@ -102,8 +102,10 @@ const runInvestigation = async (
|
|||||||
);
|
);
|
||||||
return investigated.ok
|
return investigated.ok
|
||||||
? investigated.value.evidence
|
? investigated.value.evidence
|
||||||
: {
|
: cliError(investigated.error);
|
||||||
error: investigated.error._tag,
|
|
||||||
message: investigated.error.message,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cliError = (error: AnalysisError) => ({
|
||||||
|
error: "Analysis failed",
|
||||||
|
...projectAnalysisError(error),
|
||||||
|
});
|
||||||
@@ -191,6 +191,15 @@ const analysisErrorProjectionSchema = z
|
|||||||
"execution_failure",
|
"execution_failure",
|
||||||
]),
|
]),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
details: z
|
||||||
|
.object({
|
||||||
|
logical_path: z.string(),
|
||||||
|
declared_sha256: z.string().nullable(),
|
||||||
|
calculated_sha256: z.string().nullable(),
|
||||||
|
unpacked: z.boolean(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.optional(),
|
||||||
code: z
|
code: z
|
||||||
.enum([
|
.enum([
|
||||||
"private_display_unavailable",
|
"private_display_unavailable",
|
||||||
|
|||||||
@@ -349,6 +349,7 @@ export interface AnalysisErrorProjection
|
|||||||
| "execution_failure";
|
| "execution_failure";
|
||||||
readonly message: string;
|
readonly message: string;
|
||||||
readonly code?: HopperStartupFailureCode;
|
readonly code?: HopperStartupFailureCode;
|
||||||
|
readonly details?: Readonly<Record<string, JsonValue>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Project expected failures into exhaustive, secret-safe caller fields. */
|
/** Project expected failures into exhaustive, secret-safe caller fields. */
|
||||||
@@ -358,10 +359,22 @@ export const projectAnalysisError = (
|
|||||||
assertKnownTag(error._tag);
|
assertKnownTag(error._tag);
|
||||||
const code =
|
const code =
|
||||||
error instanceof HopperProcessError ? error.failureCode : undefined;
|
error instanceof HopperProcessError ? error.failureCode : undefined;
|
||||||
|
const details =
|
||||||
|
error instanceof ArtifactOperationError ? error.artifactDetails : undefined;
|
||||||
return {
|
return {
|
||||||
category: errorCategory(error),
|
category: errorCategory(error),
|
||||||
message: userMessage(error),
|
message: userMessage(error),
|
||||||
...(code === undefined ? {} : { code }),
|
...(code === undefined ? {} : { code }),
|
||||||
|
...(details === undefined
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
details: {
|
||||||
|
logical_path: details.logicalPath,
|
||||||
|
declared_sha256: details.declaredSha256,
|
||||||
|
calculated_sha256: details.calculatedSha256,
|
||||||
|
unpacked: details.unpacked,
|
||||||
|
},
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export interface BridgeSession {
|
|||||||
export interface BridgeLaunch {
|
export interface BridgeLaunch {
|
||||||
readonly process: ChildProcess;
|
readonly process: ChildProcess;
|
||||||
readonly ownsProcessLifetime: boolean;
|
readonly ownsProcessLifetime: boolean;
|
||||||
|
/** True when verified process cleanup completes bridge shutdown. */
|
||||||
|
readonly shutdownByCleanup?: boolean;
|
||||||
readonly cleanup?: () => Promise<ProcessCleanupResult>;
|
readonly cleanup?: () => Promise<ProcessCleanupResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,11 +71,13 @@ export class HopperApplicationLauncher implements BridgeLauncher {
|
|||||||
options: { readonly signal?: AbortSignal } = {},
|
options: { readonly signal?: AbortSignal } = {},
|
||||||
): Promise<Result<BridgeLaunch, HopperStartError | HopperCancelledError>> {
|
): Promise<Result<BridgeLaunch, HopperStartError | HopperCancelledError>> {
|
||||||
const bootstrapPath = `${session.directory}/bootstrap.py`;
|
const bootstrapPath = `${session.directory}/bootstrap.py`;
|
||||||
|
const ownsProcessLifetime = usesLinuxDemo(this.options);
|
||||||
const source = [
|
const source = [
|
||||||
`REA_SOCKET = ${JSON.stringify(session.socketPath)}`,
|
`REA_SOCKET = ${JSON.stringify(session.socketPath)}`,
|
||||||
`REA_TOKEN = ${JSON.stringify(session.token)}`,
|
`REA_TOKEN = ${JSON.stringify(session.token)}`,
|
||||||
`REA_RUN_ID = ${JSON.stringify(session.runId)}`,
|
`REA_RUN_ID = ${JSON.stringify(session.runId)}`,
|
||||||
`REA_TARGET_PATH = ${JSON.stringify(this.options.targetPath)}`,
|
`REA_TARGET_PATH = ${JSON.stringify(this.options.targetPath)}`,
|
||||||
|
`REA_OWNS_PROCESS_LIFETIME = ${ownsProcessLifetime ? "True" : "False"}`,
|
||||||
`exec(compile(open(${JSON.stringify(this.options.bridgeScriptPath)}, 'rb').read(), ${JSON.stringify(this.options.bridgeScriptPath)}, 'exec'))`,
|
`exec(compile(open(${JSON.stringify(this.options.bridgeScriptPath)}, 'rb').read(), ${JSON.stringify(this.options.bridgeScriptPath)}, 'exec'))`,
|
||||||
"",
|
"",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
@@ -141,6 +145,7 @@ export class HopperApplicationLauncher implements BridgeLauncher {
|
|||||||
return ok({
|
return ok({
|
||||||
process: child,
|
process: child,
|
||||||
ownsProcessLifetime: true,
|
ownsProcessLifetime: true,
|
||||||
|
shutdownByCleanup: ownsProcessLifetime,
|
||||||
cleanup: () =>
|
cleanup: () =>
|
||||||
cleanupOwnedProcessGroup(
|
cleanupOwnedProcessGroup(
|
||||||
ownedProcessGroup(session, pid, ownershipCommand),
|
ownedProcessGroup(session, pid, ownershipCommand),
|
||||||
@@ -163,11 +168,7 @@ const linuxDemoLaunch = (
|
|||||||
readonly ownershipCommand: string;
|
readonly ownershipCommand: string;
|
||||||
}
|
}
|
||||||
| undefined => {
|
| undefined => {
|
||||||
if (
|
if (!usesLinuxDemo(options) || options.demoHelperPath === undefined)
|
||||||
process.platform !== "linux" ||
|
|
||||||
basename(options.launcherPath) !== "Hopper" ||
|
|
||||||
options.demoHelperPath === undefined
|
|
||||||
)
|
|
||||||
return undefined;
|
return undefined;
|
||||||
return {
|
return {
|
||||||
command: "/usr/bin/python3",
|
command: "/usr/bin/python3",
|
||||||
@@ -185,6 +186,11 @@ const linuxDemoLaunch = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const usesLinuxDemo = (options: HopperApplicationLauncherOptions): boolean =>
|
||||||
|
process.platform === "linux" &&
|
||||||
|
basename(options.launcherPath) === "Hopper" &&
|
||||||
|
options.demoHelperPath !== undefined;
|
||||||
|
|
||||||
const spawnLauncher = async (
|
const spawnLauncher = async (
|
||||||
command: string,
|
command: string,
|
||||||
args: readonly string[],
|
args: readonly string[],
|
||||||
|
|||||||
@@ -173,7 +173,13 @@ export class HopperClient {
|
|||||||
{},
|
{},
|
||||||
{ timeoutMs: SHUTDOWN_TIMEOUT_MS },
|
{ timeoutMs: SHUTDOWN_TIMEOUT_MS },
|
||||||
).catch(() => err(new HopperProcessError(null)));
|
).catch(() => err(new HopperProcessError(null)));
|
||||||
if (!shutdown.ok || !isShutdownAcknowledgement(shutdown.value))
|
if (
|
||||||
|
!shutdown.ok ||
|
||||||
|
!isShutdownAcknowledgement(
|
||||||
|
shutdown.value,
|
||||||
|
this.#launch?.shutdownByCleanup === true,
|
||||||
|
)
|
||||||
|
)
|
||||||
this.#logger.warn(
|
this.#logger.warn(
|
||||||
{ status: shutdown.ok ? "invalid-acknowledgement" : "failed" },
|
{ status: shutdown.ok ? "invalid-acknowledgement" : "failed" },
|
||||||
"Hopper document shutdown was not confirmed",
|
"Hopper document shutdown was not confirmed",
|
||||||
@@ -412,13 +418,17 @@ const connectOnce = async (
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const isShutdownAcknowledgement = (value: JsonValue): boolean =>
|
const isShutdownAcknowledgement = (
|
||||||
typeof value === "object" &&
|
value: JsonValue,
|
||||||
value !== null &&
|
ownsProcessLifetime: boolean,
|
||||||
!Array.isArray(value) &&
|
): boolean => {
|
||||||
value.shutdown === true &&
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
||||||
value.analysis_stopped === true &&
|
return false;
|
||||||
value.document_closed === true;
|
if (value.shutdown !== true) return false;
|
||||||
|
if (value.analysis_stopped === true && value.document_closed === true)
|
||||||
|
return true;
|
||||||
|
return ownsProcessLifetime && value.cleanup_required === true;
|
||||||
|
};
|
||||||
|
|
||||||
const parseServerInfo = (
|
const parseServerInfo = (
|
||||||
value: JsonValue,
|
value: JsonValue,
|
||||||
|
|||||||
+68
-44
@@ -1,4 +1,4 @@
|
|||||||
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
@@ -17,51 +17,75 @@ import { createServer } from "../src/server/createServer.js";
|
|||||||
import { observed } from "./fixtures/analysisExecution.js";
|
import { observed } from "./fixtures/analysisExecution.js";
|
||||||
|
|
||||||
describe("artifact graph MCP integration", () => {
|
describe("artifact graph MCP integration", () => {
|
||||||
it("returns a safe ASAR integrity error without paths or hashes", async () => {
|
it.each([
|
||||||
const root = await mkdtemp(join(tmpdir(), "rea-asar-integrity-mcp-"));
|
{ unpacked: false, label: "embedded" },
|
||||||
const source = join(root, "source");
|
{ unpacked: true, label: "unpacked" },
|
||||||
const original = "console.log('ok');\n";
|
])(
|
||||||
const changed = "changed();\n";
|
"returns actionable local details for an $label ASAR integrity error",
|
||||||
await mkdir(source);
|
async ({ unpacked }) => {
|
||||||
await writeFile(join(source, "main.js"), original);
|
const root = await mkdtemp(join(tmpdir(), "rea-asar-integrity-mcp-"));
|
||||||
const archive = join(root, "fixture.asar");
|
const source = join(root, "source");
|
||||||
await createPackageWithOptions(source, archive, { unpack: "*.js" });
|
const original = "console.log('ok');\n";
|
||||||
await writeFile(join(`${archive}.unpacked`, "main.js"), changed);
|
const changed = "console.log('no');\n";
|
||||||
|
await mkdir(source);
|
||||||
|
await writeFile(join(source, "main.js"), original);
|
||||||
|
const archive = join(root, "fixture.asar");
|
||||||
|
await createPackageWithOptions(
|
||||||
|
source,
|
||||||
|
archive,
|
||||||
|
unpacked ? { unpack: "*.js" } : {},
|
||||||
|
);
|
||||||
|
if (unpacked) {
|
||||||
|
await writeFile(join(`${archive}.unpacked`, "main.js"), changed);
|
||||||
|
} else {
|
||||||
|
const bytes = await readFile(archive);
|
||||||
|
const contentOffset = bytes.indexOf(original);
|
||||||
|
expect(contentOffset).toBeGreaterThanOrEqual(0);
|
||||||
|
bytes.write(changed, contentOffset, "utf8");
|
||||||
|
await writeFile(archive, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
const session = new BinarySession(new ArtifactProvider());
|
const session = new BinarySession(new ArtifactProvider());
|
||||||
const server = createServer(session, session);
|
const server = createServer(session, session);
|
||||||
const client = new Client({ name: "asar-integrity-test", version: "1" });
|
const client = new Client({ name: "asar-integrity-test", version: "1" });
|
||||||
const [clientTransport, serverTransport] =
|
const [clientTransport, serverTransport] =
|
||||||
InMemoryTransport.createLinkedPair();
|
InMemoryTransport.createLinkedPair();
|
||||||
try {
|
try {
|
||||||
await server.connect(serverTransport);
|
await server.connect(serverTransport);
|
||||||
await client.connect(clientTransport);
|
await client.connect(clientTransport);
|
||||||
const opened = await client.callTool({
|
const opened = await client.callTool({
|
||||||
name: "open_binary",
|
name: "open_binary",
|
||||||
arguments: { path: archive },
|
arguments: { path: archive },
|
||||||
});
|
});
|
||||||
expect(opened.isError).not.toBe(true);
|
expect(opened.isError).not.toBe(true);
|
||||||
|
|
||||||
const result = await client.callTool({
|
const result = await client.callTool({
|
||||||
name: "inventory_artifact",
|
name: "inventory_artifact",
|
||||||
arguments: {},
|
arguments: {},
|
||||||
});
|
});
|
||||||
expect(result.isError).toBe(true);
|
expect(result.isError).toBe(true);
|
||||||
expect(result.structuredContent).toEqual({
|
expect(result.structuredContent).toEqual({
|
||||||
error: {
|
error: {
|
||||||
category: "integrity_mismatch",
|
category: "integrity_mismatch",
|
||||||
message:
|
message:
|
||||||
"Artifact is invalid or has changed. Get a fresh copy and try again.",
|
"Artifact is invalid or has changed. Get a fresh copy and try again.",
|
||||||
},
|
details: {
|
||||||
});
|
logical_path: "main.js",
|
||||||
} finally {
|
declared_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
|
||||||
await Promise.allSettled([
|
calculated_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
|
||||||
client.close(),
|
unpacked,
|
||||||
server.close(),
|
},
|
||||||
session.close(),
|
},
|
||||||
]);
|
});
|
||||||
}
|
} finally {
|
||||||
});
|
await Promise.allSettled([
|
||||||
|
client.close(),
|
||||||
|
server.close(),
|
||||||
|
session.close(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("rejects altered payloads that reuse session Evidence IDs", async () => {
|
it("rejects altered payloads that reuse session Evidence IDs", async () => {
|
||||||
const session = new BinarySession(() => ({
|
const session = new BinarySession(() => ({
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ describe("Hopper bridge truthfulness", () => {
|
|||||||
expect(bridgeSource).toContain("document.getDatabaseFilePath()");
|
expect(bridgeSource).toContain("document.getDatabaseFilePath()");
|
||||||
expect(bridgeSource).toContain("document.backgroundProcessActive()");
|
expect(bridgeSource).toContain("document.backgroundProcessActive()");
|
||||||
expect(bridgeSource).toContain("document.requestBackgroundProcessStop()");
|
expect(bridgeSource).toContain("document.requestBackgroundProcessStop()");
|
||||||
|
expect(bridgeSource).toContain("if REA_OWNS_PROCESS_LIFETIME:");
|
||||||
|
expect(bridgeSource).toContain('"cleanup_required": True');
|
||||||
expect(bridgeSource).toContain("document.waitForBackgroundProcessToEnd()");
|
expect(bridgeSource).toContain("document.waitForBackgroundProcessToEnd()");
|
||||||
expect(bridgeSource).toContain("document.closeDocument()");
|
expect(bridgeSource).toContain("document.closeDocument()");
|
||||||
expect(bridgeSource).toContain(
|
expect(bridgeSource).toContain(
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { execFile } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
|
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
|
import { createPackageWithOptions } from "@electron/asar";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { sanitizeCliOutput } from "../src/cliOutput.js";
|
import { sanitizeCliOutput } from "../src/cliOutput.js";
|
||||||
@@ -87,4 +91,28 @@ describe("CLI output boundary", () => {
|
|||||||
'ok: false\nerror:\n code: VALIDATION_ERROR\n message: "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again."\n',
|
'ok: false\nerror:\n code: VALIDATION_ERROR\n message: "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again."\n',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves artifact diagnostics in ordinary and full JSON output", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "rea-cli-diagnostics-"));
|
||||||
|
const source = join(root, "source");
|
||||||
|
await mkdir(source);
|
||||||
|
await writeFile(join(source, "main.js"), "console.log('ok');\n");
|
||||||
|
const archive = join(root, "fixture.asar");
|
||||||
|
await createPackageWithOptions(source, archive, { unpack: "*.js" });
|
||||||
|
await writeFile(join(`${archive}.unpacked`, "main.js"), "changed();\n");
|
||||||
|
|
||||||
|
for (const flags of [["--json"], ["--full-output", "--json"]]) {
|
||||||
|
const { stdout } = await execFileAsync(process.execPath, [
|
||||||
|
"scripts/rea.mjs",
|
||||||
|
...flags,
|
||||||
|
"inventory-artifact",
|
||||||
|
archive,
|
||||||
|
]);
|
||||||
|
const output = JSON.stringify(JSON.parse(stdout) as unknown);
|
||||||
|
expect(output).toContain('"logical_path":"main.js"');
|
||||||
|
expect(output).toMatch(/"declared_sha256":"[a-f0-9]{64}"/u);
|
||||||
|
expect(output).toMatch(/"calculated_sha256":"[a-f0-9]{64}"/u);
|
||||||
|
expect(output).toContain('"unpacked":true');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -58,7 +58,7 @@ describe("analysis error projection", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("projects every typed failure without internal details", () => {
|
it("projects every typed failure without secrets", () => {
|
||||||
const secretCause = new Error("secret-token");
|
const secretCause = new Error("secret-token");
|
||||||
const byTag = {
|
const byTag = {
|
||||||
AnalysisProtocolError: new AnalysisProtocolError("protocol failed"),
|
AnalysisProtocolError: new AnalysisProtocolError("protocol failed"),
|
||||||
@@ -132,7 +132,6 @@ describe("analysis error projection", () => {
|
|||||||
"fixture",
|
"fixture",
|
||||||
"overview",
|
"overview",
|
||||||
"AnalysisCapabilityUnavailableError",
|
"AnalysisCapabilityUnavailableError",
|
||||||
"main.js",
|
|
||||||
"declaredSha256",
|
"declaredSha256",
|
||||||
])
|
])
|
||||||
expect(serialized).not.toContain(hidden);
|
expect(serialized).not.toContain(hidden);
|
||||||
@@ -152,6 +151,12 @@ describe("analysis error projection", () => {
|
|||||||
),
|
),
|
||||||
).toMatchObject({
|
).toMatchObject({
|
||||||
category: "integrity_mismatch",
|
category: "integrity_mismatch",
|
||||||
|
details: {
|
||||||
|
logical_path: "main.js",
|
||||||
|
declared_sha256: "a".repeat(64),
|
||||||
|
calculated_sha256: "b".repeat(64),
|
||||||
|
unpacked: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(projected.every(({ message }) => message.length > 0)).toBe(true);
|
expect(projected.every(({ message }) => message.length > 0)).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
Reference in new issue
Block a user