369 lines
10 KiB
TypeScript
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' },
|
|
});
|
|
});
|
|
});
|