Files
Codeman/test/config/instance.test.ts
T
arkonandClaude Opus 4.8 cf6fabc070 fix(web): address self-review findings on #103 (master-safe defaults + hardening)
Make the branch genuinely master-mergeable and fix several review findings:

- Defaults are now prod-safe: CODEMAN_INSTANCE defaults to '' (→ ~/.codeman,
  -L codeman) and the web port back to 3000, so an existing install upgrades
  cleanly. Port also honors a new CODEMAN_PORT env var. Run the beta isolated
  alongside prod with scripts/run-beta.sh (CODEMAN_INSTANCE=beta + PORT 5000).
- .gitignore: anchor the root `public` symlink rule to `/public` (a bare
  `public` also swallowed src/web/public, silently un-staging new web assets);
  ignore the gesture wasm/model binaries explicitly instead.
- span-displays: add a macOS-only guard (400 elsewhere instead of spawning a
  bash that fails invisibly); extract resolveSpanUrl() for unit testing.
- server.ts: memoize asset-version stat() calls (~1s TTL) so each index render
  doesn't re-stat every script/link tag.
- styles.css: hide the multi-monitor button in solo (detached) windows.
- app.js: require two consecutive unanswered roll-calls before redocking, so a
  timer-throttled background popup isn't wrongly un-marked.
- index.html: make the "skip to terminal" link base-href-safe (onclick scroll)
  so it doesn't navigate to the dashboard from a /session/:id window.
- Tests: test/config/instance.test.ts, test/routes/system-span-displays.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:41:46 +02:00

80 lines
3.3 KiB
TypeScript

/**
* Per-instance isolation (src/config/instance.ts): the data dir + tmux socket
* derive from CODEMAN_INSTANCE, defaulting to the production layout so the
* feature branch is safe to merge to master.
*
* instance.ts reads env at module load, so each case re-imports it via
* vi.resetModules() under a controlled env. node:fs mkdirSync is mocked so
* getDataDir() never creates real directories on the test machine.
*
* Port: N/A (no server).
*/
import { describe, it, expect, afterEach, vi } from 'vitest';
import { homedir } from 'node:os';
import { join } from 'node:path';
vi.mock('node:fs', async (orig) => {
const actual = await orig<typeof import('node:fs')>();
return { ...actual, mkdirSync: vi.fn() };
});
const ENV_KEYS = ['CODEMAN_INSTANCE', 'CODEMAN_DATA_DIR'] as const;
const ORIG: Record<string, string | undefined> = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
async function load(env: Partial<Record<(typeof ENV_KEYS)[number], string | undefined>> = {}) {
vi.resetModules();
for (const k of ENV_KEYS) {
const v = env[k];
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
return import('../../src/config/instance.js');
}
afterEach(() => {
for (const k of ENV_KEYS) {
if (ORIG[k] === undefined) delete process.env[k];
else process.env[k] = ORIG[k];
}
vi.resetModules();
});
describe('config/instance', () => {
it('defaults to the production layout when CODEMAN_INSTANCE is unset', async () => {
const m = await load({ CODEMAN_INSTANCE: undefined, CODEMAN_DATA_DIR: undefined });
expect(m.CODEMAN_INSTANCE).toBe('');
expect(m.DEFAULT_TMUX_SOCKET).toBe('codeman');
expect(m.getDataDir()).toBe(join(homedir(), '.codeman'));
expect(m.dataPath('state.json')).toBe(join(homedir(), '.codeman', 'state.json'));
});
it('treats an explicitly-empty CODEMAN_INSTANCE as the production layout', async () => {
const m = await load({ CODEMAN_INSTANCE: '', CODEMAN_DATA_DIR: undefined });
expect(m.CODEMAN_INSTANCE).toBe('');
expect(m.DEFAULT_TMUX_SOCKET).toBe('codeman');
expect(m.getDataDir()).toBe(join(homedir(), '.codeman'));
});
it('scopes BOTH the data dir and the tmux socket for a named instance', async () => {
const m = await load({ CODEMAN_INSTANCE: 'beta', CODEMAN_DATA_DIR: undefined });
expect(m.CODEMAN_INSTANCE).toBe('beta');
expect(m.DEFAULT_TMUX_SOCKET).toBe('codeman-beta');
expect(m.getDataDir()).toBe(join(homedir(), '.codeman-beta'));
expect(m.dataPath('mux-sessions.json')).toBe(join(homedir(), '.codeman-beta', 'mux-sessions.json'));
});
it('supports an arbitrary instance name', async () => {
const m = await load({ CODEMAN_INSTANCE: 'foo', CODEMAN_DATA_DIR: undefined });
expect(m.DEFAULT_TMUX_SOCKET).toBe('codeman-foo');
expect(m.getDataDir()).toBe(join(homedir(), '.codeman-foo'));
});
it('CODEMAN_DATA_DIR overrides the derived data dir (socket still instance-scoped)', async () => {
const m = await load({ CODEMAN_INSTANCE: 'beta', CODEMAN_DATA_DIR: '/tmp/codeman-test-xyz' });
expect(m.getDataDir()).toBe('/tmp/codeman-test-xyz');
expect(m.dataPath('a', 'b')).toBe(join('/tmp/codeman-test-xyz', 'a', 'b'));
// Socket is derived from the instance name, not the data dir override.
expect(m.DEFAULT_TMUX_SOCKET).toBe('codeman-beta');
});
});