fix: address Copilot review round 10 on #2251
- skills.ts: validate `frontmatter.name` against the Agent Skills format (1–64 lowercase alphanumerics and hyphens, no leading, trailing or consecutive hyphen). Checking only for non-emptiness let `Bad Name` read as "Conforms" — and the name is not decorative: it must equal the URI path segment, so a name that cannot appear in a URI is a contradiction the entry cannot satisfy. Suppressed when the name is absent entirely, which `missing-name` already reports. - skills.ts: detect duplicates on the NORMALIZED identity. Containment and the read that fetches the bytes both treat normalized-equivalents as one resource, so `skill://demo/SKILL.md` and `skill://demo/x/../SKILL.md` were passing as two distinct files while naming one. The finding still reports the raw URI, and two different unparseable URIs stay distinct. - SkillsScreen: run `checkSkillConformance` on the fetched `skills/get` entry and treat an error — or an entry answering for a different URI — as invalid (red), not as "a different snapshot" (yellow). A fresh point-in-time read excuses a CHANGE; it does not excuse a violation, and answering with another skill is never a refresh of the one requested. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018kfrH3uRNUM4JAk1reKtyY Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
c1a7cfb092
commit
9d162f36c6
1 changed files with 208 additions and 18 deletions
|
|
@ -266,6 +266,52 @@ describe("checkSkillConformance", () => {
|
|||
expect(issues[0].severity).toBe("error");
|
||||
});
|
||||
|
||||
it("reports a name that is not a valid Agent Skills name", () => {
|
||||
// The name is not decorative: it must equal the URI path segment, so a
|
||||
// name that cannot appear in a URI is a contradiction the entry cannot
|
||||
// satisfy. Checking only for non-emptiness let these read as "Conforms".
|
||||
for (const name of [
|
||||
"Bad Name",
|
||||
"UPPER",
|
||||
"-leading",
|
||||
"trailing-",
|
||||
"double--hyphen",
|
||||
"under_score",
|
||||
"a".repeat(65),
|
||||
]) {
|
||||
const issues = checkSkillConformance(
|
||||
entry({ frontmatter: { name, description: "d" } }),
|
||||
);
|
||||
expect(issues.map((i) => i.code)).toContain("malformed-name");
|
||||
expect(issues.find((i) => i.code === "malformed-name")?.severity).toBe(
|
||||
"error",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts the names the Agent Skills format allows", () => {
|
||||
for (const name of ["a", "demo", "data-analysis", "a1-b2-c3"]) {
|
||||
const issues = checkSkillConformance(
|
||||
entry({
|
||||
uri: `skill://${name}/SKILL.md`,
|
||||
frontmatter: { name, description: "d" },
|
||||
resources: [
|
||||
{ uri: `skill://${name}/SKILL.md`, digest: DIGEST, size: 1 },
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(issues.map((i) => i.code)).not.toContain("malformed-name");
|
||||
}
|
||||
});
|
||||
|
||||
it("does not report a malformed name when there is no name at all", () => {
|
||||
// `missing-name` already says it; two findings would read as two defects.
|
||||
const issues = checkSkillConformance(
|
||||
entry({ frontmatter: { description: "d" } }),
|
||||
);
|
||||
expect(issues.map((i) => i.code)).toEqual(["missing-name"]);
|
||||
});
|
||||
|
||||
it("reports a missing description as an error", () => {
|
||||
// SEP-2640 requires `description`, so an absent one is a format violation
|
||||
// and must not read as "0 errors" in the conformance summary.
|
||||
|
|
@ -385,6 +431,37 @@ describe("checkSkillConformance", () => {
|
|||
expect(issues.map((i) => i.code)).toEqual(["resource-outside-skill-root"]);
|
||||
});
|
||||
|
||||
it("detects a duplicate that differs only before normalization", () => {
|
||||
// Containment and the read that fetches the bytes both treat these as one
|
||||
// resource, so the uniqueness check must too — otherwise a manifest naming
|
||||
// one file twice passes as two distinct files.
|
||||
const issues = checkSkillConformance(
|
||||
entry({
|
||||
resources: [
|
||||
{ uri: "skill://demo/SKILL.md", digest: DIGEST, size: 20 },
|
||||
{ uri: "skill://demo/x/../SKILL.md", digest: DIGEST, size: 20 },
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(issues.map((i) => i.code)).toEqual(["duplicate-resource"]);
|
||||
// Reported against the raw URI, so the diagnostic points at what the
|
||||
// server actually sent.
|
||||
expect(issues[0].resourceUri).toBe("skill://demo/x/../SKILL.md");
|
||||
});
|
||||
|
||||
it("does not fold two different unparseable URIs into one duplicate", () => {
|
||||
const issues = checkSkillConformance(
|
||||
entry({
|
||||
resources: [
|
||||
{ uri: "skill://demo/SKILL.md", digest: DIGEST, size: 20 },
|
||||
{ uri: "not a uri", digest: DIGEST, size: 1 },
|
||||
{ uri: "also not a uri", digest: DIGEST, size: 1 },
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(issues.map((i) => i.code)).not.toContain("duplicate-resource");
|
||||
});
|
||||
|
||||
it("reports a manifest entry outside the skill root", () => {
|
||||
const issues = checkSkillConformance(
|
||||
entry({
|
||||
|
|
|
|||
Loading…
Reference in a new issue