From 380fe0a43613c689617ad355451a6a22f9e28c62 Mon Sep 17 00:00:00 2001 From: nopp Date: Fri, 2 Oct 2026 17:11:57 +0700 Subject: [PATCH 1/6] fix(debug): log swallowed errors in test advisory and init setup (Refs #186) --- src/commands/init.test.ts | 31 ++++++++++++++++++++++++++++++ src/commands/init.ts | 20 ++++++++++++++------ src/commands/test.test.ts | 40 +++++++++++++++++++++++++++++++++++++++ src/commands/test.ts | 9 +++++++-- 4 files changed, 92 insertions(+), 8 deletions(-) diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index be87e27c..79450ea2 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -614,6 +614,37 @@ describe('runInit — happy path (interactive)', () => { expect(captured.stderr.some(line => line.includes('setup identity lookup failed'))).toBe(true); }); + it('debug mode reports resolve endpoint profile read failure without failing the command', async () => { + const { captured, deps } = makeCapture(); + const fetchMock = makeOkFetch(); + const badCredsPath = join(tmpdir(), `corrupt-creds-${Date.now()}.json`); + writeFileSync(badCredsPath, '{ not valid json'); + + try { + await runInit( + makeBaseOpts({ apiKey: 'sk-user-json-test', debug: true, noAgent: true, output: 'json' }), + { + ...deps, + fetchImpl: fetchMock, + credentialsPath: badCredsPath, + isTTY: false, + }, + ); + + const parsed = JSON.parse(captured.stdout.join('\n')) as Record; + expect(parsed.status).toBe('initialized'); + expect( + captured.stderr.some(line => line.includes('[debug] resolve endpoint profile read failed:')), + ).toBe(true); + } finally { + try { + unlinkSync(badCredsPath); + } catch { + // ignore + } + } + }); + it('stops before skill installation when the identity request is interrupted', async () => { const { captured, deps } = makeCapture(); const { fs: agentFs, writeCalls, mkdirCalls } = makeMemFs(); diff --git a/src/commands/init.ts b/src/commands/init.ts index d8be63ca..5d30eab0 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -48,13 +48,17 @@ const DEFAULT_API_URL = 'https://api.testsprite.com'; * configuring staging/dev (codex). On the real path this runs AFTER the profile * is written, so the persisted apiUrl is reflected faithfully. */ -function resolveReportedEndpoint(opts: InitOptions, deps: InitDeps): string { +function resolveReportedEndpoint(opts: InitOptions, deps: InitDeps, stderrFn?: (line: string) => void): string { const env = deps.env ?? process.env; const envApiUrl = normalizeEnvVar(env.TESTSPRITE_API_URL); let existing: string | undefined; try { existing = readProfile(opts.profile, { path: deps.credentialsPath })?.apiUrl; - } catch { + } catch (err) { + if (opts.debug && stderrFn) { + const reason = err instanceof Error ? err.message : String(err); + stderrFn(`[debug] resolve endpoint profile read failed: ${reason}`); + } existing = undefined; } return opts.endpointUrl ?? envApiUrl ?? existing ?? DEFAULT_API_URL; @@ -424,7 +428,7 @@ export async function runInit(opts: InitOptions, deps: InitDeps = {}): Promise, --from-env (reads TESTSPRITE_API_KEY), or run interactively.', - resolveReportedEndpoint(opts, deps), + resolveReportedEndpoint(opts, deps, stderrFn), ); } // JSON-output guard: an interactive secret prompt writes to stdout and would @@ -499,7 +503,7 @@ export async function runInit(opts: InitOptions, deps: InitDeps = {}): Promise 0) { capturedInstallResults = parsed; } - } catch { + } catch (err) { + if (opts.debug) { + const reason = err instanceof Error ? err.message : String(err); + stderrFn(`[debug] setup agent install output parse non-JSON: ${reason}`); + } // ignore non-JSON lines (shouldn't happen in json mode, but be safe) } }; @@ -643,7 +651,7 @@ export async function runInit(opts: InitOptions, deps: InitDeps = {}): Promise { false, ); }); + + it('Fix 4 — emits debug log when duplicate-name advisory lookup fails in debug mode', async () => { + const { credentialsPath } = makeCreds(); + const codeFile = writeCodeFile('// test code'); + const fetchImpl = makeFetch((url, init) => { + if ((init.method ?? 'GET') === 'GET' && url.includes('/tests')) { + return { + status: 403, + body: { + error: { + code: 'AUTH_FORBIDDEN', + message: 'Forbidden', + nextAction: '', + requestId: 'r1', + details: {}, + }, + }, + }; + } + return { body: SAMPLE_RESPONSE }; + }); + + const stderrLines: string[] = []; + await runCreate( + { + profile: 'default', + output: 'json', + debug: true, + projectId: 'project_alice', + type: 'frontend', + name: 'another test', + codeFile, + }, + { credentialsPath, fetchImpl, stdout: () => {}, stderr: line => stderrLines.push(line) }, + ); + + expect( + stderrLines.some(l => l.includes('[debug] duplicate-name advisory lookup failed: Forbidden')), + ).toBe(true); + }); }); // --------------------------------------------------------------------------- diff --git a/src/commands/test.ts b/src/commands/test.ts index 4f56afea..b7957387 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -1160,6 +1160,7 @@ async function emitDupNameAdvisoryIfNeeded( projectId: string | undefined, name: string | undefined, stderrFn: (line: string) => void, + debug?: boolean, ): Promise { if (!projectId || !name) return; // B: ordinary advisory failures must not block the create critical path. @@ -1197,6 +1198,10 @@ async function emitDupNameAdvisoryIfNeeded( } } catch (err) { if (err instanceof InterruptError) throw err; + if (debug) { + const reason = err instanceof Error ? err.message : String(err); + stderrFn(`[debug] duplicate-name advisory lookup failed: ${reason}`); + } // Other lookup failures are best-effort and must not block the create. } finally { clearTimeout(timer); @@ -1402,7 +1407,7 @@ export async function runCreate( // B3: best-effort duplicate-name advisory. Skip under --dry-run. if (!opts.dryRun) { const stderrFn = deps.stderr ?? ((line: string) => process.stderr.write(`${line}\n`)); - await emitDupNameAdvisoryIfNeeded(client, projectId, opts.name, stderrFn); + await emitDupNameAdvisoryIfNeeded(client, projectId, opts.name, stderrFn, opts.debug); } const response = await client.post('/tests', { @@ -3269,7 +3274,7 @@ export async function runCreateFromPlan( // The plan's projectId + name are available after validation above. Skip // under dry-run (no network calls); swallow all errors (advisory only). if (!opts.dryRun) { - await emitDupNameAdvisoryIfNeeded(client, plan.projectId, plan.name, stderrFn); + await emitDupNameAdvisoryIfNeeded(client, plan.projectId, plan.name, stderrFn, opts.debug); } const response = await client.post('/tests', { From b92c3f59b85c9c61afc7fdf7d33e75002fd43327 Mon Sep 17 00:00:00 2001 From: nopp Date: Fri, 2 Oct 2026 17:13:39 +0700 Subject: [PATCH 2/6] test(init): add security lint disable justifications for temp credentials path --- src/commands/init.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index 79450ea2..87914615 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -618,6 +618,7 @@ describe('runInit — happy path (interactive)', () => { const { captured, deps } = makeCapture(); const fetchMock = makeOkFetch(); const badCredsPath = join(tmpdir(), `corrupt-creds-${Date.now()}.json`); + // eslint-disable-next-line security/detect-non-literal-fs-filename -- writes this test's own temp credentials path, never user input. writeFileSync(badCredsPath, '{ not valid json'); try { @@ -638,6 +639,7 @@ describe('runInit — happy path (interactive)', () => { ).toBe(true); } finally { try { + // eslint-disable-next-line security/detect-non-literal-fs-filename -- cleanup of this test's own temp path. unlinkSync(badCredsPath); } catch { // ignore From 99fa92402d66ba93bc916f03813321b3f3718e2c Mon Sep 17 00:00:00 2001 From: nopp Date: Fri, 2 Oct 2026 17:16:16 +0700 Subject: [PATCH 3/6] style(init): format init.ts and init.test.ts with prettier --- src/commands/init.test.ts | 4 +++- src/commands/init.ts | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index 87914615..af263b8c 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -635,7 +635,9 @@ describe('runInit — happy path (interactive)', () => { const parsed = JSON.parse(captured.stdout.join('\n')) as Record; expect(parsed.status).toBe('initialized'); expect( - captured.stderr.some(line => line.includes('[debug] resolve endpoint profile read failed:')), + captured.stderr.some(line => + line.includes('[debug] resolve endpoint profile read failed:'), + ), ).toBe(true); } finally { try { diff --git a/src/commands/init.ts b/src/commands/init.ts index 5d30eab0..45aab11b 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -48,7 +48,11 @@ const DEFAULT_API_URL = 'https://api.testsprite.com'; * configuring staging/dev (codex). On the real path this runs AFTER the profile * is written, so the persisted apiUrl is reflected faithfully. */ -function resolveReportedEndpoint(opts: InitOptions, deps: InitDeps, stderrFn?: (line: string) => void): string { +function resolveReportedEndpoint( + opts: InitOptions, + deps: InitDeps, + stderrFn?: (line: string) => void, +): string { const env = deps.env ?? process.env; const envApiUrl = normalizeEnvVar(env.TESTSPRITE_API_URL); let existing: string | undefined; From d081107d8e9a5544f232812439109ac31a03bcfb Mon Sep 17 00:00:00 2001 From: nopp Date: Sun, 4 Oct 2026 03:50:08 +0700 Subject: [PATCH 4/6] test(init): mock readFileSync failure to exercise debug log in resolveReportedEndpoint (Refs #186) --- src/commands/init.test.ts | 60 ++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index af263b8c..b2638aab 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -616,37 +616,39 @@ describe('runInit — happy path (interactive)', () => { it('debug mode reports resolve endpoint profile read failure without failing the command', async () => { const { captured, deps } = makeCapture(); - const fetchMock = makeOkFetch(); - const badCredsPath = join(tmpdir(), `corrupt-creds-${Date.now()}.json`); - // eslint-disable-next-line security/detect-non-literal-fs-filename -- writes this test's own temp credentials path, never user input. - writeFileSync(badCredsPath, '{ not valid json'); - - try { - await runInit( - makeBaseOpts({ apiKey: 'sk-user-json-test', debug: true, noAgent: true, output: 'json' }), - { - ...deps, - fetchImpl: fetchMock, - credentialsPath: badCredsPath, - isTTY: false, - }, - ); + const fetchMock = makeOkFetch()!; + const actual = await vi.importActual('node:fs'); + let fetchCount = 0; + const trackingFetch = vi.fn(async (input: Parameters[0], init?: Parameters[1]) => { + fetchCount++; + return fetchMock(input, init); + }); - const parsed = JSON.parse(captured.stdout.join('\n')) as Record; - expect(parsed.status).toBe('initialized'); - expect( - captured.stderr.some(line => - line.includes('[debug] resolve endpoint profile read failed:'), - ), - ).toBe(true); - } finally { - try { - // eslint-disable-next-line security/detect-non-literal-fs-filename -- cleanup of this test's own temp path. - unlinkSync(badCredsPath); - } catch { - // ignore + vi.mocked(readFileSync).mockImplementation((...args) => { + // After both configure and whoami fetch calls have completed, fail the subsequent readProfile call in resolveReportedEndpoint + if (fetchCount >= 2 && args[0] === credentialsPath) { + throw new Error('simulated credentials read error'); } - } + return actual.readFileSync(...args); + }); + + await runInit( + makeBaseOpts({ apiKey: 'sk-user-json-test', debug: true, noAgent: true, output: 'json' }), + { + ...deps, + fetchImpl: trackingFetch, + credentialsPath, + isTTY: false, + }, + ); + + const parsed = JSON.parse(captured.stdout.join('\n')) as Record; + expect(parsed.status).toBe('initialized'); + expect( + captured.stderr.some(line => + line.includes('[debug] resolve endpoint profile read failed: simulated credentials read error'), + ), + ).toBe(true); }); it('stops before skill installation when the identity request is interrupted', async () => { From 5234e84cca5f7d6fc105219ed802a9029bf25232 Mon Sep 17 00:00:00 2001 From: nopp Date: Sun, 4 Oct 2026 03:56:05 +0700 Subject: [PATCH 5/6] style(init): format init.test.ts with prettier --- src/commands/init.test.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index b2638aab..3b8e198f 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -619,10 +619,12 @@ describe('runInit — happy path (interactive)', () => { const fetchMock = makeOkFetch()!; const actual = await vi.importActual('node:fs'); let fetchCount = 0; - const trackingFetch = vi.fn(async (input: Parameters[0], init?: Parameters[1]) => { - fetchCount++; - return fetchMock(input, init); - }); + const trackingFetch = vi.fn( + async (input: Parameters[0], init?: Parameters[1]) => { + fetchCount++; + return fetchMock(input, init); + }, + ); vi.mocked(readFileSync).mockImplementation((...args) => { // After both configure and whoami fetch calls have completed, fail the subsequent readProfile call in resolveReportedEndpoint @@ -646,7 +648,9 @@ describe('runInit — happy path (interactive)', () => { expect(parsed.status).toBe('initialized'); expect( captured.stderr.some(line => - line.includes('[debug] resolve endpoint profile read failed: simulated credentials read error'), + line.includes( + '[debug] resolve endpoint profile read failed: simulated credentials read error', + ), ), ).toBe(true); }); From 30eee57c624c49e846630ffdce476bae2ad178d6 Mon Sep 17 00:00:00 2001 From: naufalfx805-source Date: Tue, 10 Nov 2026 18:59:03 +0700 Subject: [PATCH 6/6] test(debug): assert no [debug] output without --debug at each site (Refs #186) Review follow-up for #354. - Duplicate-name advisory lookup failure (test create and create-from-plan) and resolve-endpoint profile read failure: without --debug, stderr has no [debug] line. Each test also checks that the failure really happened. - Cover the agent install output parse site, with and without --debug. runInstall prints one JSON document in json mode, so the tests wrap it with a pass-through double (same shape as the node:fs doubles in that file) and make it write one stray line first. - Cover the create-from-plan call site of the advisory under --debug. Co-Authored-By: Claude Fable 5.2 --- src/commands/init.test.ts | 99 +++++++++++++++++++++++++++++++++++++-- src/commands/test.test.ts | 99 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 3 deletions(-) diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index 4f553b9d..622c81dc 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -21,6 +21,8 @@ import { resetDryRunBannerForTesting } from '../lib/client-factory.js'; import { readProfile, writeProfile } from '../lib/credentials.js'; import type { MeResponse } from './auth.js'; import type { AgentFs } from './agent.js'; +import type * as AgentModule from './agent.js'; +import { runInstall } from './agent.js'; import { Command } from 'commander'; import type { InitDeps } from './init.js'; import { runInit, addSetupOptions } from './init.js'; @@ -44,6 +46,13 @@ vi.mock('node:fs', async importOriginal => { }; }); +// runInstall gets the same pass-through double as the node:fs functions above, +// so a test can make the agent install step write a stray stdout line. +vi.mock('./agent.js', async importOriginal => { + const actual = await importOriginal(); + return { ...actual, runInstall: vi.fn(actual.runInstall) }; +}); + // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- @@ -216,6 +225,7 @@ afterEach(() => { for (const fn of [mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync]) { vi.mocked(fn).mockReset(); } + vi.mocked(runInstall).mockReset(); }); describe('runInit — session-only environment credentials', () => { @@ -685,11 +695,18 @@ describe('runInit — happy path (interactive)', () => { expect(captured.stderr.some(line => line.includes('setup identity lookup failed'))).toBe(true); }); - it('debug mode reports resolve endpoint profile read failure without failing the command', async () => { + /** + * Runs setup with the profile read in resolveReportedEndpoint failing: once + * the configure and whoami fetches are done, reading the credentials file throws. + */ + async function runInitWithFailingProfileRead( + debug: boolean, + ): Promise<{ captured: Captured; failedReads: number }> { const { captured, deps } = makeCapture(); const fetchMock = makeOkFetch()!; const actual = await vi.importActual('node:fs'); let fetchCount = 0; + let failedReads = 0; const trackingFetch = vi.fn( async (input: Parameters[0], init?: Parameters[1]) => { fetchCount++; @@ -698,15 +715,15 @@ describe('runInit — happy path (interactive)', () => { ); vi.mocked(readFileSync).mockImplementation((...args) => { - // After both configure and whoami fetch calls have completed, fail the subsequent readProfile call in resolveReportedEndpoint if (fetchCount >= 2 && args[0] === credentialsPath) { + failedReads++; throw new Error('simulated credentials read error'); } return actual.readFileSync(...args); }); await runInit( - makeBaseOpts({ apiKey: 'sk-user-json-test', debug: true, noAgent: true, output: 'json' }), + makeBaseOpts({ apiKey: 'sk-user-json-test', debug, noAgent: true, output: 'json' }), { ...deps, fetchImpl: trackingFetch, @@ -715,8 +732,15 @@ describe('runInit — happy path (interactive)', () => { }, ); + return { captured, failedReads }; + } + + it('debug mode reports resolve endpoint profile read failure without failing the command', async () => { + const { captured, failedReads } = await runInitWithFailingProfileRead(true); + const parsed = JSON.parse(captured.stdout.join('\n')) as Record; expect(parsed.status).toBe('initialized'); + expect(failedReads).toBeGreaterThan(0); expect( captured.stderr.some(line => line.includes( @@ -726,6 +750,75 @@ describe('runInit — happy path (interactive)', () => { ).toBe(true); }); + it('keeps stderr free of [debug] lines when the profile read fails without --debug', async () => { + const { captured, failedReads } = await runInitWithFailingProfileRead(false); + + const parsed = JSON.parse(captured.stdout.join('\n')) as Record; + expect(parsed.status).toBe('initialized'); + // The read did fail; it just must not be reported. + expect(failedReads).toBeGreaterThan(0); + expect(captured.stderr.filter(line => line.includes('[debug]'))).toEqual([]); + }); + + /** + * Runs setup with the agent install step writing one stray non-JSON stdout + * line before its real result. runInstall prints a single JSON document in + * json mode, so the parse fallback is only reachable by faking that output. + */ + async function runInitWithStrayInstallOutput(debug: boolean): Promise { + const { captured, deps } = makeCapture(); + const { fs: agentFs } = makeMemFs(); + const actual = await vi.importActual('./agent.js'); + vi.mocked(runInstall).mockImplementationOnce(async (installOpts, installDeps) => { + installDeps?.stdout?.('not json'); + await actual.runInstall(installOpts, installDeps); + }); + + await runInit(makeBaseOpts({ apiKey: 'sk-user-json-test', debug, output: 'json' }), { + ...deps, + fetchImpl: makeOkFetch(), + credentialsPath, + isTTY: false, + cwd: CWD, + fs: agentFs, + }); + + return captured; + } + + it('debug mode reports a non-JSON line from the agent install step and keeps the summary', async () => { + const captured = await runInitWithStrayInstallOutput(true); + + const parsed = JSON.parse(captured.stdout.join('\n')) as { + status: string; + agent: { action: string; skills?: string[] } | null; + }; + expect(parsed.status).toBe('initialized'); + // The real install result after the stray line is still picked up. + expect(parsed.agent?.action).toBe('installed'); + expect(parsed.agent?.skills).toContain('testsprite-verify'); + expect(vi.mocked(runInstall)).toHaveBeenCalledTimes(1); + expect( + captured.stderr.filter(line => + line.includes('[debug] setup agent install output parse non-JSON:'), + ), + ).toHaveLength(1); + }); + + it('keeps stderr free of [debug] lines when the agent install step writes a non-JSON line without --debug', async () => { + const captured = await runInitWithStrayInstallOutput(false); + + const parsed = JSON.parse(captured.stdout.join('\n')) as { + status: string; + agent: { action: string } | null; + }; + expect(parsed.status).toBe('initialized'); + expect(parsed.agent?.action).toBe('installed'); + // The stray line was written; it just must not be reported. + expect(vi.mocked(runInstall)).toHaveBeenCalledTimes(1); + expect(captured.stderr.filter(line => line.includes('[debug]'))).toEqual([]); + }); + it('stops before skill installation when the identity request is interrupted', async () => { const { captured, deps } = makeCapture(); const { fs: agentFs, writeCalls, mkdirCalls } = makeMemFs(); diff --git a/src/commands/test.test.ts b/src/commands/test.test.ts index b1cea5ff..dda3556a 100644 --- a/src/commands/test.test.ts +++ b/src/commands/test.test.ts @@ -7703,6 +7703,49 @@ describe('runCreate', () => { stderrLines.some(l => l.includes('[debug] duplicate-name advisory lookup failed: Forbidden')), ).toBe(true); }); + + it('Fix 4 — keeps stderr free of [debug] lines when the lookup fails without --debug', async () => { + const { credentialsPath } = makeCreds(); + const codeFile = writeCodeFile('// test code'); + let listCalled = false; + const fetchImpl = makeFetch((url, init) => { + if ((init.method ?? 'GET') === 'GET' && url.includes('/tests')) { + listCalled = true; + return { + status: 403, + body: { + error: { + code: 'AUTH_FORBIDDEN', + message: 'Forbidden', + nextAction: '', + requestId: 'r1', + details: {}, + }, + }, + }; + } + return { body: SAMPLE_RESPONSE }; + }); + + const stderrLines: string[] = []; + const result = await runCreate( + { + profile: 'default', + output: 'json', + debug: false, + projectId: 'project_alice', + type: 'frontend', + name: 'another test', + codeFile, + }, + { credentialsPath, fetchImpl, stdout: () => {}, stderr: line => stderrLines.push(line) }, + ); + + // The lookup did fail and the create still went through; nothing is reported. + expect(listCalled).toBe(true); + expect(result.testId).toBe('test_new'); + expect(stderrLines.filter(l => l.includes('[debug]'))).toEqual([]); + }); }); // --------------------------------------------------------------------------- @@ -10238,6 +10281,62 @@ describe('runCreateFromPlan', () => { expect(result.testId).toBe(SAMPLE_RESPONSE.testId); }); + /** Creates from FE_PLAN while the dup-name listing (GET /tests) fails with 403. */ + async function createFromPlanWithFailingLookup(debug: boolean) { + const { credentialsPath } = makeCreds(); + let listAttempted = false; + const fetchImpl = makeFetch((url, init) => { + if ((init.method ?? 'GET') === 'GET' && url.includes('/tests')) { + listAttempted = true; + return { + status: 403, + body: { + error: { + code: 'AUTH_FORBIDDEN', + message: 'Forbidden', + nextAction: '', + requestId: 'r', + details: {}, + }, + }, + }; + } + return { body: SAMPLE_RESPONSE }; + }); + const stderrLines: string[] = []; + + const result = await runCreateFromPlan( + { profile: 'default', output: 'json', debug, planFrom: writePlanFile(FE_PLAN) }, + { + credentialsPath, + fetchImpl, + stdout: () => undefined, + stderr: line => stderrLines.push(line), + }, + ); + + return { result, stderrLines, listAttempted }; + } + + it('[finding-4] reports a failed dup-name lookup on the plan-from path under --debug', async () => { + const { result, stderrLines, listAttempted } = await createFromPlanWithFailingLookup(true); + + expect(listAttempted).toBe(true); + expect(result.testId).toBe(SAMPLE_RESPONSE.testId); + expect( + stderrLines.some(l => l.includes('[debug] duplicate-name advisory lookup failed: Forbidden')), + ).toBe(true); + }); + + it('[finding-4] keeps stderr free of [debug] lines when the plan-from lookup fails without --debug', async () => { + const { result, stderrLines, listAttempted } = await createFromPlanWithFailingLookup(false); + + // The lookup did fail and the create still went through; nothing is reported. + expect(listAttempted).toBe(true); + expect(result.testId).toBe(SAMPLE_RESPONSE.testId); + expect(stderrLines.filter(l => l.includes('[debug]'))).toEqual([]); + }); + it('[finding-4] skips dup-name lookup when dry-run is true', async () => { const { credentialsPath } = makeCreds(); let listAttempted = false;