mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
* refactor(setup): require the platform for skill destinations instead of reading the host The setup skill owner defaulted seven entry points to process.platform. Every production caller (setup host, doctor, uninstall, update maintenance) already passes the platform it was composed with, so the defaults only let a caller silently mix the host REA runs on with the one it asked about. The platform is now a required parameter; tests name the platform they exercise, the setup fixture passes its declared host, and the ambient-read baseline drops SetupSkill.ts (7 to 0). * test(setup): expect the fake host's POSIX skill paths in planning tests
89 lines
3.2 KiB
TypeScript
89 lines
3.2 KiB
TypeScript
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { expect } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import {
|
|
MANAGED_SKILL_FILES,
|
|
existingSkillDestinations,
|
|
validateCanonicalSkillDirectories,
|
|
} from "../../../src/application/SetupSkill.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
cliTest.for(["ancestor", "selected-root"] as const)(
|
|
"explains a non-directory skill %s and recovers after preserving the blocker",
|
|
async (kind, { cli }) => {
|
|
const home = await createTestTempDirectory("rea-skill-path-recovery-");
|
|
const selected = join(home, ".agents/skills/reverse-engineer-anything");
|
|
const blocker = kind === "ancestor" ? join(home, ".agents") : selected;
|
|
await mkdir(dirname(blocker), { recursive: true });
|
|
await writeFile(blocker, "caller-owned blocker\n");
|
|
const run = (
|
|
arguments_: readonly string[] = ["setup", "--yes", "--skill", "--json"],
|
|
) =>
|
|
cli.run({
|
|
arguments: arguments_,
|
|
cwd: home,
|
|
environment: { HOME: home, USERPROFILE: home },
|
|
});
|
|
expect(
|
|
await validateCanonicalSkillDirectories(home, [], {}, process.platform),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
stage: "prepare",
|
|
cause: [{ path: selected, kind: "not_directory" }],
|
|
},
|
|
});
|
|
expect(await existingSkillDestinations(home, {}, process.platform)).toEqual(
|
|
{
|
|
destinations: [],
|
|
issues: [],
|
|
},
|
|
);
|
|
const refused = await run();
|
|
expect(refused.exitCode, refused.stderr).toBe(1);
|
|
expect(refused.json).toMatchObject({
|
|
status: "needs_human",
|
|
code: "skill_install_failed",
|
|
appliedActions: [],
|
|
});
|
|
const { remediation } = z
|
|
.object({ remediation: z.string() })
|
|
.parse(refused.json);
|
|
expect(remediation).toContain("not a directory");
|
|
expect(remediation).toContain("Use a regular skill directory");
|
|
expect(remediation).toContain("--skill=false");
|
|
expect(remediation).not.toMatch(/permissions/iu);
|
|
expect(remediation.split(selected)).toHaveLength(2);
|
|
if (kind === "ancestor") expect(remediation).toContain("ENOTDIR");
|
|
expect(await readFile(blocker, "utf8")).toBe("caller-owned blocker\n");
|
|
|
|
const doctor = await run(["doctor", "--skill", "--json"]);
|
|
expect(doctor.exitCode, doctor.stderr).toBe(1);
|
|
expect(doctor.json).toMatchObject({
|
|
healthy: false,
|
|
identity: { skill: { state: "unmanaged" } },
|
|
});
|
|
expect(doctor.stdout).toContain(selected);
|
|
|
|
const saved = `${blocker}.saved`;
|
|
await rename(blocker, saved);
|
|
await mkdir(blocker);
|
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
const installed = await run();
|
|
expect(installed.exitCode, installed.stderr).toBe(0);
|
|
expect(installed.json).toMatchObject({ status: "ready" });
|
|
for (const file of MANAGED_SKILL_FILES)
|
|
expect(
|
|
(await readFile(join(selected, file))).equals(
|
|
await readFile(resolve("skills/reverse-engineer-anything", file)),
|
|
),
|
|
file,
|
|
).toBe(true);
|
|
expect(await readFile(saved, "utf8")).toBe("caller-owned blocker\n");
|
|
}
|
|
},
|
|
);
|