ef1bd6dfa9
The report writer had no interpretation methodology at all: a local agent calling the jyotish skill can read the reference library, the report model could read nothing. It could only restate the bundle. - frontend/src/lib/report-interpretation-packs/ holds one general pack and one pack per report theme, distilled from the in-repo reference guides. They constrain wording and reasoning discipline (term modernisation, how to talk about relative strength and SAV scores, the reasoning errors to avoid, the banned phrasings) and never assert a chart fact. - The general pack rides INSIDE the cached system message so the cached prefix stays byte-identical across sections; the chapter pack follows it and summary calls get the general pack only. - Skill jyotish-personal-report goes to 1.1.0 (1.0.0 deprecated): the contract now names interpretiveFacts and themeNarrativeSeeds as a bounded fact layer and states that the knowledge pack is not a fact source and cannot raise certainty. - Telemetry records interpretiveFactCount and knowledgePackCharacters as numbers only; the counter never throws so telemetry cannot break a run. Tests lock every theme resolving a pack, the 3,000 character budget, a forbidden-substring scan (paths, module names, vendor names, artefact names), the byte-stable cache prefix, and that no evidence id or date appears in the static content. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016P5RoqzmUQEbeC2qjAkeGr
556 lines
18 KiB
TypeScript
556 lines
18 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import {
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
realpathSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
existsSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { test, type TestContext } from "node:test";
|
|
import {
|
|
computeSkillPackageSha256,
|
|
createSkillPackageRegistry,
|
|
resolveActiveSkillPackage,
|
|
resolveExactSkillPackage,
|
|
resolveLiveJyotishSkill,
|
|
resolveLiveJyotishSkillRuntimePath,
|
|
resolveSkillPackageVersion,
|
|
verifyAllActiveSkillPackages,
|
|
type SkillPackageIdentity,
|
|
} from "../src/lib/skill-package-registry.ts";
|
|
|
|
const sourceCommit = "0fd111d16b45796086a6c1d0945dbd3de6755d8a";
|
|
const projectRoot = fileURLToPath(new URL("../../", import.meta.url));
|
|
|
|
type RegistryEntry = SkillPackageIdentity | Record<string, unknown>;
|
|
|
|
function sha256(bytes: string | Buffer): string {
|
|
return createHash("sha256").update(bytes).digest("hex");
|
|
}
|
|
|
|
function fixture(t: TestContext): {
|
|
root: string;
|
|
registryPath: string;
|
|
writeSkill: (packagePath: string, contents: string) => string;
|
|
writeRegistry: (packages: RegistryEntry[]) => void;
|
|
} {
|
|
const root = mkdtempSync(join(tmpdir(), "skill-registry-test-"));
|
|
const registryPath = join(root, "skills", "skill-package-registry.json");
|
|
mkdirSync(dirname(registryPath), { recursive: true });
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
|
|
return {
|
|
root,
|
|
registryPath,
|
|
writeSkill(packagePath, contents) {
|
|
const directory = join(root, packagePath);
|
|
mkdirSync(directory, { recursive: true });
|
|
writeFileSync(join(directory, "SKILL.md"), contents);
|
|
return computeSkillPackageSha256(directory);
|
|
},
|
|
writeRegistry(packages) {
|
|
writeFileSync(
|
|
registryPath,
|
|
`${JSON.stringify({ schemaVersion: 1, packages }, null, 2)}\n`,
|
|
);
|
|
},
|
|
};
|
|
}
|
|
|
|
function identity(
|
|
overrides: Partial<SkillPackageIdentity> = {},
|
|
): SkillPackageIdentity {
|
|
return {
|
|
name: "example-skill",
|
|
version: "1.0.0",
|
|
sha256: "a".repeat(64),
|
|
sourceCommit,
|
|
packagePath: "skills/example-skill-1.0.0",
|
|
status: "active",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("checked-in registry verifies hashed product packages and leaves consult on the live skill", () => {
|
|
const packages = verifyAllActiveSkillPackages({ projectRoot });
|
|
assert.deepEqual(
|
|
packages.map(({ name, version, sha256 }) => ({ name, version, sha256 })),
|
|
[
|
|
{
|
|
name: "jyotish-birth-time-rectification",
|
|
version: "10.0.14",
|
|
sha256: "60234b65badd0d69206d4a9c500d674005d1cb9aa2add58c133e538de4ad0546",
|
|
},
|
|
{
|
|
name: "jyotish-personal-report",
|
|
// was 1.0.0 / 23149b9e...982; bumped 2026-09-01 with the interpretive
|
|
// fact layer and the static interpretation packs.
|
|
version: "1.1.0",
|
|
sha256: "6be2279b69b7da9446adeb508e27746c5aeb8b00dc9f5921f48f82bd8a8010e9",
|
|
},
|
|
],
|
|
);
|
|
for (const resolvedPackage of packages) {
|
|
assert.equal(isAbsolute(resolvedPackage.resolvedPath), true);
|
|
assert.equal(
|
|
resolvedPackage.resolvedPath,
|
|
resolve(projectRoot, resolvedPackage.packagePath),
|
|
);
|
|
}
|
|
assert.equal(packages[0]?.sourceCommit, null);
|
|
assert.equal(packages[1]?.sourceCommit, null);
|
|
|
|
assert.throws(
|
|
() => resolveActiveSkillPackage("jyotish-vedic-astrology", { projectRoot }),
|
|
/No active skill package found/,
|
|
);
|
|
const live = resolveLiveJyotishSkill({ projectRoot });
|
|
assert.equal(live.name, "jyotish-vedic-astrology");
|
|
assert.equal(basename(live.resolvedPath), "jyotish-vedic-astrology");
|
|
assert.equal(live.resolvedPath, resolve(projectRoot, "skills/jyotish-vedic-astrology"));
|
|
const liveRuntime = resolveLiveJyotishSkillRuntimePath(live);
|
|
assert.equal(basename(liveRuntime), live.name);
|
|
assert.equal(existsSync(join(liveRuntime, "versions")), false);
|
|
assert.equal(
|
|
realpathSync(join(liveRuntime, "SKILL.md")),
|
|
realpathSync(join(live.resolvedPath, "SKILL.md")),
|
|
);
|
|
const deprecatedRectification = resolveExactSkillPackage(
|
|
"jyotish-birth-time-rectification",
|
|
"10.0.0",
|
|
"b66f243d266e12527b6934cc0c5925df654a9e7bcbaabea3bf244b2e1dfaf2e7",
|
|
{ projectRoot },
|
|
);
|
|
assert.equal(deprecatedRectification.status, "deprecated");
|
|
assert.equal(
|
|
deprecatedRectification.sha256,
|
|
"b66f243d266e12527b6934cc0c5925df654a9e7bcbaabea3bf244b2e1dfaf2e7",
|
|
);
|
|
const deprecated1002 = resolveExactSkillPackage(
|
|
"jyotish-birth-time-rectification",
|
|
"10.0.2",
|
|
"8d7aa2d4bea0414e9a89ef908ccbc8c708c98f79f5b78ae4f7dc229b5f7dbb30",
|
|
{ projectRoot },
|
|
);
|
|
assert.equal(deprecated1002.status, "deprecated");
|
|
assert.equal(
|
|
deprecated1002.sha256,
|
|
"8d7aa2d4bea0414e9a89ef908ccbc8c708c98f79f5b78ae4f7dc229b5f7dbb30",
|
|
);
|
|
const deprecated1011 = resolveExactSkillPackage(
|
|
"jyotish-birth-time-rectification",
|
|
"10.0.11",
|
|
"66a3282be7b227bb6f77038455b8bc17ddaadaf0279a8b9a94284e9d186975c9",
|
|
{ projectRoot },
|
|
);
|
|
assert.equal(deprecated1011.status, "deprecated");
|
|
});
|
|
|
|
test("exact resolution of an old deprecated package is independent of active switches", (t) => {
|
|
const f = fixture(t);
|
|
const oldHash = f.writeSkill("skills/example-1.0.0", "old immutable bytes\n");
|
|
const currentHash = f.writeSkill(
|
|
"skills/example-2.0.0",
|
|
"current immutable bytes\n",
|
|
);
|
|
const nextHash = f.writeSkill("skills/example-3.0.0", "next immutable bytes\n");
|
|
const oldIdentity = identity({
|
|
version: "1.0.0",
|
|
sha256: oldHash,
|
|
packagePath: "skills/example-1.0.0",
|
|
status: "deprecated",
|
|
});
|
|
const currentIdentity = identity({
|
|
version: "2.0.0",
|
|
sha256: currentHash,
|
|
packagePath: "skills/example-2.0.0",
|
|
});
|
|
const nextIdentity = identity({
|
|
version: "3.0.0",
|
|
sha256: nextHash,
|
|
packagePath: "skills/example-3.0.0",
|
|
status: "deprecated",
|
|
});
|
|
f.writeRegistry([oldIdentity, currentIdentity, nextIdentity]);
|
|
|
|
const registry = createSkillPackageRegistry({
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
});
|
|
assert.equal(registry.resolveActive("example-skill").version, "2.0.0");
|
|
assert.deepEqual(registry.resolveExact("example-skill", "1.0.0", oldHash), {
|
|
...oldIdentity,
|
|
resolvedPath: realpathSync(join(f.root, "skills/example-1.0.0")),
|
|
});
|
|
|
|
f.writeRegistry([
|
|
oldIdentity,
|
|
{ ...currentIdentity, status: "deprecated" },
|
|
{ ...nextIdentity, status: "active" },
|
|
]);
|
|
assert.equal(registry.resolveActive("example-skill").version, "3.0.0");
|
|
assert.equal(
|
|
registry.resolveExact("example-skill", "1.0.0", oldHash).sha256,
|
|
oldHash,
|
|
);
|
|
});
|
|
|
|
test("version resolution requires one non-blocked identity and verifies its bytes", (t) => {
|
|
const f = fixture(t);
|
|
const oldHash = f.writeSkill("skills/example-old", "old version\n");
|
|
const oldIdentity = identity({
|
|
sha256: oldHash,
|
|
packagePath: "skills/example-old",
|
|
status: "deprecated",
|
|
});
|
|
f.writeRegistry([oldIdentity]);
|
|
|
|
const registry = createSkillPackageRegistry({
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
});
|
|
assert.throws(() => registry.resolveActive("example-skill"), /No active/);
|
|
|
|
assert.deepEqual(
|
|
resolveSkillPackageVersion("example-skill", "1.0.0", {
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
}),
|
|
{
|
|
...oldIdentity,
|
|
resolvedPath: realpathSync(join(f.root, "skills/example-old")),
|
|
},
|
|
);
|
|
|
|
const alternateHash = f.writeSkill(
|
|
"skills/example-old-repacked",
|
|
"different bytes\n",
|
|
);
|
|
f.writeRegistry([
|
|
oldIdentity,
|
|
identity({
|
|
sha256: alternateHash,
|
|
packagePath: "skills/example-old-repacked",
|
|
status: "deprecated",
|
|
}),
|
|
]);
|
|
assert.throws(
|
|
() =>
|
|
resolveSkillPackageVersion("example-skill", "1.0.0", {
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
}),
|
|
/Expected one registry identity/,
|
|
);
|
|
|
|
f.writeRegistry([{ ...oldIdentity, status: "blocked" }]);
|
|
assert.throws(
|
|
() =>
|
|
resolveSkillPackageVersion("example-skill", "1.0.0", {
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
}),
|
|
/blocked/,
|
|
);
|
|
});
|
|
|
|
test("missing lookup identities fail closed", (t) => {
|
|
const f = fixture(t);
|
|
f.writeRegistry([]);
|
|
const options = { projectRoot: f.root, registryPath: f.registryPath };
|
|
const registry = createSkillPackageRegistry(options);
|
|
|
|
assert.throws(() => registry.resolveActive("example-skill"), /No active/);
|
|
assert.throws(
|
|
() => registry.resolveExact("example-skill", "1.0.0", "a".repeat(64)),
|
|
/No exact skill package/,
|
|
);
|
|
assert.throws(
|
|
() => resolveSkillPackageVersion("example-skill", "1.0.0", options),
|
|
/found 0/,
|
|
);
|
|
});
|
|
|
|
test("missing identity fields fail closed", (t) => {
|
|
const f = fixture(t);
|
|
f.writeSkill("skills/example-skill-1.0.0", "bytes\n");
|
|
const missingHash: Record<string, unknown> = { ...identity() };
|
|
delete missingHash.sha256;
|
|
f.writeRegistry([missingHash]);
|
|
|
|
assert.throws(
|
|
() =>
|
|
createSkillPackageRegistry({
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
}).resolveActive("example-skill"),
|
|
/missing sha256/,
|
|
);
|
|
});
|
|
|
|
test("hash mismatch fails closed after reading the complete package tree", (t) => {
|
|
const f = fixture(t);
|
|
const packagePath = "skills/example-skill-1.0.0";
|
|
const expectedHash = f.writeSkill(packagePath, "expected bytes\n");
|
|
mkdirSync(join(f.root, packagePath, "references"), { recursive: true });
|
|
writeFileSync(
|
|
join(f.root, packagePath, "references", "runtime-policy.md"),
|
|
"tampered reference bytes\n",
|
|
);
|
|
f.writeRegistry([identity({ sha256: expectedHash })]);
|
|
|
|
assert.throws(
|
|
() =>
|
|
createSkillPackageRegistry({
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
}).resolveActive("example-skill"),
|
|
/SHA-256 mismatch/,
|
|
);
|
|
});
|
|
|
|
test("package hash covers paths, executable bits, references, scripts, and assets", (t) => {
|
|
const f = fixture(t);
|
|
const packageDirectory = join(f.root, "skills", "example-skill-1.0.0");
|
|
mkdirSync(join(packageDirectory, "references"), { recursive: true });
|
|
mkdirSync(join(packageDirectory, "scripts"), { recursive: true });
|
|
mkdirSync(join(packageDirectory, "assets"), { recursive: true });
|
|
writeFileSync(join(packageDirectory, "SKILL.md"), "skill bytes\n");
|
|
writeFileSync(join(packageDirectory, "references", "guide.md"), "guide v1\n");
|
|
writeFileSync(join(packageDirectory, "scripts", "run.py"), "print('v1')\n");
|
|
writeFileSync(join(packageDirectory, "assets", "template.txt"), "asset v1\n");
|
|
|
|
const initial = computeSkillPackageSha256(packageDirectory);
|
|
writeFileSync(join(packageDirectory, "references", "guide.md"), "guide v2\n");
|
|
const changedReference = computeSkillPackageSha256(packageDirectory);
|
|
assert.notEqual(changedReference, initial);
|
|
|
|
writeFileSync(join(packageDirectory, "references", "guide.md"), "guide v1\n");
|
|
writeFileSync(join(packageDirectory, "assets", "template.txt"), "asset v2\n");
|
|
assert.notEqual(computeSkillPackageSha256(packageDirectory), initial);
|
|
});
|
|
|
|
test("path traversal and symlink escape fail closed", (t) => {
|
|
const f = fixture(t);
|
|
f.writeRegistry([
|
|
identity({
|
|
packagePath: "skills/../outside",
|
|
sha256: sha256("outside\n"),
|
|
}),
|
|
]);
|
|
assert.throws(
|
|
() =>
|
|
createSkillPackageRegistry({
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
}).resolveActive("example-skill"),
|
|
/unsafe packagePath/,
|
|
);
|
|
|
|
const outside = mkdtempSync(join(tmpdir(), "skill-registry-outside-"));
|
|
t.after(() => rmSync(outside, { recursive: true, force: true }));
|
|
writeFileSync(join(outside, "SKILL.md"), "outside\n");
|
|
const packageDirectory = join(f.root, "skills", "symlinked-skill");
|
|
mkdirSync(packageDirectory, { recursive: true });
|
|
symlinkSync(join(outside, "SKILL.md"), join(packageDirectory, "SKILL.md"));
|
|
f.writeRegistry([
|
|
identity({
|
|
packagePath: "skills/symlinked-skill",
|
|
sha256: sha256("outside\n"),
|
|
}),
|
|
]);
|
|
assert.throws(
|
|
() =>
|
|
createSkillPackageRegistry({
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
}).resolveActive("example-skill"),
|
|
/resolves outside the project root/,
|
|
);
|
|
});
|
|
|
|
test("symbolic links are rejected even when their target stays inside the project root", (t) => {
|
|
const f = fixture(t);
|
|
const packageDirectory = join(f.root, "skills", "symlinked-skill");
|
|
const sharedDirectory = join(f.root, "shared");
|
|
mkdirSync(packageDirectory, { recursive: true });
|
|
mkdirSync(sharedDirectory, { recursive: true });
|
|
writeFileSync(join(packageDirectory, "SKILL.md"), "skill bytes\n");
|
|
writeFileSync(join(sharedDirectory, "guide.md"), "shared bytes\n");
|
|
symlinkSync(sharedDirectory, join(packageDirectory, "references"));
|
|
f.writeRegistry([
|
|
identity({
|
|
packagePath: "skills/symlinked-skill",
|
|
sha256: "a".repeat(64),
|
|
}),
|
|
]);
|
|
|
|
assert.throws(
|
|
() =>
|
|
createSkillPackageRegistry({
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
}).resolveActive("example-skill"),
|
|
/must not be a symbolic link/,
|
|
);
|
|
});
|
|
|
|
test("blocked packages are rejected by active, exact, and version resolution", (t) => {
|
|
const f = fixture(t);
|
|
const hash = f.writeSkill("skills/example-skill-1.0.0", "blocked bytes\n");
|
|
f.writeRegistry([identity({ sha256: hash, status: "blocked" })]);
|
|
const registry = createSkillPackageRegistry({
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
});
|
|
|
|
assert.throws(() => registry.resolveActive("example-skill"), /No active/);
|
|
assert.throws(
|
|
() => registry.resolveExact("example-skill", "1.0.0", hash),
|
|
/blocked/,
|
|
);
|
|
assert.throws(
|
|
() => registry.resolveVersion("example-skill", "1.0.0"),
|
|
/blocked/,
|
|
);
|
|
});
|
|
|
|
test("duplicate active packages fail the entire registry closed", (t) => {
|
|
const f = fixture(t);
|
|
const firstHash = f.writeSkill("skills/example-1.0.0", "first\n");
|
|
const secondHash = f.writeSkill("skills/example-2.0.0", "second\n");
|
|
f.writeRegistry([
|
|
identity({
|
|
sha256: firstHash,
|
|
packagePath: "skills/example-1.0.0",
|
|
}),
|
|
identity({
|
|
version: "2.0.0",
|
|
sha256: secondHash,
|
|
packagePath: "skills/example-2.0.0",
|
|
}),
|
|
]);
|
|
|
|
const registry = createSkillPackageRegistry({
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
});
|
|
assert.throws(() => registry.resolveActive("example-skill"), /duplicate active/);
|
|
assert.throws(() => registry.verifyAllActive(), /duplicate active/);
|
|
});
|
|
|
|
test("nullable source commits are accepted while malformed non-null commits fail closed", (t) => {
|
|
const f = fixture(t);
|
|
const hash = f.writeSkill("skills/example-skill-1.0.0", "bytes\n");
|
|
f.writeRegistry([identity({ sha256: hash, sourceCommit: null })]);
|
|
|
|
const resolved = createSkillPackageRegistry({
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
}).resolveActive("example-skill");
|
|
assert.equal(resolved.sourceCommit, null);
|
|
|
|
f.writeRegistry([identity({ sha256: hash, sourceCommit: "not-a-commit" })]);
|
|
assert.throws(
|
|
() =>
|
|
createSkillPackageRegistry({
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
}).resolveActive("example-skill"),
|
|
/invalid sourceCommit/,
|
|
);
|
|
});
|
|
|
|
test("invalid hash, commit, and non-directory package paths fail closed", (t) => {
|
|
const f = fixture(t);
|
|
f.writeRegistry([identity({ sha256: "A".repeat(64) })]);
|
|
assert.throws(
|
|
() =>
|
|
createSkillPackageRegistry({
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
}).resolveActive("example-skill"),
|
|
/invalid sha256/,
|
|
);
|
|
|
|
f.writeRegistry([identity({ sourceCommit: "not-a-commit" })]);
|
|
assert.throws(
|
|
() =>
|
|
createSkillPackageRegistry({
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
}).resolveActive("example-skill"),
|
|
/invalid sourceCommit/,
|
|
);
|
|
|
|
const filePath = join(f.root, "skills", "not-a-directory");
|
|
writeFileSync(filePath, "file\n");
|
|
f.writeRegistry([
|
|
identity({
|
|
packagePath: "skills/not-a-directory",
|
|
sha256: sha256("file\n"),
|
|
}),
|
|
]);
|
|
assert.throws(
|
|
() =>
|
|
createSkillPackageRegistry({
|
|
projectRoot: f.root,
|
|
registryPath: f.registryPath,
|
|
}).resolveActive("example-skill"),
|
|
/must be a directory/,
|
|
);
|
|
});
|
|
|
|
test("live consult skill reads a hand-updated tree without a registry hash", (t) => {
|
|
const f = fixture(t);
|
|
const dir = join(f.root, "skills", "jyotish-vedic-astrology");
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(
|
|
join(dir, "SKILL.md"),
|
|
"---\nname: jyotish-vedic-astrology\nversion: 9.9.9\n---\n# live method\n",
|
|
);
|
|
mkdirSync(join(dir, "references"));
|
|
writeFileSync(join(dir, "references", "note.md"), "live reference\n");
|
|
mkdirSync(join(dir, "versions", "6.9.14"), { recursive: true });
|
|
writeFileSync(join(dir, "versions", "6.9.14", "SKILL.md"), "# stale snapshot\n");
|
|
|
|
const live = resolveLiveJyotishSkill({ projectRoot: f.root, skillPath: dir });
|
|
assert.equal(live.version, "9.9.9");
|
|
assert.equal(live.resolvedPath, realpathSync(dir));
|
|
const runtime = resolveLiveJyotishSkillRuntimePath(live);
|
|
assert.equal(existsSync(join(runtime, "versions")), false);
|
|
assert.match(readFileSync(join(runtime, "SKILL.md"), "utf8"), /live method/);
|
|
assert.equal(readFileSync(join(runtime, "references", "note.md"), "utf8"), "live reference\n");
|
|
});
|
|
|
|
test("live consult skill rejects a path outside the project", (t) => {
|
|
const f = fixture(t);
|
|
const dir = join(f.root, "skills", "jyotish-vedic-astrology");
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(join(dir, "SKILL.md"), "---\nname: jyotish-vedic-astrology\n---\n# live\n");
|
|
const outside = mkdtempSync(join(tmpdir(), "jyotish-skill-outside-"));
|
|
t.after(() => rmSync(outside, { recursive: true, force: true }));
|
|
assert.throws(
|
|
() => resolveLiveJyotishSkill({ projectRoot: f.root, skillPath: outside }),
|
|
/escapes the project root/,
|
|
);
|
|
});
|
|
|
|
test("live consult skill rejects a directory that is not named jyotish-vedic-astrology", (t) => {
|
|
const f = fixture(t);
|
|
const dir = join(f.root, "skills", "other-skill");
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(join(dir, "SKILL.md"), "---\nname: jyotish-vedic-astrology\n---\n# live\n");
|
|
assert.throws(
|
|
() => resolveLiveJyotishSkill({ projectRoot: f.root, skillPath: dir }),
|
|
/must be named jyotish-vedic-astrology/,
|
|
);
|
|
});
|