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
64
build/eval/interview/test/scenarios.test.mjs
Normal file
64
build/eval/interview/test/scenarios.test.mjs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// scenarios.json 의 정적 검증. 시나리오 정의만 더하고 실행은 하지 않은 항목(S19~S21)이 깨진 채 남지 않게 한다.
|
||||
//
|
||||
// node --test build/eval/interview/test/scenarios.test.mjs
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.join(HERE, "..");
|
||||
const SCENARIOS = JSON.parse(fs.readFileSync(path.join(ROOT, "scenarios.json"), "utf8"));
|
||||
const byId = Object.fromEntries(SCENARIOS.map((s) => [s.id, s]));
|
||||
|
||||
function filesUnder(dir) {
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => (e.isDirectory() ? filesUnder(path.join(dir, e.name)) : [path.join(dir, e.name)]));
|
||||
}
|
||||
|
||||
test("시나리오 ID 는 겹치지 않고 S1 부터 이어진다", () => {
|
||||
assert.deepEqual(SCENARIOS.map((s) => s.id), SCENARIOS.map((_, i) => `S${i + 1}`));
|
||||
});
|
||||
|
||||
test("필수 필드와 픽스처 디렉터리가 있다", () => {
|
||||
for (const s of SCENARIOS) {
|
||||
assert.ok(["ask", "no-interview", "no-trigger", "direction-checkpoint"].includes(s.expect), `${s.id}: expect ${s.expect}`);
|
||||
assert.equal(typeof s.prompt, "string", `${s.id}: prompt`);
|
||||
if (s.fixture) assert.ok(fs.statSync(path.join(ROOT, "fixtures", s.fixture)).isDirectory(), `${s.id}: fixtures/${s.fixture}`);
|
||||
if (s.cardMode !== undefined) {
|
||||
assert.equal(s.cardMode, "interpretation", `${s.id}: cardMode`);
|
||||
assert.equal(s.expect, "direction-checkpoint", `${s.id}: cardMode 는 direction-checkpoint 에만 쓴다`);
|
||||
}
|
||||
if (s.maxSkillReadTokens !== undefined) assert.ok(Number.isInteger(s.maxSkillReadTokens) && s.maxSkillReadTokens > 0, `${s.id}: maxSkillReadTokens`);
|
||||
}
|
||||
});
|
||||
|
||||
test("새 시나리오(S19~S21)의 픽스처는 시나리오의 전제를 실제로 만든다", () => {
|
||||
const fx = (s) => path.join(ROOT, "fixtures", byId[s].fixture);
|
||||
const names = (s) => filesUnder(fx(s)).map((p) => path.relative(fx(s), p).split(path.sep).join("/")).sort();
|
||||
// S19(J-01): design.md 가 없고, 현행 시각 언어가 코드에 있다(토큰 파일 + 같은 스타일을 쓰는 화면 둘).
|
||||
assert.deepEqual(names("S19"), ["features.html", "index.html", "styles/site.css", "styles/tokens.css"]);
|
||||
for (const page of ["index.html", "features.html"]) assert.match(fs.readFileSync(path.join(fx("S19"), page), "utf8"), /styles\/site\.css/);
|
||||
assert.match(fs.readFileSync(path.join(fx("S19"), "styles/tokens.css"), "utf8"), /--color-accent:[\s\S]*--font-display:/);
|
||||
// S20(J-12): design.md 가 있고 hover 용 토큰이 없다 — 고치려면 파생 상태 토큰을 더해야 한다.
|
||||
assert.deepEqual(names("S20"), ["design.md", "index.html"]);
|
||||
for (const f of names("S20")) assert.doesNotMatch(fs.readFileSync(path.join(fx("S20"), f), "utf8"), /--[a-z-]*hover/);
|
||||
assert.match(fs.readFileSync(path.join(fx("S20"), "design.md"), "utf8"), /색은 토큰으로만 쓴다/);
|
||||
assert.equal(byId.S20.prompt, byId.S6.prompt);
|
||||
// S21(J-08): 프롬프트가 가리키는 시안 파일이 있다.
|
||||
const mock = /refs\/[\w.-]+\.svg/.exec(byId.S21.prompt)?.[0];
|
||||
assert.ok(mock && names("S21").includes(mock), `S21 프롬프트의 시안 파일이 픽스처에 없다: ${mock}`);
|
||||
});
|
||||
|
||||
test("픽스처는 작고(시나리오당 8KB 이하) LF 줄끝이다", () => {
|
||||
for (const s of ["S19", "S20", "S21"]) {
|
||||
const files = filesUnder(path.join(ROOT, "fixtures", byId[s].fixture));
|
||||
let total = 0;
|
||||
for (const f of files) {
|
||||
const buf = fs.readFileSync(f);
|
||||
total += buf.length;
|
||||
assert.equal(buf.indexOf(0x0d), -1, `${f}: CR 줄끝`);
|
||||
}
|
||||
assert.ok(total <= 8 * 1024, `${s}: 픽스처 ${total}바이트`);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue