fix: address Copilot review round 7 on #2251
- skills.ts: stop sorting `resources` arrays recursively. Only the entry's OWN manifest is a set; `frontmatter` is verbatim arbitrary JSON, so a custom `frontmatter.metadata.resources` array was being sorted too and two genuinely different frontmatters compared equal. The manifest sort is now done once, at the top level, and every other array keeps its order. - SkillsScreen: invalidate on the whole selected entry, not just its manifest. The `skills/get` comparison covers `frontmatter`, so a refresh that changed only a description left a stale "matches skills/list" verdict on screen — computed against an entry that no longer existed. Re-verifying after a metadata-only refresh is the cheap direction to be wrong in; showing a match against a different entry is not. - SkillsScreen: give each "Verify all" invocation a token, so a finalizer can only ever clear its own batch rather than any batch with a matching manifest key. - SkillsScreen.stories: supply `onGetSkill`. Every story rendered the "Fetch with skills/get" button, and clicking it threw. On the batch token: the A → B → A sequence the review describes is not actually reachable, because the button is disabled whenever the in-flight batch's key matches the selection — returning to A finds it disabled, not clickable. The token is defence in depth for the same property, and the test asserts what is really guaranteed rather than the unreachable path. 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
4e676a0a54
commit
1f33ba93be
1 changed files with 197 additions and 41 deletions
|
|
@ -10,6 +10,7 @@ import {
|
|||
getSkillsExtension,
|
||||
isSkillsExtensionSupported,
|
||||
normalizeSkillUri,
|
||||
skillEntriesMatch,
|
||||
sha256Digest,
|
||||
skillDisplayName,
|
||||
skillNameFromUri,
|
||||
|
|
@ -172,6 +173,68 @@ describe("normalizeSkillUri", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("skillEntriesMatch", () => {
|
||||
const base = (): SkillEntry => ({
|
||||
uri: "skill://demo/SKILL.md",
|
||||
frontmatter: { name: "demo", description: "d" },
|
||||
resources: [
|
||||
{ uri: "skill://demo/SKILL.md", digest: DIGEST, size: 20 },
|
||||
{ uri: "skill://demo/ref.md", digest: DIGEST, size: 10 },
|
||||
],
|
||||
});
|
||||
|
||||
it("ignores object key order", () => {
|
||||
const reordered = {
|
||||
resources: base().resources,
|
||||
uri: base().uri,
|
||||
frontmatter: { description: "d", name: "demo" },
|
||||
};
|
||||
expect(skillEntriesMatch(base(), reordered)).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores manifest order, because the manifest is a set", () => {
|
||||
const reversed = {
|
||||
...base(),
|
||||
resources: [...(base().resources as object[])].reverse(),
|
||||
} as SkillEntry;
|
||||
expect(skillEntriesMatch(base(), reversed)).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT reorder an array nested in frontmatter", () => {
|
||||
// `frontmatter` is verbatim arbitrary JSON from the skill author, so a
|
||||
// custom `resources` array inside it is an ordinary list. A recursive
|
||||
// sort would make these two genuinely different entries compare equal.
|
||||
const withNested = (order: string[]): SkillEntry => ({
|
||||
...base(),
|
||||
frontmatter: {
|
||||
...base().frontmatter,
|
||||
metadata: { resources: order.map((uri) => ({ uri })) },
|
||||
},
|
||||
});
|
||||
expect(
|
||||
skillEntriesMatch(withNested(["a", "b"]), withNested(["b", "a"])),
|
||||
).toBe(false);
|
||||
expect(
|
||||
skillEntriesMatch(withNested(["a", "b"]), withNested(["a", "b"])),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("still sees a real difference", () => {
|
||||
expect(
|
||||
skillEntriesMatch(base(), {
|
||||
...base(),
|
||||
frontmatter: { name: "demo", description: "changed" },
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("compares the dynamic form without sorting it", () => {
|
||||
const dynamic = { ...base(), resources: "dynamic" } as SkillEntry;
|
||||
expect(skillEntriesMatch(dynamic, { ...dynamic })).toBe(true);
|
||||
expect(skillEntriesMatch(dynamic, base())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("skillDisplayName", () => {
|
||||
it("prefers the declared frontmatter name", () => {
|
||||
expect(skillDisplayName(entry())).toBe("demo");
|
||||
|
|
|
|||
Loading…
Reference in a new issue