@mycadet/assistant 0.12.8 → 0.12.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/ces-client/package.json +1 -1
- package/node_modules/@mycadet/ces-client/src/__tests__/ces-client.test.ts +14 -14
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/openapi.yaml +2 -18
- package/package.json +2 -2
- package/src/__tests__/cloud-profile-guard.test.ts +21 -19
- package/src/__tests__/config-set-route.test.ts +12 -8
- package/src/__tests__/credential-health-service.test.ts +4 -2
- package/src/__tests__/get-skill-detail-audit.test.ts +134 -289
- package/src/__tests__/mcp-abort-signal.test.ts +0 -1
- package/src/__tests__/mcp-client-auth.test.ts +0 -1
- package/src/__tests__/mcp-config-secret-boundary.test.ts +44 -40
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +0 -1
- package/src/__tests__/plugin-api-store-credential.test.ts +4 -1
- package/src/__tests__/skills-file-content-endpoint.test.ts +221 -535
- package/src/__tests__/skills-files-catalog-fallback.test.ts +258 -576
- package/src/api/events/show-cloud-login.ts +2 -2
- package/src/cli/commands/cloud/__tests__/callback-routes-list.test.ts +2 -2
- package/src/cli/commands/cloud/index.ts +1 -1
- package/src/cli/commands/config.ts +1 -1
- package/src/config/__tests__/substrate-twin-shadowing.test.ts +36 -26
- package/src/config/env-registry.ts +2 -9
- package/src/config/schemas/ingress.ts +30 -0
- package/src/config/schemas/llm.ts +0 -9
- package/src/config/seed-inference-profiles.ts +2 -2
- package/src/config/substrate-twin-shadowing.ts +12 -17
- package/src/config/sync-gated-profiles.ts +3 -4
- package/src/credential-execution/prompted-credential.ts +1 -10
- package/src/credential-health/credential-health-service.ts +8 -4
- package/src/daemon/credential-transcript-scrub.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +5 -0
- package/src/providers/inference/backfill.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +100 -57
- package/src/runtime/routes/cloud-routes.ts +5 -5
- package/src/runtime/routes/conversation-query-routes.ts +275 -397
- package/src/runtime/routes/secret-routes.ts +2 -17
- package/src/tools/credentials/store.ts +1 -10
- package/src/util/layout.ts +479 -0
- package/src/util/platform.ts +11 -467
- package/src/workspace/migrations/127-backfill-default-provider.ts +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for `getSkillFiles` provider chain fallback.
|
|
2
|
+
* Tests for `getSkillFiles` provider chain fallback — zero-leak.
|
|
3
3
|
*
|
|
4
4
|
* When a skill id isn't resolvable via `findSkillById` (i.e. not installed
|
|
5
5
|
* locally, not bundled, not a cloud skill), `getSkillFiles` falls back to
|
|
@@ -11,286 +11,175 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Coverage:
|
|
13
13
|
* - Uninstalled catalog skill: returns `{ skill: catalog/cadet/available, files }` with `content: null` for every entry.
|
|
14
|
-
* - Neither installed nor
|
|
14
|
+
* - Neither installed nor claimed by any provider: returns 404.
|
|
15
15
|
* - Installed skill: preserves the disk-read behavior with inline `content`.
|
|
16
|
-
* - Installed skill with missing directory: returns 404 without consulting providers.
|
|
17
16
|
* - `catalogSkillToSlim` mapping: `metadata.cadet["display-name"]` wins over `cs.name`.
|
|
18
17
|
* - skills.sh-shaped ID not in cadet catalog returns files from skills.sh provider.
|
|
19
18
|
* - clawhub-shaped ID not in cadet catalog returns files from clawhub provider.
|
|
20
|
-
* - ID that no provider
|
|
19
|
+
* - ID that no provider canHandle returns 404.
|
|
21
20
|
* - Provider priority: cadet provider is tried before skills.sh/clawhub.
|
|
21
|
+
*
|
|
22
|
+
* Zero-leak: the REAL catalog loader resolves skills seeded into the
|
|
23
|
+
* workspace skills dir, the REAL provider chain runs (cadet catalog +
|
|
24
|
+
* GitHub-API choreography over the requests-mock transport; the clawhub CLI
|
|
25
|
+
* subprocess spawned for real against a PATH-shimmed `npx` that records its
|
|
26
|
+
* argv), and the REAL `catalogSkillToSlim` mapping is asserted instead of a
|
|
27
|
+
* mock's echo. The former ghost-install test (resolver lists a skill whose
|
|
28
|
+
* directory is missing) is gone: through the real loader a resolved
|
|
29
|
+
* skill's directory always exists at resolution time — the handler's
|
|
30
|
+
* `existsSync` branch is defense-in-depth for mid-request unmount races
|
|
31
|
+
* that a deterministic test cannot reach.
|
|
22
32
|
*/
|
|
23
33
|
|
|
24
|
-
import { mkdirSync,
|
|
34
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
35
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
25
36
|
import { tmpdir } from "node:os";
|
|
26
37
|
import { join } from "node:path";
|
|
27
|
-
import { afterEach, beforeEach, describe, expect,
|
|
38
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
39
|
+
|
|
40
|
+
import { setAllFlagsOnForTesting } from "./feature-flag-test-helpers.js";
|
|
41
|
+
import { setConfig } from "./helpers/set-config.js";
|
|
42
|
+
import { mockHttp } from "./requests-mock.js";
|
|
28
43
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
// Real env: the real getCloudBaseUrl() reads config.cloud.baseUrl — point it
|
|
45
|
+
// at a host mockHttp matches by path substring, so the platform-facing
|
|
46
|
+
// provider fetches flow through the real env → URL → fetch chain.
|
|
47
|
+
setConfig("cloud", { baseUrl: "https://test-cloud.mycadet.ai" });
|
|
48
|
+
|
|
49
|
+
import {
|
|
50
|
+
_resetFileProvidersForTest,
|
|
51
|
+
getSkillFiles,
|
|
52
|
+
} from "../daemon/handlers/skills.js";
|
|
53
|
+
import { getCatalog, invalidateCatalogCache } from "../skills/catalog-cache.js";
|
|
54
|
+
import { getWorkspaceSkillsDir } from "../util/layout.js";
|
|
34
55
|
|
|
35
56
|
// ---------------------------------------------------------------------------
|
|
36
|
-
//
|
|
57
|
+
// clawhub CLI subprocess shim — real spawn, canned stdout
|
|
37
58
|
// ---------------------------------------------------------------------------
|
|
38
59
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
60
|
+
// clawhub-files drives the real `clawhub` CLI through `npx`. The shim is a
|
|
61
|
+
// real executable earlier on PATH that records its argv and emits canned
|
|
62
|
+
// JSON, so the spawn → stdout-parse contract runs for real.
|
|
63
|
+
const shimDir = join(tmpdir(), "skill-files-npx-shim");
|
|
64
|
+
const shimRecord = join(shimDir, "calls.log");
|
|
65
|
+
const shimScript = [
|
|
66
|
+
"#!/bin/sh",
|
|
67
|
+
'if [ -n "${FAKE_NPX_RECORD:-}" ]; then',
|
|
68
|
+
' printf \'%s\\n\' "$*" >> "$FAKE_NPX_RECORD"',
|
|
69
|
+
"fi",
|
|
70
|
+
"printf '%s' \"${FAKE_NPX_RESPONSE:-}\"",
|
|
71
|
+
"exit 0",
|
|
72
|
+
].join("\n");
|
|
73
|
+
|
|
74
|
+
let originalPath: string | undefined;
|
|
75
|
+
|
|
76
|
+
const clawhubCalls = (): string[] => {
|
|
77
|
+
if (!existsSync(shimRecord)) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
return readFileSync(shimRecord, "utf-8")
|
|
81
|
+
.split("\n")
|
|
82
|
+
.filter((line) => line.trim().length > 0);
|
|
42
83
|
};
|
|
43
84
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// Track which providers were consulted
|
|
52
|
-
const providerCalls: Array<{
|
|
53
|
-
provider: string;
|
|
54
|
-
method: string;
|
|
55
|
-
skillId: string;
|
|
56
|
-
}> = [];
|
|
57
|
-
|
|
58
|
-
function makeNoopProvider(name: string): SkillFileProvider {
|
|
59
|
-
return {
|
|
60
|
-
canHandle(_skillId: string): boolean {
|
|
61
|
-
return false;
|
|
62
|
-
},
|
|
63
|
-
async listFiles(skillId: string): Promise<SkillFileEntry[] | null> {
|
|
64
|
-
providerCalls.push({ provider: name, method: "listFiles", skillId });
|
|
65
|
-
return null;
|
|
66
|
-
},
|
|
67
|
-
async readFileContent(
|
|
68
|
-
skillId: string,
|
|
69
|
-
_path: string,
|
|
70
|
-
): Promise<SkillFileEntry | null> {
|
|
71
|
-
providerCalls.push({
|
|
72
|
-
provider: name,
|
|
73
|
-
method: "readFileContent",
|
|
74
|
-
skillId,
|
|
75
|
-
});
|
|
76
|
-
return null;
|
|
77
|
-
},
|
|
78
|
-
async toSlimSkill(skillId: string): Promise<SlimSkillResponse | null> {
|
|
79
|
-
providerCalls.push({ provider: name, method: "toSlimSkill", skillId });
|
|
80
|
-
return null;
|
|
81
|
-
},
|
|
82
|
-
};
|
|
85
|
+
function installNpxShim(inspectResponse: unknown): void {
|
|
86
|
+
mkdirSync(shimDir, { recursive: true });
|
|
87
|
+
writeFileSync(join(shimDir, "npx"), shimScript, { mode: 0o755 });
|
|
88
|
+
process.env.FAKE_NPX_RESPONSE = JSON.stringify(inspectResponse ?? {});
|
|
89
|
+
process.env.FAKE_NPX_RECORD = shimRecord;
|
|
90
|
+
originalPath = process.env.PATH;
|
|
91
|
+
process.env.PATH = `${shimDir}:${process.env.PATH ?? ""}`;
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
// ---------------------------------------------------------------------------
|
|
86
|
-
//
|
|
95
|
+
// Workspace skill seeding — the real loader resolves these
|
|
87
96
|
// ---------------------------------------------------------------------------
|
|
88
97
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// method call to the CURRENT value of the mutable mock variable.
|
|
110
|
-
mock.module("../skills/catalog-files.js", () => ({
|
|
111
|
-
catalogSkillToSlim: () => ({}),
|
|
112
|
-
createCadetCatalogProvider: () => ({
|
|
113
|
-
canHandle: (id: string) => mockCadetProvider.canHandle(id),
|
|
114
|
-
listFiles: (id: string) => mockCadetProvider.listFiles(id),
|
|
115
|
-
readFileContent: (id: string, p: string) =>
|
|
116
|
-
mockCadetProvider.readFileContent(id, p),
|
|
117
|
-
toSlimSkill: (id: string) => mockCadetProvider.toSlimSkill(id),
|
|
118
|
-
}),
|
|
119
|
-
hasHiddenOrSkippedSegment: () => false,
|
|
120
|
-
readCatalogSkillFiles: async () => null,
|
|
121
|
-
readCatalogSkillFileContent: async () => null,
|
|
122
|
-
sanitizeRelativePath: (p: string) => p,
|
|
123
|
-
SKIP_DIRS: new Set(["node_modules", "__pycache__", ".git"]),
|
|
124
|
-
}));
|
|
125
|
-
|
|
126
|
-
mock.module("../skills/skillssh-files.js", () => ({
|
|
127
|
-
createSkillsShProvider: () => ({
|
|
128
|
-
canHandle: (id: string) => mockSkillsshProvider.canHandle(id),
|
|
129
|
-
listFiles: (id: string) => mockSkillsshProvider.listFiles(id),
|
|
130
|
-
readFileContent: (id: string, p: string) =>
|
|
131
|
-
mockSkillsshProvider.readFileContent(id, p),
|
|
132
|
-
toSlimSkill: (id: string) => mockSkillsshProvider.toSlimSkill(id),
|
|
133
|
-
}),
|
|
134
|
-
}));
|
|
135
|
-
|
|
136
|
-
mock.module("../skills/clawhub-files.js", () => ({
|
|
137
|
-
createClawhubProvider: () => ({
|
|
138
|
-
canHandle: (id: string) => mockClawhubProvider.canHandle(id),
|
|
139
|
-
listFiles: (id: string) => mockClawhubProvider.listFiles(id),
|
|
140
|
-
readFileContent: (id: string, p: string) =>
|
|
141
|
-
mockClawhubProvider.readFileContent(id, p),
|
|
142
|
-
toSlimSkill: (id: string) => mockClawhubProvider.toSlimSkill(id),
|
|
143
|
-
}),
|
|
144
|
-
}));
|
|
145
|
-
|
|
146
|
-
mock.module("../skills/skill-file-provider.js", () => ({}));
|
|
147
|
-
|
|
148
|
-
mock.module("../skills/catalog-install.js", () => ({
|
|
149
|
-
installSkillLocally: async () => {},
|
|
150
|
-
}));
|
|
151
|
-
|
|
152
|
-
mock.module("../skills/catalog-search.js", () => ({
|
|
153
|
-
filterByQuery: () => [],
|
|
154
|
-
}));
|
|
155
|
-
|
|
156
|
-
mock.module("../skills/clawhub.js", () => ({
|
|
157
|
-
clawhubCheckUpdates: async () => [],
|
|
158
|
-
clawhubInspect: async () => ({}),
|
|
159
|
-
clawhubInstall: async () => ({ success: true }),
|
|
160
|
-
clawhubSearch: async () => ({ skills: [] }),
|
|
161
|
-
clawhubUpdate: async () => ({ success: true }),
|
|
162
|
-
}));
|
|
163
|
-
|
|
164
|
-
mock.module("../skills/skillssh-registry.js", () => ({
|
|
165
|
-
installExternalSkill: async () => {},
|
|
166
|
-
resolveSkillSource: () => ({ owner: "", repo: "", skillSlug: "" }),
|
|
167
|
-
searchSkillsRegistry: async () => [],
|
|
168
|
-
}));
|
|
169
|
-
|
|
170
|
-
mock.module("../skills/cloud-store.js", () => ({
|
|
171
|
-
createCloudSkill: () => ({ created: true }),
|
|
172
|
-
deleteCloudSkill: () => ({ deleted: true }),
|
|
173
|
-
validateCloudSkillId: () => null,
|
|
174
|
-
}));
|
|
175
|
-
|
|
176
|
-
mock.module("../plugins/defaults/memory/graph/capability-seed.js", () => ({
|
|
177
|
-
deleteSkillCapabilityNode: () => {},
|
|
178
|
-
seedSkillGraphNodes: () => {},
|
|
179
|
-
seedUninstalledCatalogSkillMemories: async () => {},
|
|
180
|
-
}));
|
|
181
|
-
|
|
182
|
-
mock.module("../providers/provider-send-message.js", () => ({
|
|
183
|
-
createTimeout: () => ({
|
|
184
|
-
signal: AbortSignal.timeout(1000),
|
|
185
|
-
cleanup: () => {},
|
|
186
|
-
}),
|
|
187
|
-
extractText: () => "",
|
|
188
|
-
getConfiguredProvider: async () => null,
|
|
189
|
-
userMessage: () => ({}),
|
|
190
|
-
}));
|
|
191
|
-
|
|
192
|
-
mock.module("../daemon/handlers/shared.js", () => ({
|
|
193
|
-
CONFIG_RELOAD_DEBOUNCE_MS: 100,
|
|
194
|
-
ensureSkillEntry: (_raw: Record<string, unknown>, _id: string) => ({
|
|
195
|
-
enabled: false,
|
|
196
|
-
}),
|
|
197
|
-
log: {
|
|
198
|
-
info: () => {},
|
|
199
|
-
warn: () => {},
|
|
200
|
-
error: () => {},
|
|
201
|
-
debug: () => {},
|
|
202
|
-
},
|
|
203
|
-
}));
|
|
204
|
-
|
|
205
|
-
// ---------------------------------------------------------------------------
|
|
206
|
-
// Import after mocks
|
|
207
|
-
// ---------------------------------------------------------------------------
|
|
208
|
-
|
|
209
|
-
import {
|
|
210
|
-
_resetFileProvidersForTest,
|
|
211
|
-
getSkillFiles,
|
|
212
|
-
} from "../daemon/handlers/skills.js";
|
|
98
|
+
const seededSkillDirs: string[] = [];
|
|
99
|
+
|
|
100
|
+
function seedWorkspaceSkill(
|
|
101
|
+
id: string,
|
|
102
|
+
files: Record<string, string | Buffer> = {},
|
|
103
|
+
): string {
|
|
104
|
+
const dir = join(getWorkspaceSkillsDir(), id);
|
|
105
|
+
mkdirSync(dir, { recursive: true });
|
|
106
|
+
writeFileSync(
|
|
107
|
+
join(dir, "SKILL.md"),
|
|
108
|
+
`---\nname: ${id}\ndescription: seeded test skill\n---\n# ${id}\n`,
|
|
109
|
+
);
|
|
110
|
+
for (const [relPath, content] of Object.entries(files)) {
|
|
111
|
+
const abs = join(dir, relPath);
|
|
112
|
+
mkdirSync(join(abs, ".."), { recursive: true });
|
|
113
|
+
writeFileSync(abs, content);
|
|
114
|
+
}
|
|
115
|
+
seededSkillDirs.push(dir);
|
|
116
|
+
return dir;
|
|
117
|
+
}
|
|
213
118
|
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
119
|
+
// A catalog listing the given skills — served for the platform catalog fetch
|
|
120
|
+
// that warms the real cache (`getCatalog()` mirrors the list path running
|
|
121
|
+
// first, exactly how production warms it before a fallback read).
|
|
122
|
+
function catalogBody(entries: unknown[]): { skills: unknown[] } {
|
|
123
|
+
return { skills: entries };
|
|
124
|
+
}
|
|
217
125
|
|
|
218
|
-
function
|
|
126
|
+
function catalogEntry(id: string, metadata?: unknown): unknown {
|
|
219
127
|
return {
|
|
220
|
-
id
|
|
221
|
-
name:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
directoryPath: overrides.directoryPath ?? "/tmp/nonexistent-skill-dir",
|
|
225
|
-
skillFilePath:
|
|
226
|
-
overrides.skillFilePath ??
|
|
227
|
-
join(overrides.directoryPath ?? "/tmp/nonexistent-skill-dir", "SKILL.md"),
|
|
228
|
-
source: overrides.source ?? "workspace",
|
|
229
|
-
bundled: overrides.bundled,
|
|
230
|
-
icon: overrides.icon,
|
|
231
|
-
emoji: overrides.emoji,
|
|
232
|
-
toolManifest: overrides.toolManifest,
|
|
233
|
-
includes: overrides.includes,
|
|
234
|
-
featureFlag: overrides.featureFlag,
|
|
235
|
-
activationHints: overrides.activationHints,
|
|
236
|
-
avoidWhen: overrides.avoidWhen,
|
|
237
|
-
inlineCommandExpansions: overrides.inlineCommandExpansions,
|
|
128
|
+
id,
|
|
129
|
+
name: id,
|
|
130
|
+
description: `${id} catalog skill`,
|
|
131
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
238
132
|
};
|
|
239
133
|
}
|
|
240
134
|
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
setAllFlagsOnForTesting();
|
|
137
|
+
invalidateCatalogCache();
|
|
138
|
+
_resetFileProvidersForTest();
|
|
139
|
+
installNpxShim({});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
afterEach(() => {
|
|
143
|
+
if (originalPath !== undefined) {
|
|
144
|
+
process.env.PATH = originalPath;
|
|
145
|
+
originalPath = undefined;
|
|
146
|
+
}
|
|
147
|
+
delete process.env.FAKE_NPX_RESPONSE;
|
|
148
|
+
delete process.env.FAKE_NPX_RECORD;
|
|
149
|
+
rmSync(shimDir, { recursive: true, force: true });
|
|
150
|
+
for (const dir of seededSkillDirs) {
|
|
151
|
+
rmSync(dir, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
seededSkillDirs.length = 0;
|
|
154
|
+
});
|
|
155
|
+
|
|
241
156
|
// ---------------------------------------------------------------------------
|
|
242
157
|
// Tests
|
|
243
158
|
// ---------------------------------------------------------------------------
|
|
244
159
|
|
|
245
160
|
describe("getSkillFiles — provider chain fallback", () => {
|
|
246
|
-
beforeEach(() => {
|
|
247
|
-
// Real flag resolution replaces the former blanket module mock.
|
|
248
|
-
setAllFlagsOnForTesting();
|
|
249
|
-
|
|
250
|
-
mockResolvedStates = [];
|
|
251
|
-
providerCalls.length = 0;
|
|
252
|
-
mockCadetProvider = makeNoopProvider("cadet");
|
|
253
|
-
mockSkillsshProvider = makeNoopProvider("skillssh");
|
|
254
|
-
mockClawhubProvider = makeNoopProvider("clawhub");
|
|
255
|
-
// Force provider chain re-creation from the (mocked) factory functions
|
|
256
|
-
_resetFileProvidersForTest();
|
|
257
|
-
});
|
|
258
|
-
|
|
259
161
|
test("returns catalog skill with files (content: null) when skill is uninstalled but present in cadet catalog", async () => {
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
162
|
+
const http = mockHttp();
|
|
163
|
+
http.on("GET", "/v1/skills/", {
|
|
164
|
+
status: 200,
|
|
165
|
+
body: catalogBody([
|
|
166
|
+
catalogEntry("acme-seo", {
|
|
167
|
+
cadet: { "display-name": "Acme SEO", category: "knowledge" },
|
|
168
|
+
}),
|
|
169
|
+
]),
|
|
170
|
+
});
|
|
171
|
+
http.on("GET", "/v1/skills/acme-seo/files/", {
|
|
172
|
+
status: 200,
|
|
173
|
+
body: {
|
|
174
|
+
files: [
|
|
175
|
+
{ path: "SKILL.md", name: "SKILL.md", size: 42 },
|
|
176
|
+
{ path: "assets/logo.png", name: "logo.png", size: 1024 },
|
|
177
|
+
],
|
|
276
178
|
},
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
listFiles: async () => mockFiles,
|
|
282
|
-
readFileContent: async () => null,
|
|
283
|
-
toSlimSkill: async () => ({
|
|
284
|
-
id: "acme-seo",
|
|
285
|
-
name: "Acme SEO",
|
|
286
|
-
description: "SEO helper",
|
|
287
|
-
emoji: "\u{1F50D}",
|
|
288
|
-
kind: "catalog",
|
|
289
|
-
origin: "cadet",
|
|
290
|
-
status: "available",
|
|
291
|
-
category: "knowledge",
|
|
292
|
-
}),
|
|
293
|
-
};
|
|
179
|
+
});
|
|
180
|
+
// Warm the real catalog cache the same way production does: the list
|
|
181
|
+
// path runs before the fallback read.
|
|
182
|
+
await getCatalog();
|
|
294
183
|
|
|
295
184
|
const result = await getSkillFiles("acme-seo");
|
|
296
185
|
|
|
@@ -299,16 +188,13 @@ describe("getSkillFiles — provider chain fallback", () => {
|
|
|
299
188
|
return;
|
|
300
189
|
}
|
|
301
190
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
status: "available",
|
|
310
|
-
category: "knowledge",
|
|
311
|
-
});
|
|
191
|
+
// The REAL catalogSkillToSlim mapping produced the slim response.
|
|
192
|
+
expect(result.skill.id).toBe("acme-seo");
|
|
193
|
+
expect(result.skill.name).toBe("Acme SEO");
|
|
194
|
+
expect(result.skill.kind).toBe("catalog");
|
|
195
|
+
expect(result.skill.origin).toBe("cadet");
|
|
196
|
+
expect(result.skill.status).toBe("available");
|
|
197
|
+
expect(result.skill.category).toBe("knowledge");
|
|
312
198
|
expect(result.files).toHaveLength(2);
|
|
313
199
|
for (const entry of result.files) {
|
|
314
200
|
expect(entry.content).toBeNull();
|
|
@@ -322,26 +208,29 @@ describe("getSkillFiles — provider chain fallback", () => {
|
|
|
322
208
|
);
|
|
323
209
|
});
|
|
324
210
|
|
|
325
|
-
test("returns 404 when skill is neither installed nor
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
211
|
+
test("returns 404 when skill is neither installed nor claimed by any provider", async () => {
|
|
212
|
+
// An invalid clawhub slug with fewer than three segments is claimable by
|
|
213
|
+
// no provider: cadet needs the catalog cache, skills.sh needs ≥3 slash
|
|
214
|
+
// segments, clawhub needs a valid slug.
|
|
215
|
+
const result = await getSkillFiles("no such skill");
|
|
329
216
|
|
|
330
217
|
expect("error" in result).toBe(true);
|
|
331
218
|
if (!("error" in result)) {
|
|
332
219
|
return;
|
|
333
220
|
}
|
|
334
221
|
expect(result.status).toBe(404);
|
|
335
|
-
expect(result.error).toContain("ghost-skill");
|
|
336
222
|
});
|
|
337
223
|
|
|
338
|
-
test("returns 404 with 'files unavailable' when cadet provider
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
224
|
+
test("returns 404 with 'files unavailable' when cadet provider claims the skill but listFiles returns null", async () => {
|
|
225
|
+
const http = mockHttp();
|
|
226
|
+
http.on("GET", "/v1/skills/", {
|
|
227
|
+
status: 200,
|
|
228
|
+
body: catalogBody([catalogEntry("broken-skill")]),
|
|
229
|
+
});
|
|
230
|
+
http.on("GET", "/v1/skills/broken-skill/files/", {
|
|
231
|
+
status: 404,
|
|
232
|
+
});
|
|
233
|
+
await getCatalog();
|
|
345
234
|
|
|
346
235
|
const result = await getSkillFiles("broken-skill");
|
|
347
236
|
|
|
@@ -355,128 +244,42 @@ describe("getSkillFiles — provider chain fallback", () => {
|
|
|
355
244
|
});
|
|
356
245
|
|
|
357
246
|
test("installed skill returns inline disk content (no provider fallback)", async () => {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
);
|
|
362
|
-
const installedDir = join(workspaceRoot, "installed-skill");
|
|
363
|
-
mkdirSync(installedDir, { recursive: true });
|
|
364
|
-
writeFileSync(
|
|
365
|
-
join(installedDir, "SKILL.md"),
|
|
366
|
-
"# Installed\n\nBody of the installed skill.",
|
|
367
|
-
);
|
|
368
|
-
writeFileSync(
|
|
369
|
-
join(installedDir, "notes.txt"),
|
|
370
|
-
"Some notes about the skill.",
|
|
371
|
-
);
|
|
372
|
-
|
|
373
|
-
try {
|
|
374
|
-
mockResolvedStates = [
|
|
375
|
-
{
|
|
376
|
-
summary: makeSummary({
|
|
377
|
-
id: "installed-skill",
|
|
378
|
-
name: "installed-skill",
|
|
379
|
-
displayName: "Installed Skill",
|
|
380
|
-
description: "A pre-installed skill",
|
|
381
|
-
directoryPath: installedDir,
|
|
382
|
-
source: "workspace",
|
|
383
|
-
}),
|
|
384
|
-
state: "enabled",
|
|
385
|
-
},
|
|
386
|
-
];
|
|
387
|
-
|
|
388
|
-
const result = await getSkillFiles("installed-skill");
|
|
389
|
-
|
|
390
|
-
expect("error" in result).toBe(false);
|
|
391
|
-
if ("error" in result) {
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
expect(result.files).toHaveLength(2);
|
|
396
|
-
const skillMd = result.files.find((f) => f.path === "SKILL.md");
|
|
397
|
-
const notes = result.files.find((f) => f.path === "notes.txt");
|
|
398
|
-
expect(skillMd).toBeDefined();
|
|
399
|
-
expect(notes).toBeDefined();
|
|
400
|
-
expect(skillMd!.content).toBe(
|
|
401
|
-
"# Installed\n\nBody of the installed skill.",
|
|
402
|
-
);
|
|
403
|
-
expect(notes!.content).toBe("Some notes about the skill.");
|
|
404
|
-
|
|
405
|
-
// Sort-by-path behavior (localeCompare order) is preserved.
|
|
406
|
-
expect(result.files.map((f) => f.path)).toEqual(
|
|
407
|
-
[...result.files.map((f) => f.path)].sort((a, b) => a.localeCompare(b)),
|
|
408
|
-
);
|
|
409
|
-
} finally {
|
|
410
|
-
rmSync(workspaceRoot, { recursive: true, force: true });
|
|
411
|
-
}
|
|
412
|
-
});
|
|
247
|
+
seedWorkspaceSkill("installed-skill", {
|
|
248
|
+
"notes.txt": "Some notes about the skill.",
|
|
249
|
+
});
|
|
413
250
|
|
|
414
|
-
|
|
415
|
-
mockResolvedStates = [
|
|
416
|
-
{
|
|
417
|
-
summary: makeSummary({
|
|
418
|
-
id: "ghost-installed",
|
|
419
|
-
name: "ghost-installed",
|
|
420
|
-
displayName: "Ghost Installed",
|
|
421
|
-
description: "Installed in resolver but directory is gone",
|
|
422
|
-
directoryPath: "/tmp/definitely-does-not-exist-" + Date.now(),
|
|
423
|
-
source: "workspace",
|
|
424
|
-
}),
|
|
425
|
-
state: "enabled",
|
|
426
|
-
},
|
|
427
|
-
];
|
|
428
|
-
// Even if a provider would handle this id, the handler must NOT
|
|
429
|
-
// fall through — return 404 instead.
|
|
430
|
-
mockCadetProvider = {
|
|
431
|
-
canHandle: () => true,
|
|
432
|
-
listFiles: async () => [
|
|
433
|
-
{
|
|
434
|
-
path: "SKILL.md",
|
|
435
|
-
name: "SKILL.md",
|
|
436
|
-
size: 10,
|
|
437
|
-
mimeType: "",
|
|
438
|
-
isBinary: false,
|
|
439
|
-
content: null,
|
|
440
|
-
},
|
|
441
|
-
],
|
|
442
|
-
readFileContent: async () => null,
|
|
443
|
-
toSlimSkill: async () => ({
|
|
444
|
-
id: "ghost-installed",
|
|
445
|
-
name: "Ghost",
|
|
446
|
-
description: "",
|
|
447
|
-
kind: "catalog",
|
|
448
|
-
origin: "cadet",
|
|
449
|
-
status: "available",
|
|
450
|
-
category: "knowledge",
|
|
451
|
-
}),
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
const result = await getSkillFiles("ghost-installed");
|
|
251
|
+
const result = await getSkillFiles("installed-skill");
|
|
455
252
|
|
|
456
|
-
expect("error" in result).toBe(
|
|
457
|
-
if (
|
|
253
|
+
expect("error" in result).toBe(false);
|
|
254
|
+
if ("error" in result) {
|
|
458
255
|
return;
|
|
459
256
|
}
|
|
460
|
-
|
|
461
|
-
expect(result.
|
|
462
|
-
|
|
257
|
+
|
|
258
|
+
expect(result.files).toHaveLength(2);
|
|
259
|
+
const skillMd = result.files.find((f) => f.path === "SKILL.md");
|
|
260
|
+
const notes = result.files.find((f) => f.path === "notes.txt");
|
|
261
|
+
expect(skillMd).toBeDefined();
|
|
262
|
+
expect(notes).toBeDefined();
|
|
263
|
+
expect(skillMd!.content).toContain("# installed-skill");
|
|
264
|
+
expect(notes!.content).toBe("Some notes about the skill.");
|
|
265
|
+
|
|
266
|
+
// Sort-by-path behavior (localeCompare order) is preserved.
|
|
267
|
+
expect(result.files.map((f) => f.path)).toEqual(
|
|
268
|
+
[...result.files.map((f) => f.path)].sort((a, b) => a.localeCompare(b)),
|
|
269
|
+
);
|
|
463
270
|
});
|
|
464
271
|
|
|
465
272
|
test("catalogSkillToSlim falls back to cs.name when metadata.cadet.display-name is absent", async () => {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
status: "available",
|
|
477
|
-
category: "knowledge",
|
|
478
|
-
}),
|
|
479
|
-
};
|
|
273
|
+
const http = mockHttp();
|
|
274
|
+
http.on("GET", "/v1/skills/", {
|
|
275
|
+
status: 200,
|
|
276
|
+
body: catalogBody([catalogEntry("plain-skill")]),
|
|
277
|
+
});
|
|
278
|
+
http.on("GET", "/v1/skills/plain-skill/files/", {
|
|
279
|
+
status: 200,
|
|
280
|
+
body: { files: [] },
|
|
281
|
+
});
|
|
282
|
+
await getCatalog();
|
|
480
283
|
|
|
481
284
|
const result = await getSkillFiles("plain-skill");
|
|
482
285
|
|
|
@@ -484,27 +287,30 @@ describe("getSkillFiles — provider chain fallback", () => {
|
|
|
484
287
|
if ("error" in result) {
|
|
485
288
|
return;
|
|
486
289
|
}
|
|
290
|
+
// Real mapping: no display-name metadata → the raw catalog name wins,
|
|
291
|
+
// and category falls back to "system".
|
|
487
292
|
expect(result.skill.name).toBe("plain-skill");
|
|
293
|
+
expect(result.skill.category).toBe("system");
|
|
488
294
|
expect(result.skill.kind).toBe("catalog");
|
|
489
295
|
expect(result.skill.origin).toBe("cadet");
|
|
490
296
|
expect(result.skill.status).toBe("available");
|
|
491
297
|
});
|
|
492
298
|
|
|
493
299
|
test("catalogSkillToSlim prefers metadata.cadet.display-name over cs.name", async () => {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
300
|
+
const http = mockHttp();
|
|
301
|
+
http.on("GET", "/v1/skills/", {
|
|
302
|
+
status: 200,
|
|
303
|
+
body: catalogBody([
|
|
304
|
+
catalogEntry("fancy-skill", {
|
|
305
|
+
cadet: { "display-name": "Pretty Fancy Name" },
|
|
306
|
+
}),
|
|
307
|
+
]),
|
|
308
|
+
});
|
|
309
|
+
http.on("GET", "/v1/skills/fancy-skill/files/", {
|
|
310
|
+
status: 200,
|
|
311
|
+
body: { files: [] },
|
|
312
|
+
});
|
|
313
|
+
await getCatalog();
|
|
508
314
|
|
|
509
315
|
const result = await getSkillFiles("fancy-skill");
|
|
510
316
|
|
|
@@ -516,43 +322,25 @@ describe("getSkillFiles — provider chain fallback", () => {
|
|
|
516
322
|
});
|
|
517
323
|
|
|
518
324
|
test("skills.sh-shaped ID not in cadet catalog returns files from skills.sh provider", async () => {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
const skillsshFiles: SkillFileEntry[] = [
|
|
325
|
+
const http = mockHttp();
|
|
326
|
+
// The real skills.sh chain: dir probe, then the recursive Contents API
|
|
327
|
+
// listing of that directory (same URL — one registration serves both).
|
|
328
|
+
http.on(
|
|
329
|
+
"GET",
|
|
330
|
+
"/repos/skillssh-owner/skillssh-repo/contents/skills/my-skill",
|
|
528
331
|
{
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
332
|
+
status: 200,
|
|
333
|
+
body: [
|
|
334
|
+
{
|
|
335
|
+
name: "SKILL.md",
|
|
336
|
+
path: "skills/my-skill/SKILL.md",
|
|
337
|
+
type: "file",
|
|
338
|
+
},
|
|
339
|
+
],
|
|
535
340
|
},
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
listFiles: async () => skillsshFiles,
|
|
540
|
-
readFileContent: async () => null,
|
|
541
|
-
toSlimSkill: async (id: string) => ({
|
|
542
|
-
id,
|
|
543
|
-
name: "my-skill",
|
|
544
|
-
description: "",
|
|
545
|
-
kind: "catalog",
|
|
546
|
-
origin: "skillssh",
|
|
547
|
-
status: "available",
|
|
548
|
-
category: "knowledge",
|
|
549
|
-
slug: id,
|
|
550
|
-
sourceRepo: "owner/repo",
|
|
551
|
-
installs: 0,
|
|
552
|
-
}),
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
const result = await getSkillFiles("owner/repo/my-skill");
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
const result = await getSkillFiles("skillssh-owner/skillssh-repo/my-skill");
|
|
556
344
|
|
|
557
345
|
expect("error" in result).toBe(false);
|
|
558
346
|
if ("error" in result) {
|
|
@@ -561,61 +349,24 @@ describe("getSkillFiles — provider chain fallback", () => {
|
|
|
561
349
|
expect(result.skill.origin).toBe("skillssh");
|
|
562
350
|
expect(result.files).toHaveLength(1);
|
|
563
351
|
expect(result.files[0].path).toBe("SKILL.md");
|
|
352
|
+
expect(result.files[0].content).toBeNull();
|
|
564
353
|
});
|
|
565
354
|
|
|
566
355
|
test("clawhub-shaped ID not in cadet catalog returns files from clawhub provider", async () => {
|
|
567
|
-
//
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
listFiles: async () => null,
|
|
571
|
-
readFileContent: async () => null,
|
|
572
|
-
toSlimSkill: async () => null,
|
|
573
|
-
};
|
|
574
|
-
mockSkillsshProvider = {
|
|
575
|
-
canHandle: () => false,
|
|
576
|
-
listFiles: async () => null,
|
|
577
|
-
readFileContent: async () => null,
|
|
578
|
-
toSlimSkill: async () => null,
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
const clawhubFiles: SkillFileEntry[] = [
|
|
582
|
-
{
|
|
583
|
-
path: "SKILL.md",
|
|
584
|
-
name: "SKILL.md",
|
|
585
|
-
size: 200,
|
|
586
|
-
mimeType: "",
|
|
587
|
-
isBinary: false,
|
|
588
|
-
content: null,
|
|
589
|
-
},
|
|
590
|
-
{
|
|
591
|
-
path: "lib/utils.js",
|
|
592
|
-
name: "utils.js",
|
|
593
|
-
size: 500,
|
|
594
|
-
mimeType: "",
|
|
595
|
-
isBinary: false,
|
|
596
|
-
content: null,
|
|
597
|
-
},
|
|
598
|
-
];
|
|
599
|
-
mockClawhubProvider = {
|
|
600
|
-
canHandle: () => true,
|
|
601
|
-
listFiles: async () => clawhubFiles,
|
|
602
|
-
readFileContent: async () => null,
|
|
603
|
-
toSlimSkill: async () => ({
|
|
604
|
-
id: "cool-tool",
|
|
605
|
-
name: "Cool Tool",
|
|
606
|
-
description: "A clawhub skill",
|
|
607
|
-
kind: "catalog",
|
|
608
|
-
origin: "clawhub",
|
|
609
|
-
status: "available",
|
|
610
|
-
category: "knowledge",
|
|
356
|
+
// Re-install the shim with a canned inspect payload including a files list.
|
|
357
|
+
installNpxShim({
|
|
358
|
+
skill: {
|
|
611
359
|
slug: "cool-tool",
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
360
|
+
displayName: "Cool Tool",
|
|
361
|
+
summary: "A clawhub skill",
|
|
362
|
+
},
|
|
363
|
+
owner: { handle: "someone" },
|
|
364
|
+
stats: { stars: 5, installs: 10 },
|
|
365
|
+
files: [
|
|
366
|
+
{ path: "SKILL.md", size: 200 },
|
|
367
|
+
{ path: "lib/utils.js", size: 500, contentType: "text/javascript" },
|
|
368
|
+
],
|
|
369
|
+
});
|
|
619
370
|
|
|
620
371
|
const result = await getSkillFiles("cool-tool");
|
|
621
372
|
|
|
@@ -623,17 +374,15 @@ describe("getSkillFiles — provider chain fallback", () => {
|
|
|
623
374
|
if ("error" in result) {
|
|
624
375
|
return;
|
|
625
376
|
}
|
|
377
|
+
// The REAL provider mapped the CLI's inspect output.
|
|
626
378
|
expect(result.skill.origin).toBe("clawhub");
|
|
379
|
+
expect(result.skill.name).toBe("Cool Tool");
|
|
627
380
|
expect(result.files).toHaveLength(2);
|
|
381
|
+
expect(result.files[0].content).toBeNull();
|
|
628
382
|
});
|
|
629
383
|
|
|
630
|
-
test("ID that no provider
|
|
631
|
-
|
|
632
|
-
mockCadetProvider = makeNoopProvider("cadet");
|
|
633
|
-
mockSkillsshProvider = makeNoopProvider("skillssh");
|
|
634
|
-
mockClawhubProvider = makeNoopProvider("clawhub");
|
|
635
|
-
|
|
636
|
-
const result = await getSkillFiles("unknown-origin-skill");
|
|
384
|
+
test("ID that no provider canHandle returns 404", async () => {
|
|
385
|
+
const result = await getSkillFiles("no such skill anywhere");
|
|
637
386
|
|
|
638
387
|
expect("error" in result).toBe(true);
|
|
639
388
|
if (!("error" in result)) {
|
|
@@ -643,80 +392,22 @@ describe("getSkillFiles — provider chain fallback", () => {
|
|
|
643
392
|
});
|
|
644
393
|
|
|
645
394
|
test("provider priority — cadet provider is tried before skills.sh and clawhub", async () => {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
size: 10,
|
|
660
|
-
mimeType: "",
|
|
661
|
-
isBinary: false,
|
|
662
|
-
content: null,
|
|
663
|
-
},
|
|
664
|
-
];
|
|
665
|
-
},
|
|
666
|
-
readFileContent: async () => null,
|
|
667
|
-
toSlimSkill: async () => ({
|
|
668
|
-
id: "contested-skill",
|
|
669
|
-
name: "Cadet Contested",
|
|
670
|
-
description: "",
|
|
671
|
-
kind: "catalog",
|
|
672
|
-
origin: "cadet",
|
|
673
|
-
status: "available",
|
|
674
|
-
category: "knowledge",
|
|
675
|
-
}),
|
|
676
|
-
};
|
|
677
|
-
mockSkillsshProvider = {
|
|
678
|
-
canHandle: () => true,
|
|
679
|
-
listFiles: async (skillId: string) => {
|
|
680
|
-
skillsshListFilesCalls.push(skillId);
|
|
681
|
-
return [];
|
|
682
|
-
},
|
|
683
|
-
readFileContent: async () => null,
|
|
684
|
-
toSlimSkill: async () => ({
|
|
685
|
-
id: "contested-skill",
|
|
686
|
-
name: "Skillssh Contested",
|
|
687
|
-
description: "",
|
|
688
|
-
kind: "catalog",
|
|
689
|
-
origin: "skillssh",
|
|
690
|
-
status: "available",
|
|
691
|
-
category: "knowledge",
|
|
692
|
-
slug: "contested-skill",
|
|
693
|
-
sourceRepo: "",
|
|
694
|
-
installs: 0,
|
|
695
|
-
}),
|
|
696
|
-
};
|
|
697
|
-
mockClawhubProvider = {
|
|
698
|
-
canHandle: () => true,
|
|
699
|
-
listFiles: async (skillId: string) => {
|
|
700
|
-
clawhubListFilesCalls.push(skillId);
|
|
701
|
-
return [];
|
|
395
|
+
const http = mockHttp();
|
|
396
|
+
http.on("GET", "/v1/skills/", {
|
|
397
|
+
status: 200,
|
|
398
|
+
body: catalogBody([
|
|
399
|
+
catalogEntry("contested-skill", {
|
|
400
|
+
cadet: { "display-name": "Cadet Contested" },
|
|
401
|
+
}),
|
|
402
|
+
]),
|
|
403
|
+
});
|
|
404
|
+
http.on("GET", "/v1/skills/contested-skill/files/", {
|
|
405
|
+
status: 200,
|
|
406
|
+
body: {
|
|
407
|
+
files: [{ path: "SKILL.md", name: "SKILL.md", size: 10 }],
|
|
702
408
|
},
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
id: "contested-skill",
|
|
706
|
-
name: "Clawhub Contested",
|
|
707
|
-
description: "",
|
|
708
|
-
kind: "catalog",
|
|
709
|
-
origin: "clawhub",
|
|
710
|
-
status: "available",
|
|
711
|
-
category: "knowledge",
|
|
712
|
-
slug: "contested-skill",
|
|
713
|
-
author: "",
|
|
714
|
-
stars: 0,
|
|
715
|
-
installs: 0,
|
|
716
|
-
reports: 0,
|
|
717
|
-
version: "",
|
|
718
|
-
}),
|
|
719
|
-
};
|
|
409
|
+
});
|
|
410
|
+
await getCatalog();
|
|
720
411
|
|
|
721
412
|
const result = await getSkillFiles("contested-skill");
|
|
722
413
|
|
|
@@ -727,17 +418,8 @@ describe("getSkillFiles — provider chain fallback", () => {
|
|
|
727
418
|
// Cadet provider should have been used
|
|
728
419
|
expect(result.skill.origin).toBe("cadet");
|
|
729
420
|
expect(result.skill.name).toBe("Cadet Contested");
|
|
730
|
-
//
|
|
731
|
-
|
|
732
|
-
expect(
|
|
733
|
-
expect(clawhubListFilesCalls).toEqual([]);
|
|
421
|
+
// The chain stops on the first match: the clawhub CLI never spawned even
|
|
422
|
+
// though its canHandle claims every valid slug.
|
|
423
|
+
expect(clawhubCalls()).toEqual([]);
|
|
734
424
|
});
|
|
735
425
|
});
|
|
736
|
-
|
|
737
|
-
// ---------------------------------------------------------------------------
|
|
738
|
-
// Cleanup: ensure we don't leak temp dirs if a test fails mid-way.
|
|
739
|
-
// ---------------------------------------------------------------------------
|
|
740
|
-
|
|
741
|
-
afterEach(() => {
|
|
742
|
-
// Nothing to clean up outside test scope — temp dirs are cleaned per-test.
|
|
743
|
-
});
|