fix(setup): preserve selected home paths throughout skill installation

This commit is contained in:
morluto committed 2026-10-26 18:11:54 +08:00
1 parent 4ca3032e7f
commit f8161ad7b3
3 files changed
+105 -9

No files matched your search

@@ -109,3 +109,96 @@ cliTest.skipIf(process.platform === "win32")(
).toEqual([]);
},
);
cliTest.skipIf(process.platform === "win32")(
"installs shared skills in the native HOME selection with backups and repeat setup",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-selected-skill-home-");
const selected = join(root, "selected");
await mkdir(join(selected, "nested"), { recursive: true });
await symlink(join(selected, "nested"), join(root, "alias"));
const home = `${root}/alias/..`;
const relative = ".agents/skills/reverse-engineer-anything";
const skill = join(selected, relative);
const lexicalSkill = join(root, relative);
const original = "previous selected instructions\n";
const oldReference = "previous selected reference\n";
const sentinel = "unselected instructions\n";
const sentinelReference = "unselected reference\n";
const reference = "references/connection-and-recovery.md";
for (const directory of [skill, lexicalSkill])
await mkdir(join(directory, "references"), { recursive: true });
await writeFile(join(skill, "SKILL.md"), original);
await writeFile(join(skill, reference), oldReference);
await writeFile(join(lexicalSkill, "SKILL.md"), sentinel);
await writeFile(join(lexicalSkill, reference), sentinelReference);
const run = (arguments_: readonly string[]) =>
cli.run({
arguments: [...arguments_, "--json"],
cwd: root,
environment: { HOME: home, USERPROFILE: home },
});
const configured = await run([
"setup",
"--skill-client",
"shared",
"--yes",
]);
expect(configured.exitCode, configured.stdout + configured.stderr).toBe(0);
expect(configured.json).toMatchObject({
status: "ready",
appliedActions: ["installed_skill"],
});
const installed = await readFile(join(skill, "SKILL.md"), "utf8");
expect(installed).toContain("name: reverse-engineer-anything");
const installedReference = await readFile(join(skill, reference), "utf8");
expect(installedReference).not.toBe(oldReference);
expect(await readFile(join(skill, "SKILL.md.rea.backup"), "utf8")).toBe(
original,
);
expect(await readFile(`${join(skill, reference)}.rea.backup`, "utf8")).toBe(
oldReference,
);
expect(await readFile(join(lexicalSkill, "SKILL.md"), "utf8")).toBe(
sentinel,
);
expect(await readFile(join(lexicalSkill, reference), "utf8")).toBe(
sentinelReference,
);
for (const path of [
join(lexicalSkill, "SKILL.md.rea.backup"),
`${join(lexicalSkill, reference)}.rea.backup`,
])
await expect(access(path)).rejects.toMatchObject({ code: "ENOENT" });
const repeated = await run(["setup", "--skill-client", "shared", "--yes"]);
expect(repeated.exitCode, repeated.stdout + repeated.stderr).toBe(0);
expect(repeated.json).toMatchObject({
status: "ready",
appliedActions: [],
});
expect(await readFile(join(skill, "SKILL.md"), "utf8")).toBe(installed);
expect(await readFile(join(skill, reference), "utf8")).toBe(
installedReference,
);
const doctor = await run(["doctor", "--skill"]);
// Doctor also discovers client-specific locations. Gemini uses the lexical
// HOME sibling, whose deliberately stale sentinel must remain untouched.
expect(doctor.exitCode, doctor.stdout + doctor.stderr).toBe(1);
expect(doctor.json).toMatchObject({
identity: {
skill: {
state: "stale",
issues: [{ client: "gemini_cli", path: lexicalSkill, kind: "stale" }],
},
},
});
expect(
(await readdir(skill)).filter((name) => name.startsWith(".rea-skill-")),
).toEqual([]);
expect(
(await readdir(join(skill, "references"))).filter((name) =>
name.startsWith(".rea-skill-"),
),
).toEqual([]);
},
);