clawdie-ai/setup/hosts.test.ts
Operator & Claude Code 0fcac57e42 Use RUNTIME_ID for setup-side label interpolation
Follow-up to a99f971: covers the remaining ${TENANT_ID} interpolation
sites that produced leading-hyphen / empty-path values on root installs.

- setup/ollama.ts, setup/llama-cpp.ts: preferred jail names
- setup/sanoid.ts: tenant-era home candidate
- setup/hosts.ts: jail-name discovery filter (+ test mock)
- src/telegram-commands.ts: status identity line, suppress empty
  tenant clause on root installs

Root-detection sites that key off TENANT_ID === '' are intentionally
left untouched; the invariant is preserved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---
Build: FAIL | Tests: FAIL — 15 failed
2026-05-04 06:31:21 +02:00

190 lines
6.2 KiB
TypeScript

/**
* setup/hosts.test.ts — syncLocalHosts() unit tests.
*
* Tests that /etc/hosts is always synced and that jail hosts
* are synced/skipped based on whether bastille jail dirs exist.
*
* Run with: npx vitest run setup/hosts.test.ts
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
// ---------------------------------------------------------------------------
// Mocks
// ---------------------------------------------------------------------------
vi.mock('../src/config.js', () => ({
RUNTIME_ID: 'clawdie',
AGENT_INTERNAL_DOMAIN: 'clawdie.home.arpa',
}));
vi.mock('../src/logger.js', () => ({
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
}));
vi.mock('./platform.js', () => ({
getPlatform: vi.fn().mockReturnValue('freebsd'),
}));
vi.mock('./status.js', () => ({ emitStatus: vi.fn() }));
vi.mock('../src/local-hosts.js', () => ({
renderLocalHostsBlock: vi.fn().mockReturnValue('# managed block\n'),
upsertLocalHostsBlock: vi
.fn()
.mockImplementation((content: string) => content + '\n# updated'),
}));
const fsMocks = vi.hoisted(() => ({
existsSync: vi.fn<[string], boolean>(() => false),
readFileSync: vi.fn<[string, string], string>(() => ''),
writeFileSync: vi.fn(),
mkdirSync: vi.fn(),
readdirSync: vi.fn<
[string, object],
Array<{ name: string; isDirectory: () => boolean }>
>(() => []),
}));
vi.mock('fs', async () => {
const actual = await vi.importActual<typeof import('fs')>('fs');
const mocked = { ...actual, ...fsMocks };
return { ...mocked, default: mocked };
});
import { syncLocalHosts } from './hosts.js';
const HOSTS_FILE = '/etc/hosts';
const BASTILLE_ROOT = '/usr/local/bastille';
// ---------------------------------------------------------------------------
// syncLocalHosts
// ---------------------------------------------------------------------------
describe('syncLocalHosts — no bastille jails', () => {
beforeEach(() => {
fsMocks.existsSync.mockReset().mockReturnValue(false);
fsMocks.readFileSync.mockReset().mockReturnValue('');
fsMocks.writeFileSync.mockReset();
fsMocks.readdirSync.mockReset().mockReturnValue([]);
});
it('always writes /etc/hosts', () => {
const result = syncLocalHosts();
expect(fsMocks.writeFileSync).toHaveBeenCalled();
const hostsWrite = fsMocks.writeFileSync.mock.calls.find(
(c) => c[0] === HOSTS_FILE,
);
expect(hostsWrite).toBeDefined();
});
it('returns HOSTS_FILE as hostFile', () => {
const result = syncLocalHosts();
expect(result.hostFile).toBe(HOSTS_FILE);
});
it('includes HOSTS_FILE in syncedTargets', () => {
const result = syncLocalHosts();
expect(result.syncedTargets).toContain(HOSTS_FILE);
});
it('returns empty skippedTargets when no jails dir', () => {
fsMocks.existsSync.mockImplementation((p: string) => p === HOSTS_FILE);
const result = syncLocalHosts();
expect(result.skippedTargets).toHaveLength(0);
});
});
describe('syncLocalHosts — with bastille jails', () => {
const JAILS_ROOT = `${BASTILLE_ROOT}/jails`;
beforeEach(() => {
fsMocks.existsSync.mockReset();
fsMocks.readFileSync.mockReset().mockReturnValue('127.0.0.1 localhost\n');
fsMocks.writeFileSync.mockReset();
fsMocks.readdirSync.mockReset();
});
it('syncs a jail when its /etc/hosts directory exists', () => {
fsMocks.readdirSync.mockImplementation((p: string) => {
if ((p as string) === JAILS_ROOT) {
return [{ name: 'clawdie-db', isDirectory: () => true }] as never;
}
return [] as never;
});
fsMocks.existsSync.mockImplementation((p: string) => {
// jails root exists + jail's /etc dir exists
if ((p as string) === JAILS_ROOT) return true;
return (p as string).includes('clawdie-db/root/etc');
});
const result = syncLocalHosts();
expect(result.syncedTargets).toContain('clawdie-db');
});
it('syncs a normalized underscore-form jail name', () => {
fsMocks.readdirSync.mockImplementation((p: string) => {
if ((p as string) === JAILS_ROOT) {
return [{ name: 'clawdie_db', isDirectory: () => true }] as never;
}
return [] as never;
});
fsMocks.existsSync.mockImplementation((p: string) => {
if ((p as string) === JAILS_ROOT) return true;
return (p as string).includes('clawdie_db/root/etc');
});
const result = syncLocalHosts();
expect(result.syncedTargets).toContain('clawdie_db');
});
it('skips a jail when its /etc/hosts directory does not exist', () => {
fsMocks.readdirSync.mockImplementation((p: string) => {
if ((p as string) === JAILS_ROOT) {
return [{ name: 'clawdie-db', isDirectory: () => true }] as never;
}
return [] as never;
});
fsMocks.existsSync.mockImplementation(
(p: string) => (p as string) === JAILS_ROOT,
);
const result = syncLocalHosts();
expect(result.skippedTargets).toContain('clawdie-db');
expect(result.syncedTargets).not.toContain('clawdie-db');
});
it('only syncs tenant-owned jail names', () => {
fsMocks.readdirSync.mockImplementation((p: string) => {
if ((p as string) === JAILS_ROOT) {
return [
{ name: 'clawdie-db', isDirectory: () => true },
{ name: 'clawdie_db', isDirectory: () => true },
{ name: 'other-jail', isDirectory: () => true },
] as never;
}
return [] as never;
});
fsMocks.existsSync.mockImplementation((p: string) => {
if ((p as string) === JAILS_ROOT) return true;
return (
(p as string).includes('clawdie-db/root/etc') ||
(p as string).includes('clawdie_db/root/etc') ||
(p as string).includes('other-jail/root/etc')
);
});
const result = syncLocalHosts();
// 'other-jail' should not appear in results at all (not agent-owned)
expect(result.syncedTargets.concat(result.skippedTargets)).not.toContain(
'other-jail',
);
expect(result.syncedTargets).toContain('clawdie-db');
expect(result.syncedTargets).toContain('clawdie_db');
});
it('returns HOSTS_FILE always in syncedTargets even with jails', () => {
fsMocks.readdirSync.mockReturnValue([]);
const result = syncLocalHosts();
expect(result.syncedTargets).toContain(HOSTS_FILE);
});
});