diff --git a/src/e2e/e2e-permissions.spec.ts b/src/e2e/e2e-permissions.spec.ts index e23694721..ab6a3f89b 100644 --- a/src/e2e/e2e-permissions.spec.ts +++ b/src/e2e/e2e-permissions.spec.ts @@ -119,6 +119,29 @@ describe("E2E: permissions", () => { }, ); + it.each([{ features: "permissions" }, { features: "mcp" }])( + "should fail $features generation on a malformed .vibe/config.toml instead of reporting success", + async ({ features }) => { + const testDir = getTestDir(); + + await writeFileContent(join(testDir, ".vibe", "config.toml"), "mcp: [unclosed"); + await writeFileContent( + join(testDir, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH), + JSON.stringify({ permission: { bash: { "*": "allow" } } }), + ); + await writeFileContent( + join(testDir, ".rulesync", "mcp.json"), + JSON.stringify({ mcpServers: { srv: { command: "node" } } }), + ); + + // Both features read the same shared file and must agree on what a + // malformed file means: fail the run, never a silent "up to date". + await expect(runGenerate({ target: "vibe", features })).rejects.toMatchObject({ + code: 1, + }); + }, + ); + it("should generate rovodev permissions into the repo-committed .rovodev/config.yml", async () => { const testDir = getTestDir(); diff --git a/src/lib/generate.test.ts b/src/lib/generate.test.ts index cf7e4960f..138ca36ef 100644 --- a/src/lib/generate.test.ts +++ b/src/lib/generate.test.ts @@ -662,15 +662,17 @@ describe("generate", () => { ); }); - it("should handle errors gracefully and continue", async () => { + it("should fail the run on a permissions error instead of reporting success", async () => { + // A malformed shared config used to be swallowed with a warning while + // the run exited 0 "up to date" — the MCP feature failed on the same + // file. Both features must agree that a broken file fails the run. mockConfig.getFeatures.mockReturnValue(["permissions"]); vi.mocked(PermissionsProcessor).mockImplementation(function () { throw new Error("Test error"); }); - const result = await generate({ logger, config: mockConfig as never }); - - expect(result.permissionsCount).toBe(0); + await expect(generate({ logger, config: mockConfig as never })).rejects.toThrow("Test error"); + expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("Test error")); }); }); diff --git a/src/lib/generate.ts b/src/lib/generate.ts index 1ba69710c..1b6307e1c 100644 --- a/src/lib/generate.ts +++ b/src/lib/generate.ts @@ -1134,10 +1134,14 @@ async function generatePermissionsCore(params: { allPaths.push(...result.paths); if (result.hasDiff) hasDiff = true; } catch (error) { - logger.warn( + // A malformed shared config (e.g. .vibe/config.toml) must fail the run + // the same way the MCP feature does — swallowing it reported + // "All files are up to date" while the user's permission changes were + // silently not applied. + logger.error( `Failed to generate ${toolTarget} permissions files for ${outputRoot}: ${formatError(error)}`, ); - continue; + throw error; } } }