- BUG-1049 (recurrence of BUG-504): the opening stem carries six examples and an example answer again and is server-owned on the zero-evidence opening; the body is two plain sentences (no 大运/盘面/代表分钟/精确到秒, no year, no question). Stem de-dup compares whole sentences / near-equality instead of a 12-char prefix, which had deleted the body's examples sentence sinceaa7ccb30(BUG-604) +dd8f35f7(BUG-648). - BUG-1050: plain step labels; a finished step label shows once and 「已完成 N 步」counts shown rows; failed rows read 「…未完成」 from the in-progress wording. - Skill 10.0.30 -> 10.0.31 (OpeningPolicy); 10.0.30 kept as deprecated. - VOICE / DESIGN / CHANGELOG / BUG_HISTORY / PROGRESS / real-device checklist and screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
636 lines
21 KiB
TypeScript
636 lines
21 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",
|
||
// 原值: 10.0.30 / ade7b748…102c
|
||
// 新值: 10.0.31 / 51a91251…13c1
|
||
// 原因: 开场改大白话、题干带例子和示例回答写进 Skill OpeningPolicy(BUG-1049,2026-09-26)
|
||
version: "10.0.31",
|
||
sha256: "51a9125192c4e8693ba4786cb4ec79c981ec8ad98248c2fae5631e66d8e913c1",
|
||
},
|
||
{
|
||
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");
|
||
const deprecated1017 = resolveExactSkillPackage(
|
||
"jyotish-birth-time-rectification",
|
||
"10.0.17",
|
||
"f77b06de392feaa7c23e64beb05a90bcc0734db73747eb7915cba338b8eff1e6",
|
||
{ projectRoot },
|
||
);
|
||
assert.equal(deprecated1017.status, "deprecated");
|
||
assert.equal(
|
||
deprecated1017.sha256,
|
||
"f77b06de392feaa7c23e64beb05a90bcc0734db73747eb7915cba338b8eff1e6",
|
||
);
|
||
const deprecated1021 = resolveExactSkillPackage(
|
||
"jyotish-birth-time-rectification",
|
||
"10.0.21",
|
||
"3ced107366b2c4b0f26bae81440162032501f77a8d762d42c84a20fa394ea807",
|
||
{ projectRoot },
|
||
);
|
||
assert.equal(deprecated1021.status, "deprecated");
|
||
assert.equal(
|
||
deprecated1021.sha256,
|
||
"3ced107366b2c4b0f26bae81440162032501f77a8d762d42c84a20fa394ea807",
|
||
);
|
||
const deprecated1022 = resolveExactSkillPackage(
|
||
"jyotish-birth-time-rectification",
|
||
"10.0.22",
|
||
"e3765b20ce6b49a5c7c9ed6b0a8caacc6fc4b8a16a127b93bd92c537330a54e2",
|
||
{ projectRoot },
|
||
);
|
||
assert.equal(deprecated1022.status, "deprecated");
|
||
assert.equal(
|
||
deprecated1022.sha256,
|
||
"e3765b20ce6b49a5c7c9ed6b0a8caacc6fc4b8a16a127b93bd92c537330a54e2",
|
||
);
|
||
const deprecated1023 = resolveExactSkillPackage(
|
||
"jyotish-birth-time-rectification",
|
||
"10.0.23",
|
||
"91f5839e15514b80fa0370bb8b539686af211082e83bc951dc0e8250cbdbadcb",
|
||
{ projectRoot },
|
||
);
|
||
assert.equal(deprecated1023.status, "deprecated");
|
||
assert.equal(
|
||
deprecated1023.sha256,
|
||
"91f5839e15514b80fa0370bb8b539686af211082e83bc951dc0e8250cbdbadcb",
|
||
);
|
||
const deprecated1024 = resolveExactSkillPackage(
|
||
"jyotish-birth-time-rectification",
|
||
"10.0.24",
|
||
"f4267cb93b459a84ec0006b51501b174d5bbc809b571168307affbaede22e4e7",
|
||
{ projectRoot },
|
||
);
|
||
assert.equal(deprecated1024.status, "deprecated");
|
||
assert.equal(
|
||
deprecated1024.sha256,
|
||
"f4267cb93b459a84ec0006b51501b174d5bbc809b571168307affbaede22e4e7",
|
||
);
|
||
const deprecated1025 = resolveExactSkillPackage(
|
||
"jyotish-birth-time-rectification",
|
||
"10.0.25",
|
||
"263ab00d5a62ee38dde1e13474b50a5e194e412b742d1693acaa9974db9f02e8",
|
||
{ projectRoot },
|
||
);
|
||
assert.equal(deprecated1025.status, "deprecated");
|
||
assert.equal(
|
||
deprecated1025.sha256,
|
||
"263ab00d5a62ee38dde1e13474b50a5e194e412b742d1693acaa9974db9f02e8",
|
||
);
|
||
const deprecated1026 = resolveExactSkillPackage(
|
||
"jyotish-birth-time-rectification",
|
||
"10.0.26",
|
||
"bb9cf1623db43980568738f5c011d5aae5b1aa4a94c267fad0a85ca276a5ff39",
|
||
{ projectRoot },
|
||
);
|
||
assert.equal(deprecated1026.status, "deprecated");
|
||
assert.equal(
|
||
deprecated1026.sha256,
|
||
"bb9cf1623db43980568738f5c011d5aae5b1aa4a94c267fad0a85ca276a5ff39",
|
||
);
|
||
});
|
||
|
||
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/,
|
||
);
|
||
});
|