clawdie-ai/setup/first-boot.test.ts
Operator & Codex 8145830ebf Align first boot provider setup with Codex recommendation
---
Build: pass | Tests: pass — 2441 passed (181 files)
2026-05-12 09:56:02 +02:00

369 lines
10 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
FIRST_BOOT_SETUP_SCHEMA_VERSION,
PLAINTEXT_CREDENTIAL_WARNING,
deriveFirstBootIdentity,
deriveFirstBootStorageLayout,
parseFirstBootConfig,
profileToRuntimeEnv,
} from './first-boot.js';
describe('first boot config parser', () => {
it('parses optional provider and telegram fields when present', () => {
const result = parseFirstBootConfig(`
OPENROUTER_API_KEY=sk-or-v1-test
TELEGRAM_BOT_TOKEN=123:abc
TELEGRAM_ADMIN_ID=85126311
`);
expect(result).toMatchObject({
setupSchemaVersion: FIRST_BOOT_SETUP_SCHEMA_VERSION,
isoRelease: 'v0.10.0',
isoGitCommit: null,
providerApiKeys: {
OPENROUTER_API_KEY: 'sk-or-v1-test',
},
openRouterApiKey: 'sk-or-v1-test',
telegramBotToken: '123:abc',
telegramAdminId: 85126311,
needsPostInstallProviderSetup: false,
needsPostInstallTelegramSetup: false,
installMode: 'auto',
assistantName: 'Clawdie',
profile: 'balanced',
timeZone: 'UTC',
hostname: 'clawdie',
zfsPool: 'zroot',
zfsLayout: 'single',
zfsDataDisks: 1,
zfsHotSpares: 0,
zfsPrefix: 'clawdie-runtime',
operatorEmail: null,
operatorPassword: null,
sshAuthorizedKey: null,
clawdieUserPassword: null,
plaintextCredentialWarning: null,
});
});
it('accepts absent provider and telegram fields and marks post-install setup as needed', () => {
const result = parseFirstBootConfig(`
ASSISTANT_NAME=Alpha
TIMEZONE=Europe/Ljubljana
`);
expect(result).toMatchObject({
providerApiKeys: {},
openRouterApiKey: null,
telegramBotToken: null,
telegramAdminId: null,
needsPostInstallProviderSetup: true,
needsPostInstallTelegramSetup: true,
assistantName: 'Alpha',
timeZone: 'Europe/Ljubljana',
});
});
it('supports comments, quotes, invisible chars, and explicit values', () => {
const result = parseFirstBootConfig(`
# comment
SETUP_SCHEMA_VERSION=1
ISO_RELEASE=v9.9.9
ISO_GIT_COMMIT=abcdef12
OPENROUTER_API_KEY="sk-or-v1-test"
TELEGRAM_BOT_TOKEN='123:abc'
TELEGRAM_ADMIN_ID=85126311
ASSISTANT_NAME=\u200bAlpha
PROFILE=quality
TIMEZONE=Europe/Ljubljana
HOSTNAME=osa-box
ZFS_POOL=tank
ZFS_LAYOUT=raidz1
ZFS_DATA_DISKS=3
ZFS_HOT_SPARES=1
ZFS_PREFIX=alpha-data
OPERATOR_EMAIL=operator@example.com
OPERATOR_PASSWORD=supersecret
SSH_AUTHORIZED_KEY=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG9wZXJhdG9yS2V5comment operator@example
CLAWDIE_USER_PASSWORD=sudofallback
`);
expect(result).toMatchObject({
setupSchemaVersion: 1,
isoRelease: 'v9.9.9',
isoGitCommit: 'abcdef12',
providerApiKeys: {
OPENROUTER_API_KEY: 'sk-or-v1-test',
},
openRouterApiKey: 'sk-or-v1-test',
telegramBotToken: '123:abc',
telegramAdminId: 85126311,
needsPostInstallProviderSetup: false,
needsPostInstallTelegramSetup: false,
assistantName: 'Alpha',
installMode: 'auto',
profile: 'quality',
timeZone: 'Europe/Ljubljana',
hostname: 'osa-box',
zfsPool: 'tank',
zfsLayout: 'raidz1',
zfsDataDisks: 3,
zfsHotSpares: 1,
zfsPrefix: 'alpha-data',
operatorEmail: 'operator@example.com',
operatorPassword: 'supersecret',
sshAuthorizedKey:
'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG9wZXJhdG9yS2V5comment operator@example',
clawdieUserPassword: 'sudofallback',
plaintextCredentialWarning: PLAINTEXT_CREDENTIAL_WARNING,
});
});
it('collects provider keys beyond openrouter when present', () => {
const result = parseFirstBootConfig(`
ZAI_API_KEY=zai-test
OPENAI_API_KEY=oa-test
ANTHROPIC_API_KEY=ant-test
CLAUDE_CODE_OAUTH_TOKEN=oauth-test
`);
expect(result.providerApiKeys).toEqual({
ZAI_API_KEY: 'zai-test',
OPENAI_API_KEY: 'oa-test',
ANTHROPIC_API_KEY: 'ant-test',
CLAUDE_CODE_OAUTH_TOKEN: 'oauth-test',
});
expect(result.needsPostInstallProviderSetup).toBe(false);
expect(result.needsPostInstallTelegramSetup).toBe(true);
});
it('derives and validates AGENT_DOMAIN', () => {
const result = parseFirstBootConfig('HOSTNAME=osa\nAGENT_DOMAIN=osa.home.arpa\n');
expect(result.agentDomain).toBe('osa.home.arpa');
expect(() => parseFirstBootConfig('AGENT_DOMAIN=not-a-domain\n')).toThrow(/AGENT_DOMAIN/);
});
it('rejects invalid profiles', () => {
expect(() =>
parseFirstBootConfig(`
OPENROUTER_API_KEY=sk-or-v1-test
TELEGRAM_BOT_TOKEN=123:abc
TELEGRAM_ADMIN_ID=85126311
PROFILE=wild
`),
).toThrow(/PROFILE/);
});
it('rejects unsupported setup schema versions', () => {
expect(() =>
parseFirstBootConfig(`
SETUP_SCHEMA_VERSION=2
OPENROUTER_API_KEY=sk-or-v1-test
TELEGRAM_BOT_TOKEN=123:abc
TELEGRAM_ADMIN_ID=85126311
`),
).toThrow(/SETUP_SCHEMA_VERSION=2/);
});
it('rejects invalid zfs layout combinations', () => {
expect(() =>
parseFirstBootConfig(`
OPENROUTER_API_KEY=sk-or-v1-test
TELEGRAM_BOT_TOKEN=123:abc
TELEGRAM_ADMIN_ID=85126311
ZFS_LAYOUT=single
ZFS_DATA_DISKS=2
`),
).toThrow(/ZFS_LAYOUT=single/);
expect(() =>
parseFirstBootConfig(`
OPENROUTER_API_KEY=sk-or-v1-test
TELEGRAM_BOT_TOKEN=123:abc
TELEGRAM_ADMIN_ID=85126311
ZFS_LAYOUT=raidz1
ZFS_DATA_DISKS=2
`),
).toThrow(/raidz1/);
});
it('rejects invalid zfs scalar values', () => {
expect(() =>
parseFirstBootConfig(`
OPENROUTER_API_KEY=sk-or-v1-test
TELEGRAM_BOT_TOKEN=123:abc
TELEGRAM_ADMIN_ID=85126311
ZFS_LAYOUT=wide
`),
).toThrow(/ZFS_LAYOUT/);
expect(() =>
parseFirstBootConfig(`
OPENROUTER_API_KEY=sk-or-v1-test
TELEGRAM_BOT_TOKEN=123:abc
TELEGRAM_ADMIN_ID=85126311
ZFS_DATA_DISKS=three
`),
).toThrow(/ZFS_DATA_DISKS/);
});
it('parses explicit install modes and rejects invalid ones', () => {
const upgrade = parseFirstBootConfig(`
OPENROUTER_API_KEY=sk-or-v1-test
TELEGRAM_BOT_TOKEN=123:abc
TELEGRAM_ADMIN_ID=85126311
INSTALL_MODE=upgrade
`);
expect(upgrade.installMode).toBe('upgrade');
const rescue = parseFirstBootConfig(`
OPENROUTER_API_KEY=sk-or-v1-test
TELEGRAM_BOT_TOKEN=123:abc
TELEGRAM_ADMIN_ID=85126311
INSTALL_MODE=rescue
`);
expect(rescue.installMode).toBe('rescue');
expect(() =>
parseFirstBootConfig(`
OPENROUTER_API_KEY=sk-or-v1-test
TELEGRAM_BOT_TOKEN=123:abc
TELEGRAM_ADMIN_ID=85126311
INSTALL_MODE=reinstall
`),
).toThrow(/INSTALL_MODE/);
});
it('rejects non-numeric telegram admin ids', () => {
expect(() =>
parseFirstBootConfig(`
OPENROUTER_API_KEY=sk-or-v1-test
TELEGRAM_BOT_TOKEN=123:abc
TELEGRAM_ADMIN_ID=@samob
`),
).toThrow(/TELEGRAM_ADMIN_ID/);
});
it('rejects half-configured dashboard credentials', () => {
expect(() =>
parseFirstBootConfig(`
OPENROUTER_API_KEY=sk-or-v1-test
TELEGRAM_BOT_TOKEN=123:abc
TELEGRAM_ADMIN_ID=85126311
OPERATOR_EMAIL=operator@example.com
`),
).toThrow(/OPERATOR_EMAIL/);
});
it('rejects ROOT_PASSWORD explicitly', () => {
expect(() =>
parseFirstBootConfig(`
OPENROUTER_API_KEY=sk-or-v1-test
TELEGRAM_BOT_TOKEN=123:abc
TELEGRAM_ADMIN_ID=85126311
ROOT_PASSWORD=secret
`),
).toThrow(/ROOT_PASSWORD/);
});
it('rejects invalid ssh key input', () => {
expect(() =>
parseFirstBootConfig(`
OPENROUTER_API_KEY=sk-or-v1-test
TELEGRAM_BOT_TOKEN=123:abc
TELEGRAM_ADMIN_ID=85126311
SSH_AUTHORIZED_KEY=not-a-key
`),
).toThrow(/SSH_AUTHORIZED_KEY/);
});
it('does not warn when only a public ssh key is present', () => {
const result = parseFirstBootConfig(`
OPENROUTER_API_KEY=sk-or-v1-test
TELEGRAM_BOT_TOKEN=123:abc
TELEGRAM_ADMIN_ID=85126311
SSH_AUTHORIZED_KEY=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG9wZXJhdG9yS2V5comment operator@example
`);
expect(result.sshAuthorizedKey).toContain('ssh-ed25519');
expect(result.plaintextCredentialWarning).toBeNull();
});
});
describe('first boot identity derivation', () => {
it('defaults assistant name and hostname independently', () => {
expect(deriveFirstBootIdentity({})).toEqual({
assistantName: 'Clawdie',
hostname: 'clawdie',
});
});
it('does not derive hostnames from assistant names', () => {
expect(deriveFirstBootIdentity({ assistantName: 'Alpha Prime' })).toEqual({
assistantName: 'Alpha Prime',
hostname: 'clawdie',
});
});
it('preserves explicit hostname overrides', () => {
expect(
deriveFirstBootIdentity({
assistantName: 'Alpha',
hostname: 'osa-smilepowered',
}),
).toEqual({
assistantName: 'Alpha',
hostname: 'osa-smilepowered',
});
});
});
describe('first boot storage derivation', () => {
it('derives default datasets from pool and prefix defaults', () => {
expect(deriveFirstBootStorageLayout({})).toEqual({
pool: 'zroot',
prefix: 'clawdie-runtime',
rootDataset: 'zroot/clawdie-runtime',
jailsDataset: 'zroot/clawdie-runtime/jails',
pgDataDataset: 'zroot/clawdie-runtime/pgdata',
pgWalDataset: 'zroot/clawdie-runtime/pgwal',
skillsDataset: 'zroot/clawdie-runtime/skills',
tmpDataset: 'zroot/clawdie-runtime/tmp',
});
});
it('derives dataset paths from custom pool and prefix', () => {
expect(
deriveFirstBootStorageLayout({
zfsPool: 'tank',
zfsPrefix: 'alpha-store',
}),
).toMatchObject({
pool: 'tank',
prefix: 'alpha-store',
rootDataset: 'tank/alpha-store',
jailsDataset: 'tank/alpha-store/jails',
});
});
});
describe('first boot profile bundles', () => {
it('maps balanced to the current operator bundle', () => {
expect(profileToRuntimeEnv('balanced')).toEqual({
chat: { provider: 'openai-codex', model: 'gpt-5.5' },
fallback: { provider: 'openai-codex', model: 'gpt-5.5' },
compaction: { provider: 'openai-codex', model: 'gpt-5.5' },
});
});
it('maps quality to the current premium bundle', () => {
expect(profileToRuntimeEnv('quality')).toEqual({
chat: { provider: 'openai-codex', model: 'gpt-5.5' },
fallback: { provider: 'openai-codex', model: 'gpt-5.5' },
compaction: { provider: 'openai-codex', model: 'gpt-5.5' },
});
});
});