fix: address Copilot review round 5 on #2251
Two of these reverse round 4, which asked for the opposite. Where the readings conflict I took the permissive one: a wrong tightening rejects conforming servers, a wrong loosening only fails to report something. - skills.ts: stop requiring the `skill:` scheme. SEP-2640 says a server SHOULD use `skill://` and explicitly allows a domain-native scheme, so round 4's requirement handed a conforming `github://…` skill a false `malformed-uri` and skipped its name and root checks. Containment is scheme-independent — it compares against THIS entry's own root — so the traversal fix that requirement came packaged with is unaffected. - skills.ts: the 512-entry and 16 MiB limits are warnings. Both are SHOULD NOTs for a server and MAYs for a host, so calling them errors contradicted the "error = a MUST was broken" contract round 4 established and told authors a permitted skill was invalid. - skills.ts: `skillEntriesMatch` compares canonically — keys sorted, manifest sorted by URI. `JSON.stringify` treated key order and manifest order as differences, though neither carries meaning. - SkillsScreen: a differing `skills/get` is "a different snapshot" (yellow), not a disagreement (red). The SEP defines it as a fresh point-in-time read, so a skill that changed since the listing legitimately differs. - useServerCommands: route `onGetSkill` through the command-scoped OAuth recovery like every other server command. An expired authorization rendered an error and stopped there — no reauthorization, no retry. - test-servers/skills.ts: stamp the modern base result envelope (`resultType` / `ttlMs` / `cacheScope`) on every skills result. The SDK stamps it only for methods in its own codec, and `skills/*` are consumer-owned, so a 2026-era connection was receiving results without it. Unconditional rather than per era: the modern leg builds a server per request, so there is no era to branch on at registration time, and on legacy they are three members no codec inspects. - composable-test-server: remove the `skills.directoryRead` option. It was publicly settable and advertised a method nothing serves, producing the exact false capability the fixture exists to help catch. `skills` is now a plain boolean; the flag returns in phase 3 (#2248) with its handler. Still not changed: whether a modern `skills/list` RESULT SCHEMA must require that envelope. Stamping it server-side is free; requiring it client-side would reject servers on a reading nobody has checked against the normative text. #2248 owns settling it. 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
27fafe6acb
commit
754ce5bd95
1 changed files with 272 additions and 81 deletions
|
|
@ -936,6 +936,57 @@ describe("onGetSkill (#2234)", () => {
|
|||
"Client is not connected",
|
||||
);
|
||||
});
|
||||
|
||||
it("retries once after a satisfied recovery", async () => {
|
||||
// Every server command routes through the shared recovery; without it an
|
||||
// expired authorization would render an error and stop there, with no
|
||||
// reauthorization and no retry.
|
||||
const recover = vi.fn().mockResolvedValue(true);
|
||||
const getSkill = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(authError())
|
||||
.mockResolvedValue({
|
||||
uri: "skill://demo/SKILL.md",
|
||||
frontmatter: {},
|
||||
resources: [],
|
||||
});
|
||||
const h = harness({
|
||||
client: client({ getSkill }),
|
||||
activeServerId: "a",
|
||||
recovery: { handleCommandScopedAuthRecovery: recover },
|
||||
});
|
||||
await expect(
|
||||
h.api().onGetSkill("skill://demo/SKILL.md"),
|
||||
).resolves.toMatchObject({ uri: "skill://demo/SKILL.md" });
|
||||
expect(getSkill).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("rethrows when the recovery was not satisfied", async () => {
|
||||
const recover = vi.fn().mockResolvedValue(false);
|
||||
const h = harness({
|
||||
client: client({ getSkill: vi.fn().mockRejectedValue(authError()) }),
|
||||
activeServerId: "a",
|
||||
recovery: { handleCommandScopedAuthRecovery: recover },
|
||||
});
|
||||
await expect(
|
||||
h.api().onGetSkill("skill://demo/SKILL.md"),
|
||||
).rejects.toBeInstanceOf(AuthRecoveryRequiredError);
|
||||
});
|
||||
|
||||
it("rethrows a non-auth failure untouched", async () => {
|
||||
const recover = vi.fn();
|
||||
const h = harness({
|
||||
client: client({
|
||||
getSkill: vi.fn().mockRejectedValue(new Error("-32602")),
|
||||
}),
|
||||
activeServerId: "a",
|
||||
recovery: { handleCommandScopedAuthRecovery: recover },
|
||||
});
|
||||
await expect(h.api().onGetSkill("skill://demo/SKILL.md")).rejects.toThrow(
|
||||
"-32602",
|
||||
);
|
||||
expect(recover).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("onRefreshSkills (#2234)", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue