Files
Jyotisha/frontend/tests/skill-registry.test.ts
T
Jesse_Chen a03756bf18
Independent Staging Quality Gate / validate (push) Failing after 11m51s
Independent Staging Quality Gate / publish (push) Has been skipped
feat(web): hang Agent-authored A/B/C/D cards under rectification replies
Conflict nodes stay server-owned; the Agent writes the question and option copy so users can tap instead of typing through an interrogation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 17:36:32 +08:00

547 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.10",
sha256: "c1c3578afe28b754e5c2e2aa48f9c18097b9d7fea3713f0eabc0bdaf17436846",
},
{
name: "jyotish-personal-report",
version: "1.0.0",
sha256: "23149b9e1146b5a66b0762520b58eb750b67d95007144b959f0ff3d9ee2bf982",
},
],
);
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",
);
});
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/,
);
});