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)
271 lines
10 KiB
TypeScript
271 lines
10 KiB
TypeScript
/**
|
|
* 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)');
|
|
});
|
|
});
|