diff --git a/src/cli/commands/generate.test.ts b/src/cli/commands/generate.test.ts index 83a23ec16..0a00142c9 100644 --- a/src/cli/commands/generate.test.ts +++ b/src/cli/commands/generate.test.ts @@ -446,17 +446,17 @@ describe("generateCommand", () => { mockCwd.mockRestore(); }); - it("should handle errors in ignore processing gracefully", async () => { + it("should fail the command on an ignore error instead of reporting success", async () => { vi.mocked(IgnoreProcessor).mockImplementation(function () { throw new Error("Test error"); }); const options: GenerateOptions = {}; - // Should not throw, errors are caught and processing continues - await generateCommand(mockLogger, options); - - // Should still complete without error - expect(mockLogger.info).toHaveBeenCalledWith("✓ All files are up to date (ignore)"); + // A swallowed ignore error used to end with "All files are up to date" + // while nothing was written — the fail-open direction for the feature + // that keeps secrets away from AI tools (see #2551). + await expect(generateCommand(mockLogger, options)).rejects.toThrow("Test error"); + expect(mockLogger.info).not.toHaveBeenCalledWith("✓ All files are up to date (ignore)"); }); it("should skip ignore files when no rulesync files found", async () => { @@ -1225,17 +1225,18 @@ describe("generateCommand", () => { return mockRulesProcessor as any; }); - // Set up ignore processor to throw an error (errors are caught and ignored in lib) + // Set up ignore processor to throw an error — a feature error now + // fails the whole run instead of being silently ignored (#2551). + // Ignore is the first step in GENERATION_STEP_GRAPH, so the rules step + // never runs and no success banner is emitted. vi.mocked(IgnoreProcessor).mockImplementation(function () { throw new Error("Ignore error"); }); const options: GenerateOptions = {}; - await generateCommand(mockLogger, options); - - expect(mockLogger.success).toHaveBeenCalledWith("Written 2 rules"); - expect(mockLogger.success).toHaveBeenCalledWith( + await expect(generateCommand(mockLogger, options)).rejects.toThrow("Ignore error"); + expect(mockLogger.success).not.toHaveBeenCalledWith( "🎉 All done! Written 2 file(s) total (2 rules)", ); }); diff --git a/src/lib/generate.test.ts b/src/lib/generate.test.ts index 138ca36ef..0b8fe254d 100644 --- a/src/lib/generate.test.ts +++ b/src/lib/generate.test.ts @@ -385,15 +385,17 @@ describe("generate", () => { ); }); - it("should handle errors gracefully and continue", async () => { + it("should fail the run on an ignore error instead of reporting success", async () => { + // Ignore files keep secrets out of AI tools' reach; a swallowed error + // used to exit 0 "up to date" while nothing was written (see #2486 for + // the permissions counterpart). mockConfig.getFeatures.mockReturnValue(["ignore"]); vi.mocked(IgnoreProcessor).mockImplementation(function () { throw new Error("Test error"); }); - const result = await generate({ logger, config: mockConfig as never }); - - expect(result.ignoreCount).toBe(0); + await expect(generate({ logger, config: mockConfig as never })).rejects.toThrow("Test error"); + expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("Test error")); }); it("should skip writing when no rulesync files found", async () => { diff --git a/src/lib/generate.ts b/src/lib/generate.ts index 1b6307e1c..d34ac4d29 100644 --- a/src/lib/generate.ts +++ b/src/lib/generate.ts @@ -789,10 +789,13 @@ async function generateIgnoreCore(params: { allPaths.push(...result.paths); if (result.hasDiff) hasDiff = true; } catch (error) { - logger.warn( + // Ignore files are what keep secrets out of AI tools' reach — a + // silently-skipped ignore generation is the same fail-open bug the + // permissions feature had (#2486), so it fails the run the same way. + logger.error( `Failed to generate ${toolTarget} ignore files for ${outputRoot}: ${formatError(error)}`, ); - continue; + throw error; } } }