From 8edd217f1ec90a87e46148769d7651606665b306 Mon Sep 17 00:00:00 2001 From: Theo Parker Date: Fri, 13 Nov 2026 00:15:03 -0800 Subject: [PATCH] daq backend: an explicit CONFIG_PATH is the only config it touches getConfigPath() fell back to the checkout's config/config.toml whenever CONFIG_PATH named a file not yet written. Backend tests point CONFIG_PATH at a fresh temp dir, so on any machine with a deployed config they read the real one and created profiles beside it: config/profiles/cold_flow/ and taken/ appeared in the checkout after npm test, and the calibration-profile tests failed on 'already exists' from the second run on. With CONFIG_PATH set, it is now the only candidate; the existing self-heal generates it from the profiles beside it. Unset, the lookup is unchanged. Production never sets CONFIG_PATH (the deploy scripts say so); the integration harness and replay_past_db.sh write their file first. config-path-explicit.test.ts puts a decoy where the fallback looks and fails on the old code. --- .../__tests__/config-path-explicit.test.ts | 50 +++++++++++++++++++ .../backend/src/routes/config.ts | 19 ++++--- 2 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 daq-server/diablo_server/backend/src/__tests__/config-path-explicit.test.ts diff --git a/daq-server/diablo_server/backend/src/__tests__/config-path-explicit.test.ts b/daq-server/diablo_server/backend/src/__tests__/config-path-explicit.test.ts new file mode 100644 index 00000000..e16c8f52 --- /dev/null +++ b/daq-server/diablo_server/backend/src/__tests__/config-path-explicit.test.ts @@ -0,0 +1,50 @@ +/** + * An explicit CONFIG_PATH is the only config the backend touches, even before the file exists. + * + * The backend used to fall back to the checkout's own config/config.toml whenever CONFIG_PATH + * named a file not yet written. A test that pointed CONFIG_PATH at a fresh temp directory then + * read, and created profiles beside, the developer's real config: `config/profiles/cold_flow/` + * and `taken/` appeared in the checkout after `npm test` on any machine with a deployed config. + * + * Here a decoy config sits where the fallback would look (cwd/../../config/config.toml); the + * named one is missing but has a profile to generate from. The backend must generate the named + * one and leave the decoy alone. + */ +import { describe, it, expect, afterAll } from 'vitest'; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, realpathSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; + +// realpath: on macOS the temp dir is a symlink, and cwd comes back resolved. +const root = realpathSync(mkdtempSync(join(tmpdir(), 'daq-cfgpath-'))); +const decoy = join(root, 'config', 'config.toml'); +const cwd = join(root, 'a', 'b'); +const named = join(root, 'isolated', 'config', 'config.toml'); + +mkdirSync(join(root, 'config'), { recursive: true }); +writeFileSync(decoy, '# decoy: the checkout config a test must never touch\n'); +mkdirSync(cwd, { recursive: true }); +mkdirSync(join(root, 'isolated', 'config', 'profiles', 'default'), { recursive: true }); +writeFileSync( + join(root, 'isolated', 'config', 'profiles', 'default', 'config.toml'), + '[network]\nactuator_cmd_port = 5005\n', +); + +const before = process.cwd(); +process.chdir(cwd); +process.env.CONFIG_PATH = named; +const { getConfigPath } = await import('../routes/config.js'); + +afterAll(() => { + process.chdir(before); + rmSync(root, { recursive: true, force: true }); +}); + +describe('getConfigPath with CONFIG_PATH set', () => { + it('generates the named config and never falls back to the checkout one', () => { + const got = getConfigPath(); + expect(got, 'fell back to the checkout config').not.toBe(decoy); + expect(got).toBe(named); + expect(readFileSync(named, 'utf-8')).toContain('actuator_cmd_port = 5005'); + }); +}); diff --git a/daq-server/diablo_server/backend/src/routes/config.ts b/daq-server/diablo_server/backend/src/routes/config.ts index fca918ce..7374e7c9 100644 --- a/daq-server/diablo_server/backend/src/routes/config.ts +++ b/daq-server/diablo_server/backend/src/routes/config.ts @@ -18,13 +18,18 @@ const CONFIG_PATH = process.env.CONFIG_PATH || join(process.cwd(), '..', 'config', 'config.toml'); export function getConfigPath(): string { - // Try multiple possible paths - const possiblePaths = [ - CONFIG_PATH, - join(process.cwd(), '..', '..', 'config', 'config.toml'), - // __dirname resolves relative to the compiled JS — walk up to project root - join(__dirname, '..', '..', '..', '..', 'config', 'config.toml'), - ]; + // An explicit CONFIG_PATH is the only candidate, even before its file exists: the self-heal + // below generates it from the profiles beside it. Falling back to the checkout's config here is + // how tests pointing CONFIG_PATH at a fresh temp dir wrote profiles into a developer's real + // config/. Without it, try the usual places. + const possiblePaths = process.env.CONFIG_PATH + ? [CONFIG_PATH] + : [ + CONFIG_PATH, + join(process.cwd(), '..', '..', 'config', 'config.toml'), + // __dirname resolves relative to the compiled JS — walk up to project root + join(__dirname, '..', '..', '..', '..', 'config', 'config.toml'), + ]; for (const path of possiblePaths) { try {