Files
rea/tests/boundary/filesystem/setupSkillPublication.test.ts
T
morluto 21ff4911bf refactor(setup): share publication byte verification
Use one bounded positional byte comparator for client configuration and skill
publication readback. Keep pathname identity, rollback, mode restoration,
cleanup and retry authority in the existing publication owners.

Preserve short-read progress, EOF, empty-file equality and original I/O errors.
Extend existing byte-exact skill recovery coverage to empty originals.

Validation: client configuration transaction, skill publication recovery and
process-global rollback suites passed; static, docs and discovery checks passed.
2026-10-24 02:57:11 +08:00

295 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.alloc(0),
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();
}
});
});