1import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from "node:fs";
3import { tmpdir } from "node:os";
4import { join } from "node:path";
5import { ConfigError } from "../src/util/errors.js";
6import { loadConfig } from "../src/config/loader.js";
7
8describe("loadConfig - ConfigError rethrown directly (issue #10)", () => {
9 let configDir: string;
10 let configPath: string;
11 const originalEnv = { ...process.env };
12
13 beforeEach(() => {
14 configDir = mkdtempSync(join(tmpdir(), "rumilo-cfg-test10-"));
15 const xdgBase = join(configDir, "xdg");
16 const rumiloDir = join(xdgBase, "rumilo");
17 mkdirSync(rumiloDir, { recursive: true });
18 configPath = join(rumiloDir, "config.toml");
19 process.env["XDG_CONFIG_HOME"] = xdgBase;
20 });
21
22 afterEach(() => {
23 process.env = { ...originalEnv };
24 try {
25 rmSync(configDir, { recursive: true, force: true });
26 } catch {}
27 });
28
29 test("ConfigError from validation is rethrown with original message and stack", async () => {
30 // Write invalid config that triggers ConfigError from validatePartialConfig
31 writeFileSync(configPath, `[defaults]\nmodel = 42\n`);
32 try {
33 await loadConfig();
34 throw new Error("should have thrown");
35 } catch (e: any) {
36 expect(e).toBeInstanceOf(ConfigError);
37 // The original message should include the validation details, not be re-wrapped
38 expect(e.message).toContain("/defaults/model");
39 // Stack should reference the validation function, not be a generic re-wrap
40 expect(e.stack).toBeDefined();
41 }
42 });
43
44 test("TOML parse error is wrapped as ConfigError with original message", async () => {
45 writeFileSync(configPath, `[invalid toml !!!`);
46 try {
47 await loadConfig();
48 throw new Error("should have thrown");
49 } catch (e: any) {
50 expect(e).toBeInstanceOf(ConfigError);
51 // Should contain the original TOML parse error message
52 expect(e.message.length).toBeGreaterThan(0);
53 }
54 });
55});