feat(core,cli): harden install engine and extend skill lint and interview eval
- update only touches the recorded project root; project installs do not follow symlinks or junctions; repeated --force rotates .orig backups; uninstall never deletes outside the recorded root - run npm/npx without a shell on Windows and pin optional tool versions - skip the update check in CI and non-interactive output - lint: cross-document section pointers, preflight row ids, gate rule numbers, per-document size budgets, gate copy equality - eval: scenarios S19-S21, route and read-cost rejudging, interpretation card judging, temp-dir previews and table-form cards no longer fail
This commit is contained in:
parent
ee3bef2ce5
commit
9bcb45cbd3
122 changed files with 5836 additions and 831 deletions
|
|
@ -19,9 +19,10 @@ after(async () => {
|
|||
});
|
||||
|
||||
/** 빌드 산출물을 사용자가 실행하는 방식 그대로 돌린다 */
|
||||
function run(args: string[]): string {
|
||||
function run(args: string[], cwd?: string): string {
|
||||
return execFileSync(process.execPath, [CLI, ...args, "--no-update-check"], {
|
||||
encoding: "utf8",
|
||||
...(cwd ? { cwd } : {}),
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: home,
|
||||
|
|
@ -120,3 +121,45 @@ test("설치 → doctor → 제거 왕복", { skip: !built }, async () => {
|
|||
run(["uninstall", "-y"]);
|
||||
await assert.rejects(() => fs.access(skillMd));
|
||||
});
|
||||
|
||||
test("update 는 project 설치를 기록된 root 에서 갱신하고 cwd 에는 아무것도 쓰지 않는다", { skip: !built }, async () => {
|
||||
// 보안 감사 P0 재현: repoA 에 project 설치 → 버전을 낮춤 → 무관한 repoB 에서 update.
|
||||
// 예전에는 repoB 의 AGENTS.md 에 블록이 주입되고 사용자가 만든 스킬이 백업 없이 덮였다.
|
||||
const repoA = path.join(home, "repoA");
|
||||
const repoB = path.join(home, "repoB");
|
||||
await fs.mkdir(repoA, { recursive: true });
|
||||
await fs.mkdir(path.join(repoB, ".claude", "skills", "designpaca"), { recursive: true });
|
||||
await fs.writeFile(path.join(repoB, "AGENTS.md"), "# repoB\n", "utf8");
|
||||
const userSkill = path.join(repoB, ".claude", "skills", "designpaca", "SKILL.md");
|
||||
await fs.writeFile(userSkill, "user own skill\n", "utf8");
|
||||
const listB = async () => (await fs.readdir(repoB, { recursive: true })).map(String).sort();
|
||||
const beforeB = await listB();
|
||||
|
||||
run(["install", "-t", "agents-md,claude-code", "-s", "project", "-y"], repoA);
|
||||
const manifestFile = path.join(home, ".designpaca", "manifest.json");
|
||||
const manifest = JSON.parse(await fs.readFile(manifestFile, "utf8"));
|
||||
assert.equal(manifest.installs.length, 2);
|
||||
for (const rec of manifest.installs) rec.version = "0.0.1";
|
||||
await fs.writeFile(manifestFile, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
||||
|
||||
run(["update"], repoB);
|
||||
|
||||
// repoB 는 그대로다
|
||||
assert.deepEqual(await listB(), beforeB);
|
||||
assert.equal(await fs.readFile(path.join(repoB, "AGENTS.md"), "utf8"), "# repoB\n");
|
||||
assert.equal(await fs.readFile(userSkill, "utf8"), "user own skill\n");
|
||||
|
||||
// repoA 기록만 새 버전으로 갱신됐고, 새 기록이 생기지 않았다
|
||||
const after = JSON.parse(await fs.readFile(manifestFile, "utf8"));
|
||||
const version = run(["--version"]).trim();
|
||||
assert.equal(after.installs.length, 2);
|
||||
for (const rec of after.installs) {
|
||||
assert.equal(rec.version, version);
|
||||
assert.ok(path.resolve(rec.root).startsWith(path.resolve(repoA)), `기록 root 가 repoA 밖: ${rec.root}`);
|
||||
}
|
||||
assert.ok((await fs.readFile(path.join(repoA, "AGENTS.md"), "utf8")).includes("designpaca:start"));
|
||||
|
||||
run(["uninstall", "-y"], repoB);
|
||||
await assert.rejects(() => fs.access(path.join(repoA, ".claude", "skills", "designpaca", "SKILL.md")));
|
||||
assert.equal(await fs.readFile(userSkill, "utf8"), "user own skill\n");
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue