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
190 lines
6.2 KiB
TypeScript
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);
|
|
});
|
|
});
|