fix: address Copilot review round 7 on #2248

Four findings.

**The CLI dropped what it promised to print.** `getSkill` transforms the
`skills/get` envelope to a `SkillEntry`, so every other member the loose
schema accepted was gone before the CLI saw it — including the `ttlMs` /
`cacheScope` SEP-2640 explicitly leaves open. The code contradicted a comment
two lines above it saying a CLI whose contract is "print the result" must not
reshape one. `getSkillResult` returns the envelope whole; `getSkill` is a
one-line unwrap over it, which matches the callers: the UIs want the entry,
the CLI wants the result.

**The frontmatter verdict and the digest came from different reads.** The
preview fetch and the Verify fetch are separate `resources/read` calls, so a
resource changing between them could pair a verified digest with a
frontmatter verdict for other bytes. `VerificationState.entryText` now holds
the SKILL.md as the verification read it, written in the SAME state update as
the verdict — a separate write was clobbered, because `write` returns a fresh
object and drops anything not named in it. The preview stays the fallback so
a reader who has not clicked Verify still gets the check.

**The Conformance badge excluded the frontmatter findings** it renders, so it
said `0 error(s)` above a red mismatch. Digest and size mismatches stay in
their own badge: "the listing is wrong" and "the bytes are wrong" are
different answers.

**A repeated skill URI collided two React keys** in the TUI list, in a pane
whose job is to show both entries. Index-keyed like the manifest rows.

The web fixture also gains its collision pair in the served-file lookup —
without it they were handed another skill's SKILL.md, which the badge fix
correctly began reporting as a frontmatter error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ND46Ci64DC3tzBy9aG81Nc
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-07 20:44:51 -04:00
commit d6b1e36e7e

View file

@ -41,6 +41,7 @@ function mockClient(overrides: Record<string, unknown> = {}): InspectorClient {
getSkillsExtension: vi.fn().mockReturnValue({ directoryRead: true }),
listSkills: vi.fn().mockResolvedValue({ skills: [] }),
getSkill: vi.fn(),
getSkillResult: vi.fn(),
readResourceDirectory: vi.fn(),
readResource: vi.fn().mockResolvedValue({
result: { contents: [{ uri: "skill://demo/SKILL.md", text: SKILL_MD }] },
@ -85,19 +86,21 @@ describe("runMethod skills dispatch (#2248)", () => {
// -32602 a declared server returns for a URI it does not serve.
const client = mockClient({
getSkillsExtension: vi.fn().mockReturnValue(undefined),
getSkill: vi.fn(),
getSkillResult: vi.fn(),
});
await expect(
runMethod(client, { method: "skills/get", uri: "skill://x/SKILL.md" }),
).rejects.toMatchObject({ exitCode: EXIT_CODES.USAGE });
expect(client.getSkill).not.toHaveBeenCalled();
expect(client.getSkillResult).not.toHaveBeenCalled();
});
it("keeps the { skill } envelope on skills/get", async () => {
// The client unwraps it for callers that want the entry; a CLI whose
// contract is "print the result" must not quietly reshape the wire form.
const entry = await cleanEntry();
const client = mockClient({ getSkill: vi.fn().mockResolvedValue(entry) });
const client = mockClient({
getSkillResult: vi.fn().mockResolvedValue({ skill: entry }),
});
const outcome = await runMethod(client, {
method: "skills/get",
uri: entry.uri,
@ -105,6 +108,27 @@ describe("runMethod skills dispatch (#2248)", () => {
expect(outcome).toMatchObject({ result: { skill: entry } });
});
it("prints the whole skills/get envelope, not just the entry", async () => {
// SEP-2640 leaves it open whether this result carries `ttlMs`/`cacheScope`,
// so a server may send them — and unwrapping to the entry discarded exactly
// those, from a path whose contract is "print the result" (Copilot).
const entry = await cleanEntry();
const envelope = {
skill: entry,
resultType: "complete",
ttlMs: 60,
cacheScope: "public",
};
const client = mockClient({
getSkillResult: vi.fn().mockResolvedValue(envelope),
});
const outcome = await runMethod(client, {
method: "skills/get",
uri: entry.uri,
});
expect(outcome).toMatchObject({ result: envelope });
});
it("requires --uri for skills/get", async () => {
await expect(
runMethod(mockClient(), { method: "skills/get" }),
@ -179,7 +203,9 @@ describe("runMethod skills dispatch (#2248)", () => {
it("--verify works on a single skills/get", async () => {
const entry = await cleanEntry();
const client = mockClient({ getSkill: vi.fn().mockResolvedValue(entry) });
const client = mockClient({
getSkillResult: vi.fn().mockResolvedValue({ skill: entry }),
});
const outcome = await runMethod(client, {
method: "skills/get",
uri: entry.uri,