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
2 changed files with 257 additions and 31 deletions

View file

@ -605,6 +605,29 @@ describe("SkillsTab (#2248)", () => {
expect(lastFrame() ?? "").toContain("urn:opaque");
});
it("renders both rows when a listing repeats a URI", () => {
// A malformed listing can carry the same skill twice, and this pane exists
// to show BOTH — a URI-keyed row would collide them and let React drop or
// reuse one (Copilot).
const dup: SkillEntry = {
uri: "skill://twice/SKILL.md",
frontmatter: { name: "twice", description: "Listed twice" },
resources: [],
};
const { lastFrame } = render(
<SkillsTab
skills={[dup, dup]}
pageCount={1}
inspectorClient={null}
width={140}
height={30}
/>,
);
const frame = lastFrame() ?? "";
expect(frame).toContain("Skills (2)");
expect(frame.match(/twice/g)?.length).toBeGreaterThanOrEqual(2);
});
it("keys a row by its index when the entry carries no URI", () => {
// A URI-less entry is a `malformed-uri` finding this pane reports, so it
// must still render a addressable row rather than colliding React keys.

View file

@ -333,7 +333,16 @@ export function SkillsTab({
? "warning"
: null;
return (
<Box key={skill.uri || index} paddingY={0} flexShrink={0}>
// Index-keyed like the manifest and finding rows, and for
// the same reason: a malformed listing can repeat a URI, and
// this pane exists to show BOTH entries — a URI key would
// collide them and let React drop or reuse the wrong row
// (Copilot).
<Box
key={`${index}:${skill.uri}`}
paddingY={0}
flexShrink={0}
>
<Text>
{isSelected ? "▶ " : " "}
{worst ? (