Files
rea/tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts
T
morluto 3af3811ab0 fix: integrate 24 reviewed browser/native/semantics corrections (#815)
* fix(reference): resolve emitted extensions to TypeScript source

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* test(reference): retain legitimate declaration import counterparts

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* fix(mcp): recognize implicit form elicitation capability

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(ghidra): reject contradictory NativeAOT reports

* fix(native): preserve thin architectures across echoed filename lines

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>

* fix(native): preserve app bundle executable filename whitespace

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>

* fix(doctor): validate Node-wrapped client policy

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(reference): preserve observed entry failure reasons

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* fix(artifacts): verify opaque bytes before reusing inventory identities

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* fix(browser): resolve DOM destinations against document base URL

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>

* fix: recover serialized native NIB view hierarchy

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* ci: exercise native compiled NIB hierarchy regressions

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(deps): remediate reachable archive-parsing advisories

Two high-severity advisories are reachable from untrusted target content:

- @xmldom/xmldom 0.9.10 (via plist) parses XML plist bytes from analyzed
  app bundles in the keyed-archive reader. Refreshed to 0.9.12.
- brace-expansion 5.0.7 (via @electron/asar -> minimatch) is reached while
  analyzing untrusted ASAR archives. Refreshed to 5.0.12.

Both are in-range patch updates (plist declares ^0.9.10, minimatch declares
^5.0.5), so no source or API change is required and no direct dependency
declaration moves. Lockfile-only change.

The remaining smol-toml advisory is not included: it parses operator-owned
client configuration rather than untrusted target content, and its fix moves
outside the declared range and changes leading-BOM handling.

* fix(semantics): link wrapped arrow Promise returns

* fix(reconstruction): preserve unknown graph limitations

* fix(native): preserve exact code signing identifiers

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>

* fix(native): recognize only whole otool command headers

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>

* fix(native): retain nm-proven literal export names

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>

* fix(javascript): project decoded file URL paths

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>

* fix: preserve explicit source-relative URL observation spelling

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>

* fix(native): project Mach-O evidence into container file offsets

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>

* fix(browser): normalize referrer policy fallback lists

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>

---------

Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: Rudy Celekli <rudy@gradiahq.com>
Co-authored-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: Kaoru0822 <aisakamimaki@gmail.com>
Co-authored-by: DeryFerd <deryferdikao125@gmail.com>
2026-10-07 10:33:10 +08:00

142 lines
5.2 KiB
TypeScript

import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { createPackage } from "@electron/asar";
import { describe, expect, it } from "vitest";
import { scanArtifactInventory } from "../../../src/application/ArtifactInventory.js";
import { readJavaScriptArtifactFiles } from "../../../src/application/JavaScriptArtifactFiles.js";
import { AsarArtifactReader } from "../../../src/artifacts/AsarArtifactReader.js";
import { DirectoryArtifactReader } from "../../../src/artifacts/DirectoryArtifactReader.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const fixture = async () => {
const root = await createTestTempDirectory("rea-ingestion-content-identity-");
const source = join(root, "source");
const bundle = join(root, "bundle");
await Promise.all([mkdir(source), mkdir(bundle)]);
await writeFile(join(source, "main.js"), "module.exports = 42;\n");
await writeFile(join(source, "asset.txt"), "original asset");
const archive = join(bundle, "app.asar");
await createPackage(source, archive);
return { source, bundle, archive };
};
describe("JavaScript artifact ingestion content identities", () => {
it.each([
["changed-asset", "directory"],
["added-asset", "directory"],
["changed-asset", "asar"],
["added-asset", "asar"],
])(
"rejects a rebuilt container with %s for a %s input and unchanged source",
async (change, format) => {
const { source, bundle, archive } = await fixture();
const snapshot = await scanArtifactInventory(
format === "asar" ? archive : bundle,
);
await writeFile(
join(source, change === "changed-asset" ? "asset.txt" : "new.txt"),
"rebuilt asset",
);
await createPackage(source, archive);
const reader =
format === "asar"
? new AsarArtifactReader(archive)
: new DirectoryArtifactReader(bundle);
try {
await expect(
readJavaScriptArtifactFiles(reader, snapshot),
).rejects.toMatchObject({ reason: "integrity" });
} finally {
await reader.close();
}
},
);
it("rejects changed opaque native-addon bytes after inventory", async () => {
const root = await createTestTempDirectory("rea-ingestion-native-bytes-");
const addon = join(root, "addon.node");
await writeFile(addon, Buffer.from([0xcf, 0xfa, 0xed, 0xfe, 1]));
const snapshot = await scanArtifactInventory(root);
await writeFile(addon, Buffer.from([0xcf, 0xfa, 0xed, 0xfe, 2]));
const reader = new DirectoryArtifactReader(root);
try {
await expect(
readJavaScriptArtifactFiles(reader, snapshot),
).rejects.toMatchObject({ reason: "integrity" });
} finally {
await reader.close();
}
});
it("preserves an unchanged direct ASAR raw identity and source", async () => {
const { archive } = await fixture();
const snapshot = await scanArtifactInventory(archive);
const reader = new AsarArtifactReader(archive);
try {
const files = await readJavaScriptArtifactFiles(reader, snapshot);
expect(files.containers).toEqual([]);
expect(files.files.find(({ path }) => path === "main.js")).toMatchObject({
container_sha256: snapshot.manifest.root_sha256,
text: { included: true, value: "module.exports = 42;\n" },
});
} finally {
await reader.close();
}
});
it("preserves typed cancellation when the direct ASAR filesystem stream aborts", async () => {
const { archive } = await fixture();
const snapshot = await scanArtifactInventory(archive);
const reader = new AsarArtifactReader(archive);
const controller = new AbortController();
try {
const reading = readJavaScriptArtifactFiles(
reader,
snapshot,
controller.signal,
);
controller.abort();
await expect(reading).rejects.toMatchObject({ reason: "cancelled" });
} finally {
await reader.close();
}
});
it("retains native filesystem errors for outer IO classification", async () => {
const { archive } = await fixture();
const snapshot = await scanArtifactInventory(archive);
await rm(archive);
const reader = new AsarArtifactReader(archive);
try {
await expect(
readJavaScriptArtifactFiles(reader, snapshot),
).rejects.toMatchObject({ code: "ENOENT" });
} finally {
await reader.close();
}
});
it("preserves unchanged container and opaque addon identities", async () => {
const { bundle } = await fixture();
await writeFile(join(bundle, "addon.node"), Buffer.from([1, 2, 3]));
const snapshot = await scanArtifactInventory(bundle);
const reader = new DirectoryArtifactReader(bundle);
try {
const files = await readJavaScriptArtifactFiles(reader, snapshot);
expect(files.containers).toHaveLength(1);
expect(
files.files.find(({ path }) => path === "addon.node"),
).toMatchObject({
text: { included: false, reason: "not-applicable" },
});
expect(
files.files.find(({ path }) => path === "app.asar/main.js")?.text,
).toEqual({ included: true, value: "module.exports = 42;\n" });
} finally {
await reader.close();
}
});
});