diff --git a/cli/test/helpers/fake-registry.ts b/cli/test/helpers/fake-registry.ts index 4fde3a95..51b02b94 100644 --- a/cli/test/helpers/fake-registry.ts +++ b/cli/test/helpers/fake-registry.ts @@ -23,11 +23,12 @@ export function createFakeRegistry(handlers: Record) { * Controls how a specific endpoint behaves when a failure is injected: * 'auth' => 401 { code: 401, message: 'unauthorized' } * 'forbidden' => 403 with a standard SkillHub error envelope + * 'forbidden_unstructured' => 403 with a non-JSON response body * 'not_found' => 404 { code: 404, message: 'not found' } * 'server_error' => 500 { code: 500, message: 'internal error' } * 'network' => handler throws, causing fetch() to reject with a TypeError */ -export type FailureMode = 'auth' | 'forbidden' | 'not_found' | 'server_error' | 'network' +export type FailureMode = 'auth' | 'forbidden' | 'forbidden_unstructured' | 'not_found' | 'server_error' | 'network' function failureResponse(mode: FailureMode): Response { switch (mode) { @@ -39,6 +40,11 @@ function failureResponse(mode: FailureMode): Response { msg: 'API token is missing required scope: skill:publish', requestId: 'req-test-forbidden' }, { status: 403 }) + case 'forbidden_unstructured': + return new Response('sensitive proxy denial', { + status: 403, + headers: { 'Content-Type': 'text/html' } + }) case 'not_found': return Response.json({ code: 404, message: 'not found' }, { status: 404 }) case 'server_error': diff --git a/cli/test/integration/publish-dry-run.test.ts b/cli/test/integration/publish-dry-run.test.ts index e9290d7b..5c18aad3 100644 --- a/cli/test/integration/publish-dry-run.test.ts +++ b/cli/test/integration/publish-dry-run.test.ts @@ -159,7 +159,7 @@ describe('publish --dry-run', () => { expect(result.stderr).toContain('authentication') }) - test('--dry-run preserves a structured 403 reason and request ID', async () => { + test('--dry-run surfaces the public message and request ID on a structured 403', async () => { const env = await createTempHome() registry = await startFakeRegistry({ token: 'sk_ok', failures: { validate: 'forbidden' } }) await login(env, registry.url) @@ -174,4 +174,20 @@ describe('publish --dry-run', () => { expect(result.stderr).toContain('scope') expect(result.stderr).toContain('Request ID: req-test-forbidden') }) + + test('--dry-run uses a neutral fallback without leaking an unstructured 403 body', async () => { + const env = await createTempHome() + registry = await startFakeRegistry({ token: 'sk_ok', failures: { validate: 'forbidden_unstructured' } }) + await login(env, registry.url) + + const dir = await makeTempDir(['SKILL.md', '---\nname: test\ndescription: test\n---\n']) + const result = await runCli(['publish', dir, '--dry-run', '--registry', registry.url], { + HOME: env.home, + USERPROFILE: env.home + }) + + expect(result.exitCode).toBe(2) + expect(result.stderr).toContain('access denied') + expect(result.stderr).not.toContain('sensitive proxy denial') + }) }) diff --git a/docs/superpowers/plans/2026-07-28-cli-namespace-errors.md b/docs/superpowers/plans/2026-07-28-cli-namespace-errors.md index ee71ab89..46247901 100644 --- a/docs/superpowers/plans/2026-07-28-cli-namespace-errors.md +++ b/docs/superpowers/plans/2026-07-28-cli-namespace-errors.md @@ -16,8 +16,8 @@ revalidation did not recreate historical RED states; it reran the current GREEN gates with the repository-pinned Bun 1.3.13: - Focused namespace/error/help regression: 142 tests passed. -- Complete CLI regression: 378 tests passed with - `bun test --max-concurrency=1` (peak RSS 152128 KiB). +- Complete CLI regression: 379 tests passed with + `bun test --max-concurrency=1` (peak RSS 180452 KiB). - Typecheck, lint, and build passed. - The packed `@astron-team/skillhub@0.1.9` artifact contained `dist/index.js`, `README.md`, `CHANGELOG.md`, `LICENSE`, and `package.json`.