Files
reverse-engineering-anything/tests/boundary/filesystem/setupSkillPublication.test.ts
T

294 lines
11 KiB
TypeScript

import { constants as fsConstants } from "node:fs";
import {
chmod,
copyFile,
mkdir,
readFile,
readdir,
rename,
stat,
writeFile,
} from "node:fs/promises";
import { dirname, join } from "node:path";
import { describe, expect, it, vi } from "vitest";
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
import { OwnedStagedOutput } from "../../../src/filesystem/OwnedStagedOutput.js";
import { SkillFilePublication } from "../../../src/application/SkillFilePublication.js";
import {
canonicalSkillNeedsInstall,
installCanonicalSkill,
skillDestinations,
type SkillInstallFileOperations,
} from "../../../src/application/SetupSkill.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const skillPublicationOperations: SkillInstallFileOperations = {
mkdir: async (path) => {
await mkdir(path, { recursive: true });
},
backup: (source, destination) =>
copyFile(source, destination, fsConstants.COPYFILE_EXCL),
prepare: SkillFilePublication.prepare,
};
describe("skill publication ownership", () => {
it.each([
{ change: "edit", existing: true },
{ change: "replacement", existing: true },
{ change: "mode", existing: true },
{ change: "edit", existing: false },
{ change: "replacement", existing: false },
] as const)(
"preserves a user $change after publication (existing: $existing)",
async ({ change, existing }) => {
if (change === "mode" && process.platform === "win32") return;
const home = await createTestTempDirectory(
"rea-skill-published-conflict-",
);
const manifest = join(skillDestinations(home, [])[0]!.path, "SKILL.md");
await mkdir(dirname(manifest), { recursive: true });
if (existing)
await writeFile(manifest, "original instructions\n", { mode: 0o640 });
let published: Buffer | undefined;
const userBytes = Buffer.from("new user instructions\n");
const result = await installCanonicalSkill(
home,
[],
{},
process.platform,
{
...skillPublicationOperations,
prepare: async (...args) => {
const publication = await SkillFilePublication.prepare(...args);
if (publication.path === manifest) {
const publish = publication.publish.bind(publication);
vi.spyOn(publication, "publish").mockImplementation(async () => {
await publish();
published = await readFile(manifest);
if (change === "replacement") {
const replacement = `${manifest}.user`;
await writeFile(replacement, userBytes);
await rename(replacement, manifest);
} else if (change === "mode") await chmod(manifest, 0o600);
else await writeFile(manifest, userBytes);
throw new Error("post-publication failure");
});
}
return publication;
},
},
);
if (result.ok) throw new Error("Expected controlled publication failure");
expect(result.error.stage).toBe("write");
expect(result.error.reason).toBe("post-publication failure");
expect(result.error.rollbackFailures).toEqual([
expect.objectContaining({ path: manifest }),
]);
const expected = change === "mode" ? published : userBytes;
expect(await readFile(manifest)).toEqual(expected);
if (existing)
expect(await readFile(`${manifest}.rea.backup`, "utf8")).toBe(
"original instructions\n",
);
else expect(result.error.recoveryBackupPaths).toEqual([]);
expect(await result.error.retryCleanup()).toEqual([]);
expect(await readFile(manifest)).toEqual(expected);
},
);
it("preserves a concurrently created first-install destination", async () => {
const home = await createTestTempDirectory(
"rea-skill-first-install-conflict-",
);
const manifest = join(skillDestinations(home, [])[0]!.path, "SKILL.md");
const result = await installCanonicalSkill(home, [], {}, process.platform, {
...skillPublicationOperations,
prepare: async (...args) => {
const publication = await SkillFilePublication.prepare(...args);
if (publication.path === manifest)
await writeFile(manifest, "concurrent user file\n", { flag: "wx" });
return publication;
},
});
if (result.ok) throw new Error("Expected destination conflict");
expect(result.error.rollbackFailures).toEqual([]);
expect(await readFile(manifest, "utf8")).toBe("concurrent user file\n");
expect(await readdir(dirname(manifest))).toEqual([
"SKILL.md",
"references",
]);
});
});
describe("skill publication recovery", () => {
it.each([
Buffer.from([0xff, 0xfe, 0x61, 0x00]),
Buffer.from("\ufefforiginal BOM instructions\r\n"),
])(
"restores byte-exact originals and mode after ambiguous readback",
async (bytes) => {
const home = await createTestTempDirectory(
"rea-skill-readback-recovery-",
);
const manifest = join(skillDestinations(home, [])[0]!.path, "SKILL.md");
await mkdir(dirname(manifest), { recursive: true });
await writeFile(manifest, bytes, { mode: 0o640 });
const result = await installCanonicalSkill(
home,
[],
{},
process.platform,
{
...skillPublicationOperations,
prepare: async (...args) => {
const publication = await SkillFilePublication.prepare(...args);
if (publication.path === manifest)
vi.spyOn(publication, "assertCurrent").mockRejectedValueOnce(
new Error("readback unavailable"),
);
return publication;
},
},
);
if (result.ok) throw new Error("Expected readback failure");
expect(result.error.stage).toBe("readback");
expect(result.error.reason).toBe("readback unavailable");
expect(await readFile(manifest)).toEqual(bytes);
expect(await readFile(`${manifest}.rea.backup`)).toEqual(bytes);
if (process.platform !== "win32")
expect((await stat(manifest)).mode & 0o777).toBe(0o640);
expect(
result.error.rollbackFailures.every(({ reason }) =>
reason.includes("First-install skill recovery"),
),
).toBe(true);
expect(result.error.rollbackFailures).toHaveLength(6);
},
);
it("retains a first-install publication after failed verification", async () => {
const home = await createTestTempDirectory(
"rea-skill-first-install-readback-",
);
const manifest = join(skillDestinations(home, [])[0]!.path, "SKILL.md");
const result = await installCanonicalSkill(home, [], {}, process.platform, {
...skillPublicationOperations,
prepare: async (...args) => {
const publication = await SkillFilePublication.prepare(...args);
if (publication.path === manifest)
vi.spyOn(publication, "assertCurrent").mockRejectedValueOnce(
new Error("readback unavailable"),
);
return publication;
},
});
if (result.ok) throw new Error("Expected readback failure");
expect(result.error.rollbackFailures).toHaveLength(7);
expect(
result.error.rollbackFailures.every(({ reason }) =>
reason.includes("manual removal"),
),
).toBe(true);
expect(await canonicalSkillNeedsInstall(home)).toBe(false);
expect(
(await readdir(dirname(manifest))).some((name) =>
name.startsWith(".rea-skill-"),
),
).toBe(false);
});
it.each([false, true])(
"retains exact staging cleanup for retry (scope: %s)",
async (scoped) => {
const resources = scoped ? new ArtifactResourceScope() : undefined;
const home = await createTestTempDirectory("rea-skill-cleanup-retry-");
const manifest = join(skillDestinations(home, [])[0]!.path, "SKILL.md");
let closeAttempts = 0;
const result = await installCanonicalSkill(
home,
[],
{},
process.platform,
{
...skillPublicationOperations,
prepare: async (...args) => {
const publication = await SkillFilePublication.prepare(...args);
if (publication.path === manifest) {
const close = publication.lease.release.bind(publication.lease);
vi.spyOn(publication.lease, "release").mockImplementation(
async () => {
if (++closeAttempts <= 2)
throw new Error("staging close denied");
await close();
},
);
}
return publication;
},
},
resources,
);
if (result.ok) throw new Error("Expected cleanup failure");
expect(result.error.reason).toContain("cleanup incomplete");
expect(result.error.rollbackFailures).toEqual([
{
path: expect.stringContaining(".rea-skill-"),
reason: "staging close denied",
},
]);
expect(await canonicalSkillNeedsInstall(home)).toBe(false);
expect(resources?.hasPending).toBe(scoped ? true : undefined);
if (resources !== undefined) {
await resources.retryCleanup();
expect(resources.hasPending).toBe(false);
} else expect(await result.error.retryCleanup()).toEqual([]);
expect(closeAttempts).toBe(3);
expect(
(await readdir(dirname(manifest))).some((name) =>
name.startsWith(".rea-skill-"),
),
).toBe(false);
},
);
});
describe("skill staging preparation", () => {
it("retains the primary failure and staging owner when preparation cleanup fails", async () => {
const home = await createTestTempDirectory("rea-skill-prepare-cleanup-");
const manifest = join(skillDestinations(home, [])[0]!.path, "SKILL.md");
await mkdir(dirname(manifest), { recursive: true });
await writeFile(manifest, "original instructions\n");
const ready = vi
.spyOn(OwnedStagedOutput.prototype, "assertReadyForPublication")
.mockRejectedValueOnce(new Error("staging verification denied"));
const close = vi
.spyOn(OwnedStagedOutput.prototype, "close")
.mockRejectedValueOnce(new Error("staging cleanup denied"))
.mockRejectedValueOnce(new Error("staging cleanup still denied"));
try {
const result = await installCanonicalSkill(home);
if (result.ok) throw new Error("Expected preparation failure");
expect(result.error.reason).toContain("staging verification denied");
expect(result.error.reason).toContain("staging cleanup denied");
expect(result.error.rollbackFailures).toEqual([
{
path: expect.stringContaining(".rea-skill-"),
reason: "staging cleanup still denied",
},
]);
expect(await readFile(manifest, "utf8")).toBe("original instructions\n");
expect(await result.error.retryCleanup()).toEqual([]);
expect(close).toHaveBeenCalledTimes(3);
expect(
(await readdir(dirname(manifest))).some((name) =>
name.startsWith(".rea-skill-"),
),
).toBe(false);
} finally {
ready.mockRestore();
close.mockRestore();
}
});
});