/** * Unit tests for resolveDataDir, the OD_DATA_DIR path resolver. Covers the * $HOME / ${HOME} / ~/ shorthands that launchers can pass literally when no * shell is in the loop (#390), with both forward and backslash separators so * Windows launchers behave the same as Unix ones. * * Hermetic: every test runs against a fresh mkdtemp() home + projectRoot * pair, and os.homedir() is stubbed so resolveDataDir's writability check * never touches the developer's or CI runner's real home directory. */ import os from 'node:os'; import path from 'node:path'; import { mkdtempSync } from 'node:fs'; import { rm } from 'node:fs/promises'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { resolveDataDir } from '../src/server.js'; describe('resolveDataDir', () => { let fakeHome: string; let projectRoot: string; let homedirSpy: ReturnType; beforeEach(() => { fakeHome = mkdtempSync(path.join(os.tmpdir(), 'rdd-home-')); projectRoot = mkdtempSync(path.join(os.tmpdir(), 'rdd-project-')); homedirSpy = vi.spyOn(os, 'homedir').mockReturnValue(fakeHome); }); afterEach(async () => { homedirSpy.mockRestore(); await rm(fakeHome, { recursive: true, force: true }); await rm(projectRoot, { recursive: true, force: true }); }); it('returns /.od when OD_DATA_DIR is unset', () => { expect(resolveDataDir(undefined, projectRoot)).toBe(path.join(projectRoot, '.od')); expect(resolveDataDir('', projectRoot)).toBe(path.join(projectRoot, '.od')); }); it('requires an explicit OD_DATA_DIR when sandbox mode requires one', () => { expect(() => resolveDataDir(undefined, projectRoot, { requireExplicit: true }), ).toThrow('OD_DATA_DIR is required when OD_SANDBOX_MODE is enabled'); expect(() => resolveDataDir('', projectRoot, { requireExplicit: true })).toThrow( 'OD_DATA_DIR is required when OD_SANDBOX_MODE is enabled', ); expect(() => resolveDataDir(' ', projectRoot, { requireExplicit: true }), ).toThrow('OD_DATA_DIR is required when OD_SANDBOX_MODE is enabled'); }); it('trims OD_DATA_DIR before resolving the storage root', () => { const out = resolveDataDir(' rel-od ', projectRoot, { requireExplicit: true }); expect(out).toBe(path.join(projectRoot, 'rel-od')); }); it('expands a leading ~/ against the user home directory', () => { const out = resolveDataDir('~/od-test', projectRoot); expect(out).toBe(path.join(fakeHome, 'od-test')); }); it('expands ~\\ (backslash) against the user home directory', () => { const out = resolveDataDir('~\\od-test', projectRoot); expect(out).toBe(path.join(fakeHome, 'od-test')); }); it('expands a bare ~ to the user home directory', () => { expect(resolveDataDir('~', projectRoot)).toBe(fakeHome); }); it('expands $HOME/ against the user home directory', () => { const out = resolveDataDir('$HOME/od-test', projectRoot); expect(out).toBe(path.join(fakeHome, 'od-test')); }); it('expands $HOME\\ (backslash, Windows launcher) against the user home directory', () => { const out = resolveDataDir('$HOME\\od-test', projectRoot); expect(out).toBe(path.join(fakeHome, 'od-test')); }); it('expands ${HOME}/ against the user home directory', () => { const out = resolveDataDir('${HOME}/od-test', projectRoot); expect(out).toBe(path.join(fakeHome, 'od-test')); }); it('expands ${HOME}\\ (backslash) against the user home directory', () => { const out = resolveDataDir('${HOME}\\od-test', projectRoot); expect(out).toBe(path.join(fakeHome, 'od-test')); }); it('expands a bare $HOME to the user home directory', () => { expect(resolveDataDir('$HOME', projectRoot)).toBe(fakeHome); }); it('expands a bare ${HOME} to the user home directory', () => { expect(resolveDataDir('${HOME}', projectRoot)).toBe(fakeHome); }); it('passes absolute paths through unchanged', async () => { const abs = mkdtempSync(path.join(os.tmpdir(), 'rdd-abs-')); try { expect(resolveDataDir(abs, projectRoot)).toBe(abs); } finally { await rm(abs, { recursive: true, force: true }); } }); it('resolves relative paths against projectRoot', () => { const out = resolveDataDir('rel-od', projectRoot); expect(out).toBe(path.join(projectRoot, 'rel-od')); }); });