test: add freebsd-timezones and env-audit tests
54 new tests: pure timezone helpers (getTimezoneOptions, prioritizeTimezones, findTimezoneOption, isValidTimezone) with no mocks, plus auditEnvFile covering defaults, missing key detection, feature-flag warnings, GIT_MIRROR_URLS parsing, and value masking — using real temp files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- Build: FAIL | Tests: pass — Tests 1363 passed (1363)
This commit is contained in:
parent
01a501a045
commit
abe3be71fd
2 changed files with 433 additions and 0 deletions
271
setup/env-audit.test.ts
Normal file
271
setup/env-audit.test.ts
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
/**
|
||||
* setup/env-audit.test.ts — auditEnvFile unit tests.
|
||||
*
|
||||
* Tests defaults, missing keys, warnings, and feature-flag interactions.
|
||||
* Uses real temp files (simpler than mocking fs for a function that
|
||||
* reads env file content line-by-line).
|
||||
*
|
||||
* Run with: npx vitest run setup/env-audit.test.ts
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { vi } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock non-auditEnvFile deps (only used by run(), not by auditEnvFile)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
vi.mock('./status.js', () => ({ emitStatus: vi.fn() }));
|
||||
vi.mock('./platform.js', () => ({ getPlatform: vi.fn().mockReturnValue('freebsd') }));
|
||||
|
||||
import { auditEnvFile } from './env-audit.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let tmpDir: string;
|
||||
let envFile: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'env-audit-test-'));
|
||||
envFile = path.join(tmpDir, '.env');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function writeEnv(content: string): void {
|
||||
fs.writeFileSync(envFile, content, 'utf-8');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Missing env file
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('auditEnvFile — no env file', () => {
|
||||
it('uses defaults when file does not exist', () => {
|
||||
const result = auditEnvFile(path.join(tmpDir, 'nonexistent.env'));
|
||||
expect(result.values.AGENT_NAME).toBe('clawdie');
|
||||
expect(result.values.AGENT_SUBNET_BASE).toBe('10.0.0');
|
||||
});
|
||||
|
||||
it('ASSISTANT_NAME is in missing list when file is absent', () => {
|
||||
const result = auditEnvFile(path.join(tmpDir, 'nonexistent.env'));
|
||||
expect(result.missing).toContain('ASSISTANT_NAME');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Defaults
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('auditEnvFile — defaults', () => {
|
||||
it('uses "clawdie" as default AGENT_NAME', () => {
|
||||
writeEnv('');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.AGENT_NAME).toBe('clawdie');
|
||||
});
|
||||
|
||||
it('defaults AGENT_SUBNET_BASE to 10.0.0', () => {
|
||||
writeEnv('');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.AGENT_SUBNET_BASE).toBe('10.0.0');
|
||||
});
|
||||
|
||||
it('derives WARDEN_GATEWAY from subnet base', () => {
|
||||
writeEnv('AGENT_SUBNET_BASE=192.168.1\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.WARDEN_GATEWAY).toBe('192.168.1.1');
|
||||
});
|
||||
|
||||
it('defaults WARDEN_DB_IP to subnet.3', () => {
|
||||
writeEnv('AGENT_SUBNET_BASE=10.1.2\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.WARDEN_DB_IP).toBe('10.1.2.3');
|
||||
});
|
||||
|
||||
it('defaults CODE_HOSTING_MODE to "git"', () => {
|
||||
writeEnv('');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.CODE_HOSTING_MODE).toBe('git');
|
||||
});
|
||||
|
||||
it('defaults DB_RUNTIME to "jail"', () => {
|
||||
writeEnv('');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.DB_RUNTIME).toBe('jail');
|
||||
});
|
||||
|
||||
it('lowercases AGENT_NAME value', () => {
|
||||
writeEnv('AGENT_NAME=MyBot\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.AGENT_NAME).toBe('mybot');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Missing key detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('auditEnvFile — missing keys', () => {
|
||||
it('adds ASSISTANT_NAME to missing when absent', () => {
|
||||
writeEnv('AGENT_NAME=clawdie\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.missing).toContain('ASSISTANT_NAME');
|
||||
});
|
||||
|
||||
it('ASSISTANT_NAME is not missing when set', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.missing).not.toContain('ASSISTANT_NAME');
|
||||
});
|
||||
|
||||
it('returns empty missing array for a complete minimal env', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.missing).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Warnings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('auditEnvFile — warnings', () => {
|
||||
it('warns when DISPLAY_LOCALE is not set', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.warnings.some((w) => w.includes('DISPLAY_LOCALE'))).toBe(true);
|
||||
});
|
||||
|
||||
it('no DISPLAY_LOCALE warning when set', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\nDISPLAY_LOCALE=en-GB\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.warnings.some((w) => w.startsWith('DISPLAY_LOCALE') || w.includes('DISPLAY_LOCALE not set'))).toBe(false);
|
||||
});
|
||||
|
||||
it('warns when FEATURE_TAILSCALE=YES but TAILSCALE_AUTHKEY not set', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\nFEATURE_TAILSCALE=YES\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.warnings.some((w) => w.includes('TAILSCALE_AUTHKEY'))).toBe(true);
|
||||
});
|
||||
|
||||
it('no tailscale authkey warning when authkey is set', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\nFEATURE_TAILSCALE=YES\nTAILSCALE_AUTHKEY=tskey-abc\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.warnings.some((w) => w.includes('TAILSCALE_AUTHKEY'))).toBe(false);
|
||||
});
|
||||
|
||||
it('warns when FEATURE_GITEA=YES but FEATURE_GIT is not enabled', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\nFEATURE_GITEA=YES\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.warnings.some((w) => w.includes('FEATURE_GITEA'))).toBe(true);
|
||||
});
|
||||
|
||||
it('warns when LOCAL_LLM_PROVIDER=ollama but FEATURE_OLLAMA is not enabled', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\nLOCAL_LLM_PROVIDER=ollama\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.warnings.some((w) => w.includes('FEATURE_OLLAMA'))).toBe(true);
|
||||
});
|
||||
|
||||
it('warns when LOCAL_LLM_PROVIDER=llama_cpp but FEATURE_LLAMA_CPP is not enabled', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\nLOCAL_LLM_PROVIDER=llama_cpp\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.warnings.some((w) => w.includes('FEATURE_LLAMA_CPP'))).toBe(true);
|
||||
});
|
||||
|
||||
it('no LLM provider warning when provider=none', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\nLOCAL_LLM_PROVIDER=none\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.warnings.some((w) => w.includes('FEATURE_OLLAMA') || w.includes('FEATURE_LLAMA_CPP'))).toBe(false);
|
||||
});
|
||||
|
||||
it('warns when SSH_PUBLIC_KEY not set', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.warnings.some((w) => w.includes('SSH_PUBLIC_KEY'))).toBe(true);
|
||||
});
|
||||
|
||||
it('no SSH warning when SSH_PUBLIC_KEY is set', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\nSSH_PUBLIC_KEY=ssh-ed25519 AAAA...\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.warnings.some((w) => w.includes('SSH_PUBLIC_KEY'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Feature flags (isTruthyFlag)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('auditEnvFile — feature flags', () => {
|
||||
it('recognises YES as truthy', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\nFEATURE_GIT=YES\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.FEATURE_GIT).toBe('YES');
|
||||
});
|
||||
|
||||
it('recognises true as truthy', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\nFEATURE_GIT=true\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.FEATURE_GIT).toBe('YES');
|
||||
});
|
||||
|
||||
it('recognises 1 as truthy', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\nFEATURE_GIT=1\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.FEATURE_GIT).toBe('YES');
|
||||
});
|
||||
|
||||
it('treats NO as falsy for feature flags', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\nFEATURE_GIT=NO\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.FEATURE_GIT).toBe('NO');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GIT_MIRROR_URLS parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('auditEnvFile — GIT_MIRROR_URLS', () => {
|
||||
it('parses comma-separated mirror URLs', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\nGIT_MIRROR_URLS=https://a.com,https://b.com\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.GIT_MIRROR_URLS).toContain('https://a.com');
|
||||
expect(result.values.GIT_MIRROR_URLS).toContain('https://b.com');
|
||||
});
|
||||
|
||||
it('shows (unset) when GIT_MIRROR_URLS is empty', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.GIT_MIRROR_URLS).toBe('(unset)');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TAILSCALE_AUTHKEY masking
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('auditEnvFile — value masking', () => {
|
||||
it('shows "(set)" for TAILSCALE_AUTHKEY when present', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\nTAILSCALE_AUTHKEY=tskey-secret\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.TAILSCALE_AUTHKEY).toBe('(set)');
|
||||
});
|
||||
|
||||
it('shows "(unset)" for TAILSCALE_AUTHKEY when absent', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.TAILSCALE_AUTHKEY).toBe('(unset)');
|
||||
});
|
||||
|
||||
it('shows "(set)" for SSH_PUBLIC_KEY when present', () => {
|
||||
writeEnv('ASSISTANT_NAME=Clawdie\nSSH_PUBLIC_KEY=ssh-ed25519 AAAA...\n');
|
||||
const result = auditEnvFile(envFile);
|
||||
expect(result.values.SSH_PUBLIC_KEY).toBe('(set)');
|
||||
});
|
||||
});
|
||||
162
setup/freebsd-timezones.test.ts
Normal file
162
setup/freebsd-timezones.test.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
/**
|
||||
* setup/freebsd-timezones.test.ts — pure timezone helper tests.
|
||||
*
|
||||
* No mocks needed — all functions are pure.
|
||||
*
|
||||
* Run with: npx vitest run setup/freebsd-timezones.test.ts
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
getTimezoneOptions,
|
||||
formatTimezoneLabel,
|
||||
prioritizeTimezones,
|
||||
findTimezoneOption,
|
||||
isValidTimezone,
|
||||
} from './freebsd-timezones.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getTimezoneOptions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('getTimezoneOptions', () => {
|
||||
it('returns a non-empty array', () => {
|
||||
expect(getTimezoneOptions().length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('includes Europe/Ljubljana', () => {
|
||||
const opts = getTimezoneOptions();
|
||||
expect(opts.some((o) => o.timezone === 'Europe/Ljubljana')).toBe(true);
|
||||
});
|
||||
|
||||
it('includes UTC', () => {
|
||||
const opts = getTimezoneOptions();
|
||||
expect(opts.some((o) => o.timezone === 'UTC')).toBe(true);
|
||||
});
|
||||
|
||||
it('every entry has timezone, label, and region fields', () => {
|
||||
for (const opt of getTimezoneOptions()) {
|
||||
expect(typeof opt.timezone).toBe('string');
|
||||
expect(typeof opt.label).toBe('string');
|
||||
expect(typeof opt.region).toBe('string');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns the same reference each call (stable array)', () => {
|
||||
// Both calls should return equal content (not necessarily same reference)
|
||||
expect(getTimezoneOptions()).toEqual(getTimezoneOptions());
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// formatTimezoneLabel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('formatTimezoneLabel', () => {
|
||||
it('returns the label field of the option', () => {
|
||||
const opt = { timezone: 'UTC', label: 'UTC (Coordinated Universal Time)', region: 'Etc' };
|
||||
expect(formatTimezoneLabel(opt)).toBe('UTC (Coordinated Universal Time)');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// prioritizeTimezones
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('prioritizeTimezones', () => {
|
||||
const opts = getTimezoneOptions();
|
||||
|
||||
it('returns list unchanged when preferred is null', () => {
|
||||
const result = prioritizeTimezones(opts, null);
|
||||
expect(result).toEqual(opts);
|
||||
});
|
||||
|
||||
it('returns list unchanged when preferred is undefined', () => {
|
||||
const result = prioritizeTimezones(opts);
|
||||
expect(result).toEqual(opts);
|
||||
});
|
||||
|
||||
it('moves preferred timezone to first position', () => {
|
||||
const result = prioritizeTimezones(opts, 'UTC');
|
||||
expect(result[0].timezone).toBe('UTC');
|
||||
});
|
||||
|
||||
it('keeps preferred timezone in list (no duplication)', () => {
|
||||
const result = prioritizeTimezones(opts, 'UTC');
|
||||
const count = result.filter((o) => o.timezone === 'UTC').length;
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
it('preserves total length when preferred is found', () => {
|
||||
const result = prioritizeTimezones(opts, 'America/New_York');
|
||||
expect(result).toHaveLength(opts.length);
|
||||
});
|
||||
|
||||
it('preserves total length when preferred is not found', () => {
|
||||
const result = prioritizeTimezones(opts, 'Unknown/Timezone');
|
||||
expect(result).toHaveLength(opts.length);
|
||||
});
|
||||
|
||||
it('returns list unchanged when preferred is not in list', () => {
|
||||
const result = prioritizeTimezones(opts, 'Unknown/Timezone');
|
||||
expect(result).toEqual(opts);
|
||||
});
|
||||
|
||||
it('does not mutate the original array', () => {
|
||||
const original = [...opts];
|
||||
prioritizeTimezones(opts, 'UTC');
|
||||
expect(opts).toEqual(original);
|
||||
});
|
||||
|
||||
it('places Ljubljana first when preferred', () => {
|
||||
const result = prioritizeTimezones(opts, 'Europe/Ljubljana');
|
||||
expect(result[0].timezone).toBe('Europe/Ljubljana');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// findTimezoneOption
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('findTimezoneOption', () => {
|
||||
it('returns the option for a known timezone', () => {
|
||||
const result = findTimezoneOption('UTC');
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result?.timezone).toBe('UTC');
|
||||
});
|
||||
|
||||
it('returns undefined for an unknown timezone', () => {
|
||||
expect(findTimezoneOption('Mars/Olympus')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns the correct label for America/New_York', () => {
|
||||
const result = findTimezoneOption('America/New_York');
|
||||
expect(result?.label).toContain('New York');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isValidTimezone
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('isValidTimezone', () => {
|
||||
it('returns true for UTC', () => {
|
||||
expect(isValidTimezone('UTC')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for Europe/Ljubljana', () => {
|
||||
expect(isValidTimezone('Europe/Ljubljana')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for an unknown timezone string', () => {
|
||||
expect(isValidTimezone('Fake/Zone')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for empty string', () => {
|
||||
expect(isValidTimezone('')).toBe(false);
|
||||
});
|
||||
|
||||
it('is case-sensitive (lowercase fails)', () => {
|
||||
expect(isValidTimezone('utc')).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue