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.
57 lines
1.7 KiB
TypeScript
57 lines
1.7 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { z } from 'zod';
|
|
import { loadSettingsOrExit } from '../src/config-errors.js';
|
|
|
|
describe('loadSettingsOrExit', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('returns the factory value and does not exit on success', () => {
|
|
const exitSpy = vi
|
|
.spyOn(process, 'exit')
|
|
.mockImplementation((() => undefined) as never);
|
|
|
|
const result = loadSettingsOrExit(() => ({ ok: true }));
|
|
|
|
expect(result).toEqual({ ok: true });
|
|
expect(exitSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('prints a named-variable message and exits(1) on a ZodError', () => {
|
|
// Mock process.exit to throw so control stops at the exit point, mirroring
|
|
// the real exit-the-process behaviour without killing the test runner.
|
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((
|
|
code?: number,
|
|
) => {
|
|
throw new Error(`exit:${code}`);
|
|
}) as never);
|
|
const writeSpy = vi
|
|
.spyOn(process.stderr, 'write')
|
|
.mockImplementation(() => true);
|
|
|
|
expect(() =>
|
|
loadSettingsOrExit(() => z.object({ FOO: z.string() }).parse({})),
|
|
).toThrow('exit:1');
|
|
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
const written = writeSpy.mock.calls.map((c) => String(c[0])).join('');
|
|
expect(written).toContain('Missing required variable(s)');
|
|
expect(written).toContain('FOO');
|
|
});
|
|
|
|
it('propagates a non-ZodError without exiting', () => {
|
|
const exitSpy = vi
|
|
.spyOn(process, 'exit')
|
|
.mockImplementation((() => undefined) as never);
|
|
const boom = new Error('x');
|
|
|
|
expect(() =>
|
|
loadSettingsOrExit(() => {
|
|
throw boom;
|
|
}),
|
|
).toThrow(boom);
|
|
expect(exitSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|