skillrepo 4.8.3 → 4.8.4
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/package.json
CHANGED
package/src/lib/http.mjs
CHANGED
|
@@ -370,6 +370,25 @@ async function mapErrorResponse(res, url) {
|
|
|
370
370
|
if (res.status === 404) {
|
|
371
371
|
return null; // Caller decides
|
|
372
372
|
}
|
|
373
|
+
if (res.status === 422 && code === "handle_required") {
|
|
374
|
+
// #2310: the push would CREATE a skill while the account's Author ID
|
|
375
|
+
// is still an unchosen placeholder. Line 1 is the server's message
|
|
376
|
+
// ("Set your Author ID before adding skills."); the hint points at
|
|
377
|
+
// the Settings surface that unblocks it. Derive the base URL from
|
|
378
|
+
// the request URL's origin (same posture as the 401 auth hint above)
|
|
379
|
+
// so staging users get a staging link, with the prod host as the
|
|
380
|
+
// parse-failure fallback.
|
|
381
|
+
let settingsBase = "https://skillrepo.dev";
|
|
382
|
+
try {
|
|
383
|
+
settingsBase = new URL(url).origin;
|
|
384
|
+
} catch {
|
|
385
|
+
// URL parse failure shouldn't happen — `safeFetch` already
|
|
386
|
+
// validated. Fall back to the prod default.
|
|
387
|
+
}
|
|
388
|
+
return validationError(message, {
|
|
389
|
+
hint: `Set it in Settings → Account Profile: ${settingsBase}/app/settings`,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
373
392
|
if (res.status === 429) {
|
|
374
393
|
// 429 hits mapErrorResponse in two cases: (1) a read-only
|
|
375
394
|
// endpoint's retry budget was exhausted (safeFetch with
|
|
@@ -220,7 +220,7 @@ describe("runPublish — error paths", () => {
|
|
|
220
220
|
server.setPublishResponse("alice", "my-skill", {
|
|
221
221
|
status: 422,
|
|
222
222
|
body: {
|
|
223
|
-
error: "
|
|
223
|
+
error: "Set your Author ID before publishing.",
|
|
224
224
|
code: "namespace_unset",
|
|
225
225
|
},
|
|
226
226
|
});
|
|
@@ -453,6 +453,30 @@ describe("runPush — server error mapping", () => {
|
|
|
453
453
|
);
|
|
454
454
|
});
|
|
455
455
|
|
|
456
|
+
it("maps server 422 handle_required to EXIT_VALIDATION with the Settings hint (#2310, #2345)", async () => {
|
|
457
|
+
// The dedicated #2310 branch in mapErrorResponse: friendly message
|
|
458
|
+
// plus the Settings pointer. Previously pinned only at the
|
|
459
|
+
// http.test.mjs layer against hand-built JSON — this runs the whole
|
|
460
|
+
// runPush chain against the same wire shape the server emits
|
|
461
|
+
// (route.ts:724: `{ error, code: "handle_required" }`).
|
|
462
|
+
file("SKILL.md", VALID_SKILL_MD);
|
|
463
|
+
server.setPushResponse({
|
|
464
|
+
status: 422,
|
|
465
|
+
body: {
|
|
466
|
+
error: "Set your Author ID before adding skills.",
|
|
467
|
+
code: "handle_required",
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
await assert.rejects(
|
|
471
|
+
runPush(["--key", VALID_KEY, "--url", serverUrl, "skill"], { stdout }),
|
|
472
|
+
(err) =>
|
|
473
|
+
err instanceof CliError &&
|
|
474
|
+
err.exitCode === EXIT_VALIDATION &&
|
|
475
|
+
/author id/i.test(err.message) &&
|
|
476
|
+
/settings/i.test(err.hint ?? ""),
|
|
477
|
+
);
|
|
478
|
+
});
|
|
479
|
+
|
|
456
480
|
it("maps server 413 payload_too_large to EXIT_VALIDATION (generic 4xx)", async () => {
|
|
457
481
|
// 413 has NO explicit branch in mapErrorResponse (only 401/403/404/
|
|
458
482
|
// 429/5xx are special-cased). It therefore falls through to the
|
|
@@ -1296,6 +1296,93 @@ describe("pushSkill", () => {
|
|
|
1296
1296
|
}
|
|
1297
1297
|
});
|
|
1298
1298
|
|
|
1299
|
+
it("maps 422 handle_required to validationError with a Settings hint on the server's own host (#2310)", async () => {
|
|
1300
|
+
const SERVER_MESSAGE = "Set your Author ID before adding skills.";
|
|
1301
|
+
const srv = await makePushServer((req, res) => {
|
|
1302
|
+
jsonRes(res, 422, {
|
|
1303
|
+
error: SERVER_MESSAGE,
|
|
1304
|
+
code: "handle_required",
|
|
1305
|
+
});
|
|
1306
|
+
});
|
|
1307
|
+
try {
|
|
1308
|
+
await assert.rejects(
|
|
1309
|
+
() =>
|
|
1310
|
+
pushSkill(srv.url, VALID_KEY, {
|
|
1311
|
+
files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
|
|
1312
|
+
}),
|
|
1313
|
+
(err) =>
|
|
1314
|
+
err instanceof CliError &&
|
|
1315
|
+
err.exitCode === EXIT_VALIDATION &&
|
|
1316
|
+
// Line 1: the server's message verbatim; line 2 (hint): the
|
|
1317
|
+
// Settings path on the REQUEST host (staging users get a
|
|
1318
|
+
// staging link), not hardcoded prod.
|
|
1319
|
+
err.message === SERVER_MESSAGE &&
|
|
1320
|
+
err.hint === `Set it in Settings → Account Profile: ${srv.url}/app/settings`,
|
|
1321
|
+
);
|
|
1322
|
+
} finally {
|
|
1323
|
+
await srv.close();
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
it("handle_required hint falls back to the prod host when the request URL is unparseable (#2310)", async () => {
|
|
1328
|
+
// The `new URL(url)` origin-derivation inside the handle_required
|
|
1329
|
+
// mapping can only throw if the URL that reached fetch was itself
|
|
1330
|
+
// unparseable — impossible over a real socket (undici validates the
|
|
1331
|
+
// URL before connecting), so stub fetch to hand back the 422
|
|
1332
|
+
// directly. `normalizeUrl` passes any non-empty string through, so
|
|
1333
|
+
// the raw serverUrl below reaches mapErrorResponse verbatim.
|
|
1334
|
+
const SERVER_MESSAGE = "Set your Author ID before adding skills.";
|
|
1335
|
+
const realFetch = globalThis.fetch;
|
|
1336
|
+
globalThis.fetch = async () =>
|
|
1337
|
+
new Response(
|
|
1338
|
+
JSON.stringify({ error: SERVER_MESSAGE, code: "handle_required" }),
|
|
1339
|
+
{ status: 422, headers: { "Content-Type": "application/json" } },
|
|
1340
|
+
);
|
|
1341
|
+
try {
|
|
1342
|
+
await assert.rejects(
|
|
1343
|
+
() =>
|
|
1344
|
+
pushSkill("not a url", VALID_KEY, {
|
|
1345
|
+
files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
|
|
1346
|
+
}),
|
|
1347
|
+
(err) =>
|
|
1348
|
+
err instanceof CliError &&
|
|
1349
|
+
err.exitCode === EXIT_VALIDATION &&
|
|
1350
|
+
err.message === SERVER_MESSAGE &&
|
|
1351
|
+
// Parse-failure fallback: the hint pins the prod host instead
|
|
1352
|
+
// of the (unusable) request origin.
|
|
1353
|
+
err.hint ===
|
|
1354
|
+
"Set it in Settings → Account Profile: https://skillrepo.dev/app/settings",
|
|
1355
|
+
);
|
|
1356
|
+
} finally {
|
|
1357
|
+
globalThis.fetch = realFetch;
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
it("does NOT special-case a 422 without the handle_required code (generic validation)", async () => {
|
|
1362
|
+
// A 422 carrying a different code (e.g. idempotency_key_reused) must
|
|
1363
|
+
// keep the pre-#2310 generic mapping: validationError, no Settings hint.
|
|
1364
|
+
const srv = await makePushServer((req, res) => {
|
|
1365
|
+
jsonRes(res, 422, {
|
|
1366
|
+
error: "Idempotency-Key was reused with a different request body.",
|
|
1367
|
+
code: "idempotency_key_reused",
|
|
1368
|
+
});
|
|
1369
|
+
});
|
|
1370
|
+
try {
|
|
1371
|
+
await assert.rejects(
|
|
1372
|
+
() =>
|
|
1373
|
+
pushSkill(srv.url, VALID_KEY, {
|
|
1374
|
+
files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
|
|
1375
|
+
}),
|
|
1376
|
+
(err) =>
|
|
1377
|
+
err instanceof CliError &&
|
|
1378
|
+
err.exitCode === EXIT_VALIDATION &&
|
|
1379
|
+
err.hint === undefined,
|
|
1380
|
+
);
|
|
1381
|
+
} finally {
|
|
1382
|
+
await srv.close();
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1299
1386
|
it("maps 413 payload_too_large through mapErrorResponse", async () => {
|
|
1300
1387
|
const srv = await makePushServer((req, res) => {
|
|
1301
1388
|
jsonRes(res, 413, {
|