Set up the project structure per the new-project guide, adapted from the Python skeleton to the Node/TS stack fixed in SPEC.md (reuses docmost-mcp). Scaffold only — the sync engine is not implemented yet. - src/settings.ts: single config layer on zod, schema keyed by real ENV names; credentials and own-service address have no default (fail fast). - src/config-errors.ts: loadSettingsOrExit — clear startup message naming the missing/invalid env var instead of a raw stack trace; exit(1). - src/index.ts: thin entry point that validates config and logs (stub). - test/: vitest unit tests for settings parsing and config errors (10 tests). - Makefile (install/env/build/test/run/dev/clean), strict tsconfig, vitest. - Dockerfile (single-stage, no EXPOSE, prunes dev deps), docker-compose (daemon, volume on /app/data, watchtower), ghcr CI with build needs test. - .env.example, .gitignore/.dockerignore, AGENTS.md, README.md. - Pinned deps (dotenv, zod) + committed package-lock.json.
77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { parseSettings } from '../src/settings.js';
|
|
|
|
// A minimal valid environment with every required variable set. Tests clone and
|
|
// mutate this object so process.env is never touched (hermetic).
|
|
const baseEnv = {
|
|
DOCMOST_API_URL: 'https://docmost.example.com',
|
|
DOCMOST_EMAIL: 'you@example.com',
|
|
DOCMOST_PASSWORD: 'secret',
|
|
DOCMOST_SPACE_ID: 'space-123',
|
|
} as NodeJS.ProcessEnv;
|
|
|
|
describe('parseSettings', () => {
|
|
it('maps a full valid env to the camelCase Settings object', () => {
|
|
const settings = parseSettings({
|
|
...baseEnv,
|
|
VAULT_PATH: 'data/custom-vault',
|
|
GIT_REMOTE: 'git@github.com:you/vault.git',
|
|
POLL_INTERVAL_MS: '5000',
|
|
DEBOUNCE_MS: '1000',
|
|
LOG_LEVEL: 'debug',
|
|
});
|
|
|
|
expect(settings).toEqual({
|
|
docmostApiUrl: 'https://docmost.example.com',
|
|
docmostEmail: 'you@example.com',
|
|
docmostPassword: 'secret',
|
|
docmostSpaceId: 'space-123',
|
|
vaultPath: 'data/custom-vault',
|
|
gitRemote: 'git@github.com:you/vault.git',
|
|
pollIntervalMs: 5000,
|
|
debounceMs: 1000,
|
|
logLevel: 'debug',
|
|
});
|
|
});
|
|
|
|
it('applies defaults when optional vars are omitted', () => {
|
|
const settings = parseSettings({ ...baseEnv });
|
|
|
|
expect(settings.vaultPath).toBe('data/vault');
|
|
expect(settings.pollIntervalMs).toBe(15000);
|
|
expect(settings.debounceMs).toBe(2000);
|
|
expect(settings.logLevel).toBe('info');
|
|
expect(settings.gitRemote).toBeUndefined();
|
|
});
|
|
|
|
it('coerces numeric strings to numbers', () => {
|
|
const settings = parseSettings({ ...baseEnv, POLL_INTERVAL_MS: '3000' });
|
|
|
|
expect(settings.pollIntervalMs).toBe(3000);
|
|
expect(typeof settings.pollIntervalMs).toBe('number');
|
|
});
|
|
|
|
it('throws when a required var is missing', () => {
|
|
const { DOCMOST_API_URL: _omit, ...rest } = baseEnv;
|
|
void _omit;
|
|
expect(() => parseSettings(rest as NodeJS.ProcessEnv)).toThrow();
|
|
});
|
|
|
|
it('throws on an invalid LOG_LEVEL', () => {
|
|
expect(() =>
|
|
parseSettings({ ...baseEnv, LOG_LEVEL: 'verbose' }),
|
|
).toThrow();
|
|
});
|
|
|
|
it('throws on a non-numeric POLL_INTERVAL_MS', () => {
|
|
expect(() =>
|
|
parseSettings({ ...baseEnv, POLL_INTERVAL_MS: 'soon' }),
|
|
).toThrow();
|
|
});
|
|
|
|
it('treats an empty GIT_REMOTE as undefined', () => {
|
|
const settings = parseSettings({ ...baseEnv, GIT_REMOTE: '' });
|
|
expect(settings.gitRemote).toBeUndefined();
|
|
});
|
|
});
|