From 1ef4a6fa2bccf10de67f768030b5fa8000454064 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 04:13:35 +0000 Subject: [PATCH 1/4] test: the first start saves the default config into a data directory not there yet Without ./snapraid, loadAppConfig logs "Failed to save default config: ENOENT" and the defaults are not saved: the directory is made only later, by the log manager. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y3rdfbWb1xDUdPRng7TX9Z --- apps/server/src/__tests__/config.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/apps/server/src/__tests__/config.test.ts b/apps/server/src/__tests__/config.test.ts index c294bf2..d14f05d 100644 --- a/apps/server/src/__tests__/config.test.ts +++ b/apps/server/src/__tests__/config.test.ts @@ -1,4 +1,6 @@ import { expect, test } from "bun:test"; +import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { BASE_PATH, resolveFromBase, toStoredPath } from "../config.ts"; @@ -21,3 +23,25 @@ test("toStoredPath - files outside BASE_PATH are stored absolute", () => { expect(toStoredPath("/etc/snapraid.conf")).toEqual("/etc/snapraid.conf"); expect(toStoredPath("../outside.conf")).toEqual(resolveFromBase("../outside.conf")); }); + +// BASE_PATH is read once, when config.ts is imported: the first start runs in a process of its own +test("loadAppConfig - the first start creates the data directory and saves the default config", async () => { + const dir = await mkdtemp(join(tmpdir(), "snapraid-ui-first-start-")); + const base = join(dir, "snapraid"); + try { + const proc = Bun.spawn( + [process.execPath, "-e", `import { loadAppConfig } from ${JSON.stringify(join(import.meta.dir, "..", "config-parser.ts"))}; await loadAppConfig();`], + { env: { ...process.env, SNAPRAID_BASE_PATH: base }, stdout: "pipe", stderr: "pipe" }, + ); + const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]); + + expect(stderr).not.toContain("Failed to save default config"); + expect(exitCode).toBe(0); + // As the log directory and every other directory the backend makes: mkdir's default, less the umask + expect((await stat(base)).mode & 0o777).toBe(0o777 & ~process.umask()); + const saved = JSON.parse(await readFile(join(base, "config.json"), "utf8")); + expect(saved.snapraidConfigs[0].path).toBe("snapraid.conf"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); From c22800adc63ab79aaad77097bc10b5d2fb970b03 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 04:13:46 +0000 Subject: [PATCH 2/4] fix: create the data directory before saving the default config On the first start without login nothing had made SNAPRAID_BASE_PATH yet when loadAppConfig saved the defaults (only loadSessionSecret and the log manager mkdir it), so the save failed with ENOENT and config.json appeared only on the next start. saveAppConfig now makes the directory with mkdir's default mode, as the log manager does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y3rdfbWb1xDUdPRng7TX9Z --- apps/server/src/config-parser.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/server/src/config-parser.ts b/apps/server/src/config-parser.ts index 4ff3537..38ba62f 100644 --- a/apps/server/src/config-parser.ts +++ b/apps/server/src/config-parser.ts @@ -1,9 +1,9 @@ -import { basename, join } from "node:path"; +import { basename, dirname, join } from "node:path"; import type { AppConfig, ParityLevel, ParsedSnapRaidConfig } from "@shared/types.ts"; import { resolveFromBase } from "./config.ts"; import { effectiveSmartIgnore, parseSmartIgnoreLine } from "./smart-ignore.ts"; import { writeTextFileAtomic } from "./atomic-write.ts"; -import { readFile } from "node:fs/promises"; +import { mkdir, readFile } from "node:fs/promises"; import { isNotFound } from "./fs.ts"; export const MAX_PARITY_LEVEL = 6; @@ -174,5 +174,7 @@ export const loadAppConfig = async (): Promise => { */ export const saveAppConfig = async (config: AppConfig): Promise => { const configPath = resolveFromBase("config.json"); + // On the first start the data directory is not there yet; made as the log directory is + await mkdir(dirname(configPath), { recursive: true }); await writeTextFileAtomic(configPath, JSON.stringify(config, null, 2)); }; From 3a7a8dbde2e4437afb1cc672c6666f58faf1d5b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 04:17:28 +0000 Subject: [PATCH 3/4] test: wait for the toast of the job just finished, not an earlier one's expectFinished matched any "