Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13cc7a752b | ||
|
|
bd1d281107 |
No files matched your search
+30
-8
@@ -1,5 +1,5 @@
|
||||
import { constants } from "node:fs";
|
||||
import { access, stat } from "node:fs/promises";
|
||||
import { access, lstat, stat } from "node:fs/promises";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
import type {
|
||||
@@ -240,7 +240,7 @@ const cacheCheck = async (
|
||||
};
|
||||
await access(cacheRoot, constants.W_OK | constants.X_OK);
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") {
|
||||
if (isMissing(cause)) {
|
||||
const creatable = await nearestDirectoryWritable(cacheRoot);
|
||||
return creatable.ok
|
||||
? {
|
||||
@@ -285,13 +285,17 @@ const cacheCheck = async (
|
||||
};
|
||||
};
|
||||
|
||||
/** Whether the nearest existing ancestor is a directory REA may create entries in. */
|
||||
/**
|
||||
* Whether the nearest existing entry on `path` is a directory REA may create
|
||||
* entries in. A missing target is only absent when no entry occupies its name;
|
||||
* a dangling symbolic link blocks creation just as a regular file does.
|
||||
*/
|
||||
const nearestDirectoryWritable = async (
|
||||
path: string,
|
||||
): Promise<
|
||||
{ readonly ok: true } | { readonly ok: false; readonly reason: string }
|
||||
> => {
|
||||
let candidate = dirname(path);
|
||||
let candidate = path;
|
||||
for (;;) {
|
||||
try {
|
||||
if (!(await stat(candidate)).isDirectory())
|
||||
@@ -299,15 +303,33 @@ const nearestDirectoryWritable = async (
|
||||
await access(candidate, constants.W_OK | constants.X_OK);
|
||||
return { ok: true };
|
||||
} catch (cause: unknown) {
|
||||
const missing =
|
||||
cause instanceof Error && "code" in cause && cause.code === "ENOENT";
|
||||
const parent = dirname(candidate);
|
||||
if (!missing || parent === candidate)
|
||||
const absent = isMissing(cause) && (await entryAbsent(candidate));
|
||||
if (absent !== true || parent === candidate)
|
||||
return {
|
||||
ok: false,
|
||||
reason: `${candidate} is not writable: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
reason:
|
||||
typeof absent === "string"
|
||||
? absent
|
||||
: `${candidate} is not writable: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
};
|
||||
candidate = parent;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isMissing = (cause: unknown): boolean =>
|
||||
cause instanceof Error && "code" in cause && cause.code === "ENOENT";
|
||||
|
||||
/** True when nothing occupies `path`; otherwise why its name is unusable. */
|
||||
const entryAbsent = async (path: string): Promise<true | string> => {
|
||||
try {
|
||||
return (await lstat(path)).isSymbolicLink()
|
||||
? `${path} is a symbolic link whose target does not exist.`
|
||||
: `${path} exists but cannot be resolved.`;
|
||||
} catch (cause: unknown) {
|
||||
return isMissing(cause)
|
||||
? true
|
||||
: `${path} cannot be inspected: ${cause instanceof Error ? cause.message : String(cause)}`;
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { lstat, mkdir, readlink, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it as test } from "vitest";
|
||||
@@ -139,6 +139,49 @@ it("reports a cache root REA cannot create under its nearest existing parent", a
|
||||
});
|
||||
});
|
||||
|
||||
it("reports a dangling cache link as unusable and accepts a resolving link", async () => {
|
||||
const root = await createTestTempDirectory("rea-kuna-doctor-link-");
|
||||
const kuna = await writeFakeKuna(root);
|
||||
const dangling = join(root, "dangling");
|
||||
const missing = join(root, "missing-target");
|
||||
await symlink(missing, dangling);
|
||||
const target = join(root, "target");
|
||||
await mkdir(target);
|
||||
const resolving = join(root, "resolving");
|
||||
await symlink(target, resolving);
|
||||
|
||||
const direct = await inspectKunaProvider({
|
||||
REA_KUNA_COMMAND: kuna,
|
||||
REA_KUNA_CACHE_DIR: dangling,
|
||||
});
|
||||
const beneath = await inspectKunaProvider({
|
||||
REA_KUNA_COMMAND: kuna,
|
||||
REA_KUNA_CACHE_DIR: join(dangling, "nested", "cache"),
|
||||
});
|
||||
const ready = await inspectKunaProvider({
|
||||
REA_KUNA_COMMAND: kuna,
|
||||
REA_KUNA_CACHE_DIR: resolving,
|
||||
});
|
||||
|
||||
for (const blocked of [direct, beneath])
|
||||
expect(blocked.checks[1]).toMatchObject({
|
||||
name: "cache",
|
||||
ok: false,
|
||||
code: "open_options_invalid",
|
||||
detail: expect.stringContaining(
|
||||
`${dangling} is a symbolic link whose target does not exist.`,
|
||||
),
|
||||
});
|
||||
expect(ready.checks[1]).toMatchObject({
|
||||
name: "cache",
|
||||
ok: true,
|
||||
detail: expect.stringContaining(`cached in ${resolving}`),
|
||||
});
|
||||
// Diagnosis neither creates the cache nor repairs the link.
|
||||
expect(await readlink(dangling)).toBe(missing);
|
||||
await expect(lstat(missing)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
const doctorEnvironment = (root: string, kuna: string): NodeJS.ProcessEnv => ({
|
||||
HOME: root,
|
||||
PATH: "",
|
||||
|
||||
Reference in new issue
Block a user