mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(artifacts): retain permission omissions through JavaScript lookups
This commit is contained in:
1 parent
dd55e2c7d5
commit
693ecec082
14 files changed
+343
-30
No files matched your search
@@ -97,8 +97,9 @@ Omit the selection for the existing complete
|
||||
application workflow.
|
||||
|
||||
The result and summary retain `source_selection.requested_paths` and
|
||||
`source_selection.unselected_files`. Every relevant file keeps its original
|
||||
inventory identity; unselected files report `text_status:
|
||||
`source_selection.unselected_files`. Selecting every relevant file leaves this
|
||||
count at zero and adds no unselected-source caveat. Every relevant file keeps its
|
||||
original inventory identity; unselected files report `text_status:
|
||||
"outside-analysis-scope"`. Their relationships remain unknown, and application
|
||||
coverage is partial while any file remains outside scope. Selection does not
|
||||
claim whole-application completeness or change integrity policy. Summary detail
|
||||
@@ -106,6 +107,19 @@ only reduces delivery; source selection reduces text acquisition and analysis.
|
||||
Explicit native provider or snapshot routing rejects `--source-path` rather
|
||||
than ignoring it.
|
||||
|
||||
For directory input, a permission-denied child file remains an unavailable
|
||||
inventory occurrence, and an unreadable child directory records an unknown
|
||||
descendant omission. Readable sources remain available; application coverage is
|
||||
partial and retains those inventory limitations. An unreadable selected root
|
||||
remains an error. Explicit readers that require selected child bytes keep their
|
||||
access errors rather than treating denied paths as absent.
|
||||
Incomplete traversal also qualifies negative path and package-metadata lookups:
|
||||
an unobserved earlier loader candidate cannot be skipped in favor of a later
|
||||
observed candidate. An explicit source selection whose discovery is unknown
|
||||
returns an unavailable error with the retained inventory, rather than claiming
|
||||
the selected source does not exist. Observed exact file candidates keep their
|
||||
identity; unknown traversal cannot establish a unique application root.
|
||||
|
||||
ASAR header/index admission defaults to 16 MiB / 16777216 bytes. Set
|
||||
`REA_ASAR_MAX_HEADER_BYTES` in the environment that starts REA to a decimal
|
||||
byte count of at most 268435456 (256 MiB) to raise or lower it; restart REA
|
||||
|
||||
@@ -159,6 +159,7 @@ function* buildJavaScriptArtifactGraphInputSteps(
|
||||
root,
|
||||
filesByPath: new Map(fileSet.files.map((file) => [file.path, file])),
|
||||
unanalyzedFiles: {
|
||||
coverage: snapshot.coverage,
|
||||
get: (path) =>
|
||||
unavailableFilesByPath.get(path) ?? fileSet.unanalyzed_files?.get(path),
|
||||
hasDirectory: (path, container) =>
|
||||
|
||||
@@ -118,6 +118,7 @@ export const applicationRootPath = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
source: JavaScriptArtifactFile,
|
||||
): string | null => {
|
||||
if (context.snapshot.coverage.status !== "complete") return null;
|
||||
// Unread launch metadata can name a main in another inventoried container.
|
||||
// Archive occurrence identity does not prove isolation from the Node loader.
|
||||
if (context.applicationOwnershipUnknownContainers.size > 0) return null;
|
||||
|
||||
@@ -329,6 +329,7 @@ it.each([
|
||||
gap === "opaque"
|
||||
? {
|
||||
get: (path) => (path === target.path ? target : undefined),
|
||||
coverage: base.snapshot.coverage,
|
||||
hasDirectory: () => false,
|
||||
}
|
||||
: undefined,
|
||||
|
||||
@@ -10,6 +10,72 @@ import { artifactLocalIdentity } from "./JavaScriptArtifactGraphContext.js";
|
||||
import { applicationNodeIdentitySchema } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
|
||||
import { analyzeJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js";
|
||||
|
||||
const completeInventoryCoverage = {
|
||||
scope: "container-entries",
|
||||
status: "complete",
|
||||
} as const;
|
||||
|
||||
describe("incomplete inventory lookups", () => {
|
||||
it.each(["complete", "unknown"] as const)(
|
||||
"qualifies missing candidates before extension fallbacks under %s traversal",
|
||||
(status) => {
|
||||
const files = fileMap([
|
||||
file("app/main.cjs", "root"),
|
||||
file("app/target.js", "root"),
|
||||
]);
|
||||
const unanalyzedFiles = {
|
||||
coverage: { scope: "container-entries" as const, status },
|
||||
get: () => undefined,
|
||||
hasDirectory: () => false,
|
||||
};
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath: "./target",
|
||||
sourcePath: "app/main.cjs",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
unanalyzedFiles,
|
||||
}),
|
||||
).toMatchObject(
|
||||
status === "complete"
|
||||
? { resolution_status: "resolved", resolved_path: "app/target.js" }
|
||||
: {
|
||||
resolution_status: "unavailable",
|
||||
resolved_path: null,
|
||||
candidate_path: "app/target",
|
||||
},
|
||||
);
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath: "./target.js",
|
||||
sourcePath: "app/main.cjs",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
unanalyzedFiles,
|
||||
}),
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "app/target.js",
|
||||
});
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath: "./missing.js",
|
||||
sourcePath: "app/main.cjs",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
unanalyzedFiles,
|
||||
}),
|
||||
).toMatchObject({
|
||||
resolution_status: status === "complete" ? "not-found" : "unavailable",
|
||||
resolved_path: null,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("Windows CommonJS request classification", () => {
|
||||
it.each([
|
||||
"..\\shared\\window.cjs",
|
||||
@@ -578,6 +644,7 @@ describe("loader-specific module suffix lookup", () => {
|
||||
const files = fileMap([file("app/entry.cjs", "root")]);
|
||||
const unanalyzedFiles = {
|
||||
get: (path: string) => opaque.get(path),
|
||||
coverage: completeInventoryCoverage,
|
||||
hasDirectory: () => false,
|
||||
};
|
||||
expect(
|
||||
@@ -650,6 +717,7 @@ describe("unavailable inventoried native add-ons", () => {
|
||||
files: fileMap([file("app/entry.cjs", "root")]),
|
||||
unanalyzedFiles: {
|
||||
get: (path) => (path === unavailable.path ? unavailable : undefined),
|
||||
coverage: completeInventoryCoverage,
|
||||
hasDirectory: () => false,
|
||||
},
|
||||
});
|
||||
@@ -689,6 +757,7 @@ describe("unavailable package metadata", () => {
|
||||
requestedPaths.push(path);
|
||||
return path === manifest.path ? manifest : undefined;
|
||||
},
|
||||
coverage: completeInventoryCoverage,
|
||||
hasDirectory: () => false,
|
||||
};
|
||||
const relativeRequire = resolve({
|
||||
@@ -757,6 +826,7 @@ describe("unavailable package metadata", () => {
|
||||
]);
|
||||
const lookup = {
|
||||
get: (path: string) => (path === manifest.path ? manifest : undefined),
|
||||
coverage: completeInventoryCoverage,
|
||||
hasDirectory: () => false,
|
||||
};
|
||||
for (const [sourcePath, moduleKind] of [
|
||||
@@ -4128,6 +4198,7 @@ describe("Node launch package main ownership", () => {
|
||||
limitations: ["main readback unavailable"],
|
||||
}
|
||||
: undefined,
|
||||
coverage: completeInventoryCoverage,
|
||||
hasDirectory: () => false,
|
||||
},
|
||||
}),
|
||||
@@ -4376,6 +4447,7 @@ describe("package metadata at archive module boundaries", () => {
|
||||
unanalyzedFiles: {
|
||||
get: (requested) =>
|
||||
opaque && requested === path ? target : undefined,
|
||||
coverage: completeInventoryCoverage,
|
||||
hasDirectory: () => false,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1426,6 +1426,11 @@ const readPackageMetadata = (
|
||||
unavailable.limitations?.[0] ??
|
||||
`Directory package metadata ${path} was inventoried but its bytes are unavailable.`,
|
||||
};
|
||||
if (input.unanalyzedFiles?.coverage.status === "unknown")
|
||||
return {
|
||||
kind: "unavailable",
|
||||
limitation: `Directory package metadata ${path} was not observed, and artifact traversal is incomplete; its absence is unknown.`,
|
||||
};
|
||||
return { kind: "absent" };
|
||||
}
|
||||
if (!admitsRuntimeContainer(input, path, file.container_sha256))
|
||||
@@ -1750,6 +1755,15 @@ const findFileCandidate = (
|
||||
: `${path} is an inventoried ${unanalyzed.kind} file, but its bytes are unavailable (hash status ${unanalyzed.hash_status ?? "unknown"}); resolution cannot be confirmed.`,
|
||||
],
|
||||
};
|
||||
if (input.unanalyzedFiles?.coverage.status === "unknown")
|
||||
return {
|
||||
resolvedPath: null,
|
||||
status: "unavailable",
|
||||
candidatePath: path,
|
||||
limitations: [
|
||||
`Artifact path ${path} was not observed, and artifact traversal is incomplete; its absence is unknown.`,
|
||||
],
|
||||
};
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -94,7 +94,10 @@ export class DirectoryArtifactReader implements ArtifactReader {
|
||||
readonly #files = new Map<OwnedFileHandle, string>();
|
||||
readonly #fileCleanup = new WeakMap<OwnedFileHandle, AnalysisCleanupLease>();
|
||||
|
||||
constructor(root: string) {
|
||||
constructor(
|
||||
root: string,
|
||||
private readonly unreadableDirectories: "fail" | "omit" = "fail",
|
||||
) {
|
||||
this.#rootPath = root;
|
||||
}
|
||||
|
||||
@@ -131,18 +134,36 @@ export class DirectoryArtifactReader implements ArtifactReader {
|
||||
await withDirectoryIoContext("inspect directory", directory, () =>
|
||||
assertContainedDirectory(root, directory, admittedRoot.identity),
|
||||
);
|
||||
for await (const entry of this.#readDirectory(
|
||||
directory,
|
||||
pending,
|
||||
signal,
|
||||
options,
|
||||
examine,
|
||||
(rawHex) => {
|
||||
unrepresentableNameCount += 1;
|
||||
firstUnrepresentableNameHex ??= rawHex;
|
||||
},
|
||||
))
|
||||
yield entry;
|
||||
try {
|
||||
for await (const entry of this.#readDirectory(
|
||||
directory,
|
||||
pending,
|
||||
signal,
|
||||
options,
|
||||
examine,
|
||||
(rawHex) => {
|
||||
unrepresentableNameCount += 1;
|
||||
firstUnrepresentableNameHex ??= rawHex;
|
||||
},
|
||||
))
|
||||
yield entry;
|
||||
} catch (cause: unknown) {
|
||||
if (
|
||||
this.unreadableDirectories !== "omit" ||
|
||||
!(cause instanceof DirectoryArtifactEntryAccessDenied)
|
||||
)
|
||||
throw cause;
|
||||
abortIfNeeded(signal);
|
||||
options?.onOmission?.({
|
||||
path: cause.entryPath,
|
||||
kind: "directory-descendants",
|
||||
});
|
||||
options?.onCoverage?.({
|
||||
status: "unknown",
|
||||
limitations: [cause.message],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
await withDirectoryIoContext("inspect directory", directory, () =>
|
||||
assertContainedDirectory(root, directory, admittedRoot.identity),
|
||||
);
|
||||
@@ -204,6 +225,14 @@ export class DirectoryArtifactReader implements ArtifactReader {
|
||||
const handle = new OwnedDirectoryHandle(
|
||||
await opendir(directory, { encoding: "buffer" as BufferEncoding }).catch(
|
||||
(cause: unknown) => {
|
||||
const code = hostErrorCode(cause);
|
||||
if (directory !== root && (code === "EACCES" || code === "EPERM"))
|
||||
throw new DirectoryArtifactEntryAccessDenied(
|
||||
code,
|
||||
relative(root, directory).split(sep).join("/"),
|
||||
directoryIoFailure("open directory", directory, cause).message,
|
||||
{ cause },
|
||||
);
|
||||
throw directoryIoFailure(
|
||||
"open directory",
|
||||
directory,
|
||||
|
||||
@@ -176,8 +176,8 @@ const materializeArtifactInventoryInScope = async (
|
||||
input.inputPath,
|
||||
`Archive entry ${occurrence.logical_path} is encrypted; REA does not decrypt archive entries, so the archive cannot be extracted completely`,
|
||||
);
|
||||
// Inventory keeps an ASAR unpacked entry whose companion file is absent,
|
||||
// but extraction cannot materialize bytes it never observed.
|
||||
// Inventory retains unavailable source occurrences, but extraction cannot
|
||||
// materialize bytes it never observed.
|
||||
if (
|
||||
occurrence.entry_kind === "file" &&
|
||||
occurrence.artifact_id === null &&
|
||||
@@ -185,13 +185,13 @@ const materializeArtifactInventoryInScope = async (
|
||||
)
|
||||
throw new ArtifactReaderFailure(
|
||||
"unavailable",
|
||||
`ASAR unpacked companion bytes are unavailable for ${occurrence.logical_path}`,
|
||||
`Inventoried source bytes are unavailable for ${occurrence.logical_path}`,
|
||||
undefined,
|
||||
{
|
||||
logicalPath: occurrence.logical_path,
|
||||
declaredSha256: null,
|
||||
calculatedSha256: null,
|
||||
unpacked: true,
|
||||
unpacked: occurrence.unpacked,
|
||||
},
|
||||
);
|
||||
if (
|
||||
|
||||
@@ -31,7 +31,7 @@ export const createReader = (
|
||||
return new ZipArtifactReader(path, format, undefined, rootSource);
|
||||
switch (format) {
|
||||
case "directory":
|
||||
return new DirectoryArtifactReader(path);
|
||||
return new DirectoryArtifactReader(path, "omit");
|
||||
case "asar":
|
||||
return new AsarArtifactReader(
|
||||
path,
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
type ArtifactReaderTraversal,
|
||||
} from "../ArtifactReader.js";
|
||||
import { ArtifactTraversalObservation } from "../ArtifactTraversalObservation.js";
|
||||
import { DirectoryArtifactEntryAccessDenied } from "../DirectoryArtifactReader.js";
|
||||
import { AsarArtifactReader } from "../AsarArtifactReader.js";
|
||||
import type { ArtifactNode } from "../../domain/artifactGraph.js";
|
||||
import {
|
||||
@@ -34,7 +35,7 @@ import {
|
||||
STRICT_INTEGRITY_POLICY,
|
||||
type ArtifactIntegrityPolicy,
|
||||
} from "./types.js";
|
||||
import { hashReadable } from "../ArtifactHash.js";
|
||||
import { abortIfNeeded, hashReadable } from "../ArtifactHash.js";
|
||||
|
||||
export interface PendingIntegrityContradiction {
|
||||
readonly occurrence: MutableOccurrence;
|
||||
@@ -214,9 +215,17 @@ const visitArtifactEntries = async (
|
||||
occurrence,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
if (!isUnavailableUnpackedEntry(cause, entry)) throw cause;
|
||||
let limitation = UNAVAILABLE_UNPACKED_LIMITATION;
|
||||
if (cause instanceof DirectoryArtifactEntryAccessDenied) {
|
||||
abortIfNeeded(context.signal);
|
||||
limitation = cause.message;
|
||||
context.traversal.onCoverage({
|
||||
status: "unknown",
|
||||
limitations: [limitation],
|
||||
});
|
||||
} else if (!isUnavailableUnpackedEntry(cause, entry)) throw cause;
|
||||
occurrence.hash_status = "unavailable";
|
||||
occurrence.limitations.push(UNAVAILABLE_UNPACKED_LIMITATION);
|
||||
occurrence.limitations.push(limitation);
|
||||
}
|
||||
if (digested === undefined) noteUnreadContainerSuffix(occurrence);
|
||||
else {
|
||||
|
||||
@@ -80,6 +80,23 @@ export const readJavaScriptArtifactFiles = async (
|
||||
sourcePaths === undefined
|
||||
? undefined
|
||||
: new Map([...new Set(sourcePaths)].map((path, index) => [path, index]));
|
||||
const undiscoveredPath = sourcePaths?.find(
|
||||
(path) =>
|
||||
relevantKind(path) !== undefined &&
|
||||
!expected.has(path) &&
|
||||
!inventory.unavailableFiles.has(path) &&
|
||||
snapshot.occurrences[
|
||||
lowerBound(snapshot.occurrences, path, (entry) => entry.logical_path)
|
||||
]?.logical_path !== path,
|
||||
);
|
||||
if (undiscoveredPath !== undefined && snapshot.coverage.status === "unknown")
|
||||
throw new ArtifactReaderFailure(
|
||||
"unavailable",
|
||||
`Selected source discovery is unavailable under incomplete artifact traversal: ${undiscoveredPath}`,
|
||||
{
|
||||
partialObservation: { kind: "artifact-inventory", inventory: snapshot },
|
||||
},
|
||||
);
|
||||
const issues = (sourcePaths ?? []).flatMap((path, index) =>
|
||||
expected.has(path) || inventory.unavailableFiles.has(path)
|
||||
? []
|
||||
@@ -229,11 +246,7 @@ const visitReader = async (
|
||||
`Nested ASAR was not present in inventory: ${path}`,
|
||||
);
|
||||
if (container.hash_status === "mismatched") continue;
|
||||
if (container.hash_status === "unavailable")
|
||||
throw new ArtifactReaderFailure(
|
||||
"unavailable",
|
||||
`Nested ASAR bytes were unavailable during inventory: ${path}`,
|
||||
);
|
||||
if (container.hash_status === "unavailable") continue;
|
||||
if (container.hash_status !== "verified")
|
||||
throw new ArtifactReaderFailure(
|
||||
"integrity",
|
||||
@@ -512,6 +525,7 @@ const createUnanalyzedArtifactLookup = (
|
||||
occurrence.artifact_id === null &&
|
||||
relevantKind(occurrence.logical_path) !== undefined;
|
||||
return {
|
||||
coverage: snapshot.coverage,
|
||||
get(path) {
|
||||
if (requested.has(path)) return requested.get(path);
|
||||
const occurrence =
|
||||
|
||||
@@ -16,4 +16,4 @@ export const createJavaScriptArtifactReader = (
|
||||
? {}
|
||||
: { headerAdmission: asarHeaderAdmission },
|
||||
)
|
||||
: new DirectoryArtifactReader(path);
|
||||
: new DirectoryArtifactReader(path, "omit");
|
||||
@@ -1,7 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import { digestSchema } from "../digests.js";
|
||||
import type { JavaScriptSourceSelection } from "./javascriptSourceSelection.js";
|
||||
import type { ArtifactOccurrence } from "../artifactGraph.js";
|
||||
import type {
|
||||
ArtifactInventoryCoverage,
|
||||
ArtifactOccurrence,
|
||||
} from "../artifactGraph.js";
|
||||
|
||||
/** One physical file occurrence, independent of its graph representations. */
|
||||
export const javaScriptFileOccurrenceSchema = z.strictObject({
|
||||
@@ -81,6 +84,8 @@ export interface JavaScriptUnanalyzedArtifactFile {
|
||||
|
||||
/** Borrowed inventory lookup; metadata is materialized only for requested paths. */
|
||||
export interface JavaScriptUnanalyzedArtifactLookup {
|
||||
/** Inventory traversal coverage qualifies every negative lookup. */
|
||||
readonly coverage: ArtifactInventoryCoverage;
|
||||
get(path: string): JavaScriptUnanalyzedArtifactFile | undefined;
|
||||
hasDirectory(path: string, containerSha256: string | undefined): boolean;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { scanArtifactInventory } from "../../fixtures/artifactInventory.js";
|
||||
import { analyzeJavaScriptApplication } from "../../support/javascriptApplicationScope.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { DirectoryArtifactReader } from "../../../src/artifacts/DirectoryArtifactReader.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"projects inventoried special-entry omissions into partial application coverage",
|
||||
async () => {
|
||||
const root = await createTestTempDirectory("rea-js-directory-fifo-");
|
||||
await writeFile(join(root, "good.js"), "export const answer = 42;");
|
||||
await promisify(execFile)("mkfifo", [join(root, "events.pipe")], {
|
||||
timeout: 2_000,
|
||||
});
|
||||
const analyzed = await analyzeJavaScriptApplication({ input_path: root });
|
||||
if (!analyzed.ok) throw analyzed.error;
|
||||
const result = javascriptApplicationAnalysisResultSchema.parse(
|
||||
analyzed.value.normalized_result,
|
||||
);
|
||||
expect(result.graph.coverage).toMatchObject({
|
||||
status: "partial",
|
||||
truncated: false,
|
||||
});
|
||||
expect(result.graph.limitations).toContain(
|
||||
'Artifact traversal omitted fifo at "events.pipe".',
|
||||
);
|
||||
expect(
|
||||
result.semantic_graph.nodes.some(
|
||||
({ identity }) => identity.module_path === "good.js",
|
||||
),
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
|
||||
"retains readable source facts and permission omissions while refusing an unreadable root",
|
||||
async () => {
|
||||
const root = await createTestTempDirectory("rea-js-directory-permissions-");
|
||||
const deniedFile = join(root, "closed.js");
|
||||
const deniedDirectory = join(root, "private");
|
||||
await mkdir(deniedDirectory);
|
||||
await writeFile(
|
||||
join(root, "good.js"),
|
||||
'require("./private/hidden.js"); const { BrowserWindow } = require("electron"); new BrowserWindow().loadFile("page.html"); module.exports = 42;',
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "page.html"),
|
||||
"<!doctype html><title>Fixture</title>",
|
||||
);
|
||||
await writeFile(deniedFile, "export const unseen = true;");
|
||||
await writeFile(
|
||||
join(deniedDirectory, "hidden.js"),
|
||||
"export const hidden = true;",
|
||||
);
|
||||
await chmod(deniedFile, 0);
|
||||
await chmod(deniedDirectory, 0);
|
||||
try {
|
||||
const inventory = await scanArtifactInventory(root);
|
||||
expect(inventory.coverage.status).toBe("unknown");
|
||||
expect(
|
||||
inventory.occurrences.find(
|
||||
({ logical_path }) => logical_path === "closed.js",
|
||||
),
|
||||
).toMatchObject({ hash_status: "unavailable", artifact_id: null });
|
||||
expect(
|
||||
inventory.occurrences.some(
|
||||
({ logical_path }) => logical_path === "private/hidden.js",
|
||||
),
|
||||
).toBe(false);
|
||||
expect(inventory.limitations).toContainEqual(
|
||||
expect.stringContaining('directory-descendants at "private"'),
|
||||
);
|
||||
const selectedReader = new DirectoryArtifactReader(root);
|
||||
try {
|
||||
await expect(
|
||||
(async () => {
|
||||
for await (const entry of selectedReader.entries())
|
||||
expect(entry.path).not.toBe("private/hidden.js");
|
||||
})(),
|
||||
).rejects.toMatchObject({
|
||||
reason: "io",
|
||||
entryPath: "private",
|
||||
});
|
||||
} finally {
|
||||
await selectedReader.close();
|
||||
}
|
||||
const analyzed = await analyzeJavaScriptApplication({ input_path: root });
|
||||
if (!analyzed.ok) throw analyzed.error;
|
||||
const result = javascriptApplicationAnalysisResultSchema.parse(
|
||||
analyzed.value.normalized_result,
|
||||
);
|
||||
expect(result.graph.coverage).toMatchObject({
|
||||
status: "partial",
|
||||
truncated: false,
|
||||
});
|
||||
expect(result.graph.limitations).toEqual(
|
||||
expect.arrayContaining([...inventory.limitations]),
|
||||
);
|
||||
expect(
|
||||
result.semantic_graph.nodes.some(
|
||||
({ identity }) => identity.module_path === "good.js",
|
||||
),
|
||||
).toBe(true);
|
||||
const moduleLinks = result.graph.edges.filter(
|
||||
({ relation, properties }) =>
|
||||
relation === "imports" &&
|
||||
properties.specifier === "./private/hidden.js" &&
|
||||
properties.module_link_kind !== undefined,
|
||||
);
|
||||
expect(moduleLinks).toHaveLength(1);
|
||||
expect(moduleLinks[0]?.properties).toMatchObject({
|
||||
resolution_status: "unavailable",
|
||||
resolved_path: null,
|
||||
});
|
||||
const renderer = result.graph.nodes
|
||||
.flatMap(({ observations }) => observations)
|
||||
.find(({ properties }) => properties.declared_path === "page.html");
|
||||
expect(renderer?.properties).toMatchObject({
|
||||
resolution_status: "unavailable",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(
|
||||
await analyzeJavaScriptApplication({
|
||||
input_path: root,
|
||||
source_paths: ["private/hidden.js"],
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
reason: "unavailable",
|
||||
partialObservation: {
|
||||
kind: "artifact-inventory",
|
||||
inventory: { coverage: { status: "unknown" } },
|
||||
},
|
||||
},
|
||||
});
|
||||
await chmod(root, 0);
|
||||
await expect(scanArtifactInventory(root)).rejects.toMatchObject({
|
||||
reason: "io",
|
||||
});
|
||||
} finally {
|
||||
await chmod(root, 0o700);
|
||||
await chmod(deniedDirectory, 0o700);
|
||||
await chmod(deniedFile, 0o600);
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user