chore(scaffold): bootstrap docmost-sync Node/TS project skeleton
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.
This commit is contained in:
56
test/config-errors.test.ts
Normal file
56
test/config-errors.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user