import { EventBus } from "@suaveplan/events"; import { describe, expect, it } from "@suaveplan/testing/runner"; import { createConfig } from "../src/core/config-builder/config-builder.js"; import { ConfigError } from "../src/error-codes/error-codes.js"; import { cliLayer } from "../src/layers/cli-layer/cli-layer.js"; import { defaultsLayer } from "../src/layers/defaults-layer/defaults-layer.js"; import { envLayer } from "../src/layers/env-layer/env-layer.js"; import { fileLayer } from "../src/layers/file-layer/file-layer.js"; import { objectLayer } from "../src/layers/object-layer/object-layer.js"; describe("createConfig integration", () => { it("full 5-layer pipeline produces correct merged config", { timeout: 5000, }, () => { const store = createConfig({ layers: [ defaultsLayer({ server: { port: 3000, host: "0.0.0.0" }, db: { pool: 5, timeout: 30_000, password: "defaultpw" }, log: { level: "info", pretty: true }, }), fileLayer({ content: { server: { port: 3100 }, db: { pool: 10 }, }, }), objectLayer({ server: { port: 3200 }, }), envLayer({ mapping: { PORT: "server.port", DB_POOL: "db.pool" }, source: { PORT: "3300", DB_POOL: "15" }, }), cliLayer({ args: ["--port=3400", "--debug"], mapping: { port: "server.port", debug: "log.debug" }, }), ], sensitive: ["db.password"], }); // CLI (priority 40) wins for port expect(store.get("server.port")).toBe(3400); // ENV (priority 30) wins for db.pool over file/object/defaults expect(store.get("db.pool")).toBe(15); // CLI added debug flag expect(store.get("log.debug")).toBe(true); // Defaults survive for host expect(store.get("server.host")).toBe("0.0.0.0"); // File layer contributed timeout (no higher layer overrode it) expect(store.get("db.timeout")).toBe(30_000); // sensitive masking const masked = store.toMasked() as Record< string, Record >; expect(masked.db.password).toBe("****"); expect(store.get("db.password")).toBe("defaultpw"); }); it("has() returns true/false correctly", { timeout: 5000 }, () => { const store = createConfig({ layers: [defaultsLayer({ port: 3000 })], }); expect(store.has("port")).toBe(true); expect(store.has("missing")).toBe(false); }); it("toJSON returns full frozen config", { timeout: 5000 }, () => { const store = createConfig({ layers: [defaultsLayer({ port: 3000 })], }); const json = store.toJSON(); expect(json).toEqual({ port: 3000 }); expect(() => { (json as Record).port = 9999; }).toThrow(); }); it("reload with validation throws when validator rejects", { timeout: 30000, }, async () => { let pass = true; const dynamic = { name: "dynamic", priority: 0, resolve: () => ({ port: pass ? 3000 : "bad" }), }; const store = createConfig({ layers: [dynamic], validate: (d) => { if (typeof (d as Record).port !== "number") { throw new Error("port must be number"); } return d; }, }); pass = false; await expect(store.reload()).rejects.toBeInstanceOf(ConfigError); }); it("watch-mode: fileLayer with EventBus triggers reload", { timeout: 30000, }, async () => { type BusMap = { "config:file:changed": Record }; const bus = new EventBus(); let fileContent = { port: 3000 }; const fLayer = fileLayer({ get content() { return fileContent; }, watchBus: bus, }); const store = createConfig({ layers: [fLayer], }); expect(store.get("port")).toBe(3000); // Simulate file change fileContent = { port: 4000 }; await bus.emit("config:file:changed", {}); // Give the async reload a tick to complete await new Promise((resolve) => setTimeout(resolve, 50)); expect(store.get("port")).toBe(4000); }); it("environment override applies on reload", { timeout: 30000, }, async () => { let counter = 0; const store = createConfig({ layers: [ { name: "dynamic", priority: 0, resolve: () => ({ count: ++counter, debug: false }), }, ], environments: { test: { debug: true } }, environment: "test", }); expect(store.get("debug")).toBe(true); await store.reload(); expect(store.get("debug")).toBe(true); }); });