create-agent-rig 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/package.json +54 -0
- package/packages/cli/dist/commands/create.js +109 -0
- package/packages/cli/dist/index.js +102 -0
- package/packages/cli/dist/lib/colors.js +14 -0
- package/packages/cli/dist/lib/composition.js +20 -0
- package/packages/cli/dist/lib/copy-tree.js +91 -0
- package/packages/cli/dist/lib/prompts.js +24 -0
- package/packages/cli/dist/lib/substitute.js +22 -0
- package/packages/cli/dist/lib/summary.js +41 -0
- package/packages/cli/dist/lib/targets.js +14 -0
- package/packages/cli/dist/templates.js +21 -0
- package/scripts/prepare.mjs +29 -0
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +49 -0
- package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +59 -0
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +51 -0
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +39 -0
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +36 -0
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +40 -0
- package/templates/agent-os/universal/.claude/agents/test-writer.md +36 -0
- package/templates/agent-os/universal/.claude/hooks/block-no-verify.mjs +43 -0
- package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +79 -0
- package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +53 -0
- package/templates/agent-os/universal/.claude/rules/architecture.md +74 -0
- package/templates/agent-os/universal/.claude/rules/autonomy.md +81 -0
- package/templates/agent-os/universal/.claude/rules/workflow.md +62 -0
- package/templates/agent-os/universal/.claude/settings.json +28 -0
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +53 -0
- package/templates/agent-os/universal/CLAUDE.md +62 -0
- package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +23 -0
- package/templates/skeleton/aws-serverless/README.md +78 -0
- package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +17 -0
- package/templates/skeleton/aws-serverless/apps/web/package.json +19 -0
- package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +17 -0
- package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +96 -0
- package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +29 -0
- package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +23 -0
- package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +38 -0
- package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +14 -0
- package/templates/skeleton/aws-serverless/eslint.config.mjs +20 -0
- package/templates/skeleton/aws-serverless/gitignore +9 -0
- package/templates/skeleton/aws-serverless/infra/bin/app.ts +19 -0
- package/templates/skeleton/aws-serverless/infra/cdk.json +3 -0
- package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +116 -0
- package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +32 -0
- package/templates/skeleton/aws-serverless/infra/package.json +18 -0
- package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +104 -0
- package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +41 -0
- package/templates/skeleton/aws-serverless/package.json +30 -0
- package/templates/skeleton/aws-serverless/packages/core/package.json +11 -0
- package/templates/skeleton/aws-serverless/packages/core/src/events.ts +14 -0
- package/templates/skeleton/aws-serverless/packages/core/src/index.ts +15 -0
- package/templates/skeleton/aws-serverless/packages/core/src/note.ts +69 -0
- package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +23 -0
- package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +101 -0
- package/templates/skeleton/aws-serverless/packages/db/package.json +14 -0
- package/templates/skeleton/aws-serverless/packages/db/src/client.ts +17 -0
- package/templates/skeleton/aws-serverless/packages/db/src/index.ts +2 -0
- package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +52 -0
- package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +91 -0
- package/templates/skeleton/aws-serverless/packages/shared/package.json +11 -0
- package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +17 -0
- package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +33 -0
- package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +3 -0
- package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +20 -0
- package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +26 -0
- package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +28 -0
- package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +19 -0
- package/templates/skeleton/aws-serverless/pnpm-lock.yaml +2855 -0
- package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +14 -0
- package/templates/skeleton/aws-serverless/services/api/package.json +15 -0
- package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +26 -0
- package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +42 -0
- package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +24 -0
- package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +12 -0
- package/templates/skeleton/aws-serverless/services/api/src/main.ts +21 -0
- package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +30 -0
- package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +14 -0
- package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +92 -0
- package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +45 -0
- package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +51 -0
- package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +22 -0
- package/templates/skeleton/aws-serverless/services/worker/package.json +12 -0
- package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +15 -0
- package/templates/skeleton/aws-serverless/services/worker/src/main.ts +7 -0
- package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +37 -0
- package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +61 -0
- package/templates/skeleton/aws-serverless/tsconfig.base.json +15 -0
- package/templates/skeleton/aws-serverless/tsconfig.json +16 -0
- package/templates/skeleton/aws-serverless/vitest.config.ts +14 -0
- package/templates/skeleton/node-service/.github/workflows/ci.yml +22 -0
- package/templates/skeleton/node-service/README.md +74 -0
- package/templates/skeleton/node-service/apps/web/next.config.mjs +17 -0
- package/templates/skeleton/node-service/apps/web/package.json +19 -0
- package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +17 -0
- package/templates/skeleton/node-service/apps/web/src/app/page.tsx +96 -0
- package/templates/skeleton/node-service/apps/web/src/lib/api.ts +29 -0
- package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +23 -0
- package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +38 -0
- package/templates/skeleton/node-service/apps/web/tsconfig.json +14 -0
- package/templates/skeleton/node-service/eslint.config.mjs +20 -0
- package/templates/skeleton/node-service/gitignore +9 -0
- package/templates/skeleton/node-service/package.json +28 -0
- package/templates/skeleton/node-service/packages/core/package.json +11 -0
- package/templates/skeleton/node-service/packages/core/src/events.ts +14 -0
- package/templates/skeleton/node-service/packages/core/src/index.ts +15 -0
- package/templates/skeleton/node-service/packages/core/src/note.ts +69 -0
- package/templates/skeleton/node-service/packages/core/test/events.test.ts +23 -0
- package/templates/skeleton/node-service/packages/core/test/note.test.ts +101 -0
- package/templates/skeleton/node-service/packages/db/package.json +12 -0
- package/templates/skeleton/node-service/packages/db/src/index.ts +1 -0
- package/templates/skeleton/node-service/packages/db/src/note-store.ts +63 -0
- package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +80 -0
- package/templates/skeleton/node-service/packages/shared/package.json +11 -0
- package/templates/skeleton/node-service/packages/shared/src/env.ts +17 -0
- package/templates/skeleton/node-service/packages/shared/src/errors.ts +33 -0
- package/templates/skeleton/node-service/packages/shared/src/index.ts +3 -0
- package/templates/skeleton/node-service/packages/shared/src/logger.ts +20 -0
- package/templates/skeleton/node-service/packages/shared/test/env.test.ts +26 -0
- package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +28 -0
- package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +19 -0
- package/templates/skeleton/node-service/pnpm-lock.yaml +2399 -0
- package/templates/skeleton/node-service/pnpm-workspace.yaml +13 -0
- package/templates/skeleton/node-service/services/api/package.json +17 -0
- package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +23 -0
- package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +40 -0
- package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +23 -0
- package/templates/skeleton/node-service/services/api/src/main.ts +47 -0
- package/templates/skeleton/node-service/services/api/src/server.ts +89 -0
- package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +30 -0
- package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +14 -0
- package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +64 -0
- package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +43 -0
- package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +48 -0
- package/templates/skeleton/node-service/services/api/test/server.test.ts +123 -0
- package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +32 -0
- package/templates/skeleton/node-service/services/worker/package.json +16 -0
- package/templates/skeleton/node-service/services/worker/src/main.ts +28 -0
- package/templates/skeleton/node-service/services/worker/src/spool.ts +60 -0
- package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +38 -0
- package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +34 -0
- package/templates/skeleton/node-service/services/worker/test/spool.test.ts +76 -0
- package/templates/skeleton/node-service/tsconfig.base.json +15 -0
- package/templates/skeleton/node-service/tsconfig.json +13 -0
- package/templates/skeleton/node-service/vitest.config.ts +12 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { GetCommand, PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
import { NotFoundError } from '@app/shared';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { NoteModel } from '../src/note-model.js';
|
|
5
|
+
import type { DocumentClient } from '../src/client.js';
|
|
6
|
+
|
|
7
|
+
const note = {
|
|
8
|
+
id: 'n1',
|
|
9
|
+
title: 'Hello',
|
|
10
|
+
slug: 'hello',
|
|
11
|
+
tags: ['x'],
|
|
12
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function stubClient(response: unknown = {}) {
|
|
16
|
+
const sent: unknown[] = [];
|
|
17
|
+
const client: DocumentClient = {
|
|
18
|
+
send: (command) => {
|
|
19
|
+
sent.push(command);
|
|
20
|
+
return Promise.resolve(response);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
return { client, sent };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('NoteModel.put', () => {
|
|
27
|
+
it('writes the note under the single-table key, refusing overwrites', async () => {
|
|
28
|
+
const { client, sent } = stubClient();
|
|
29
|
+
await new NoteModel(client, 'notes-table').put(note);
|
|
30
|
+
expect(sent).toHaveLength(1);
|
|
31
|
+
const command = sent[0] as PutCommand;
|
|
32
|
+
expect(command).toBeInstanceOf(PutCommand);
|
|
33
|
+
expect(command.input).toMatchObject({
|
|
34
|
+
TableName: 'notes-table',
|
|
35
|
+
Item: { pk: 'NOTE#n1', sk: 'META', ...note },
|
|
36
|
+
ConditionExpression: 'attribute_not_exists(pk)',
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('NoteModel.get', () => {
|
|
42
|
+
it('reads by key and validates the stored shape', async () => {
|
|
43
|
+
const { client, sent } = stubClient({ Item: { pk: 'NOTE#n1', sk: 'META', ...note } });
|
|
44
|
+
const loaded = await new NoteModel(client, 'notes-table').get('n1');
|
|
45
|
+
expect(loaded).toEqual(note);
|
|
46
|
+
const command = sent[0] as GetCommand;
|
|
47
|
+
expect(command).toBeInstanceOf(GetCommand);
|
|
48
|
+
expect(command.input).toMatchObject({
|
|
49
|
+
TableName: 'notes-table',
|
|
50
|
+
Key: { pk: 'NOTE#n1', sk: 'META' },
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('throws NotFoundError on a miss', async () => {
|
|
55
|
+
const { client } = stubClient({});
|
|
56
|
+
await expect(new NoteModel(client, 'notes-table').get('nope')).rejects.toThrow(NotFoundError);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('refuses corrupt stored data instead of returning it', async () => {
|
|
60
|
+
const { client } = stubClient({ Item: { pk: 'NOTE#n1', sk: 'META', id: 'n1' } });
|
|
61
|
+
await expect(new NoteModel(client, 'notes-table').get('n1')).rejects.toThrow();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('NoteModel.list', () => {
|
|
66
|
+
it('scans meta items, validates them, and returns newest-first', async () => {
|
|
67
|
+
const older = { ...note, id: 'a', createdAt: '2024-01-01T00:00:00.000Z' };
|
|
68
|
+
const newer = { ...note, id: 'b', createdAt: '2024-02-01T00:00:00.000Z' };
|
|
69
|
+
const { client, sent } = stubClient({
|
|
70
|
+
Items: [
|
|
71
|
+
{ pk: 'NOTE#a', sk: 'META', ...older },
|
|
72
|
+
{ pk: 'NOTE#b', sk: 'META', ...newer },
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
const listed = await new NoteModel(client, 'notes-table').list();
|
|
76
|
+
expect(listed.map((n) => n.id)).toEqual(['b', 'a']);
|
|
77
|
+
const command = sent[0] as ScanCommand;
|
|
78
|
+
expect(command).toBeInstanceOf(ScanCommand);
|
|
79
|
+
expect(command.input.TableName).toBe('notes-table');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns [] for an empty table', async () => {
|
|
83
|
+
const { client } = stubClient({ Items: [] });
|
|
84
|
+
expect(await new NoteModel(client, 'notes-table').list()).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('refuses corrupt stored entries instead of skipping them', async () => {
|
|
88
|
+
const { client } = stubClient({ Items: [{ pk: 'NOTE#x', sk: 'META', id: 'x' }] });
|
|
89
|
+
await expect(new NoteModel(client, 'notes-table').list()).rejects.toThrow();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ZodType } from 'zod';
|
|
2
|
+
import { AppError } from './errors.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse the environment through a zod schema, once, at startup. Configuration
|
|
6
|
+
* is invalid → the process fails loudly at boot, not deep inside a request.
|
|
7
|
+
*/
|
|
8
|
+
export function loadEnv<T>(schema: ZodType<T>, source: Record<string, string | undefined>): T {
|
|
9
|
+
const parsed = schema.safeParse(source);
|
|
10
|
+
if (!parsed.success) {
|
|
11
|
+
const details = parsed.error.issues
|
|
12
|
+
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
13
|
+
.join('; ');
|
|
14
|
+
throw new AppError(`invalid environment: ${details}`, { code: 'ENV_INVALID' });
|
|
15
|
+
}
|
|
16
|
+
return parsed.data;
|
|
17
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed errors. Every error that crosses a layer boundary is one of these —
|
|
3
|
+
* handlers map them to transport codes without inspecting messages.
|
|
4
|
+
*/
|
|
5
|
+
export class AppError extends Error {
|
|
6
|
+
readonly code: string;
|
|
7
|
+
readonly statusCode: number;
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
message: string,
|
|
11
|
+
options: { code?: string; statusCode?: number; cause?: unknown } = {},
|
|
12
|
+
) {
|
|
13
|
+
super(message, options.cause !== undefined ? { cause: options.cause } : undefined);
|
|
14
|
+
this.name = new.target.name;
|
|
15
|
+
this.code = options.code ?? 'INTERNAL';
|
|
16
|
+
this.statusCode = options.statusCode ?? 500;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ValidationError extends AppError {
|
|
21
|
+
readonly issues: readonly string[];
|
|
22
|
+
|
|
23
|
+
constructor(message: string, issues: readonly string[] = []) {
|
|
24
|
+
super(message, { code: 'VALIDATION', statusCode: 400 });
|
|
25
|
+
this.issues = issues;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class NotFoundError extends AppError {
|
|
30
|
+
constructor(message: string) {
|
|
31
|
+
super(message, { code: 'NOT_FOUND', statusCode: 404 });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Minimal structured logger: one JSON object per line on stdout. */
|
|
2
|
+
export interface Logger {
|
|
3
|
+
info(message: string, fields?: Record<string, unknown>): void;
|
|
4
|
+
warn(message: string, fields?: Record<string, unknown>): void;
|
|
5
|
+
error(message: string, fields?: Record<string, unknown>): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createLogger(
|
|
9
|
+
base: Record<string, unknown> = {},
|
|
10
|
+
write: (line: string) => void = (line) => process.stdout.write(line + '\n'),
|
|
11
|
+
): Logger {
|
|
12
|
+
const emit = (level: string, message: string, fields?: Record<string, unknown>) => {
|
|
13
|
+
write(JSON.stringify({ level, message, ...base, ...fields }));
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
info: (message, fields) => emit('info', message, fields),
|
|
17
|
+
warn: (message, fields) => emit('warn', message, fields),
|
|
18
|
+
error: (message, fields) => emit('error', message, fields),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { AppError } from '../src/errors.js';
|
|
4
|
+
import { loadEnv } from '../src/env.js';
|
|
5
|
+
|
|
6
|
+
const schema = z.object({
|
|
7
|
+
TABLE_NAME: z.string().min(1),
|
|
8
|
+
QUEUE_URL: z.string().min(1),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('loadEnv', () => {
|
|
12
|
+
it('returns the typed subset of the environment', () => {
|
|
13
|
+
const env = loadEnv(schema, { TABLE_NAME: 't', QUEUE_URL: 'q', UNRELATED: 'x' });
|
|
14
|
+
expect(env).toEqual({ TABLE_NAME: 't', QUEUE_URL: 'q' });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('fails loudly, naming every missing key', () => {
|
|
18
|
+
expect(() => loadEnv(schema, {})).toThrow(AppError);
|
|
19
|
+
expect(() => loadEnv(schema, {})).toThrow(/TABLE_NAME/);
|
|
20
|
+
expect(() => loadEnv(schema, {})).toThrow(/QUEUE_URL/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('rejects empty values', () => {
|
|
24
|
+
expect(() => loadEnv(schema, { TABLE_NAME: '', QUEUE_URL: 'q' })).toThrow(/TABLE_NAME/);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { AppError, NotFoundError, ValidationError } from '../src/errors.js';
|
|
3
|
+
|
|
4
|
+
describe('typed errors', () => {
|
|
5
|
+
it('AppError defaults to INTERNAL/500', () => {
|
|
6
|
+
const error = new AppError('boom');
|
|
7
|
+
expect(error.code).toBe('INTERNAL');
|
|
8
|
+
expect(error.statusCode).toBe(500);
|
|
9
|
+
expect(error.name).toBe('AppError');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('preserves an explicit cause', () => {
|
|
13
|
+
const cause = new Error('root');
|
|
14
|
+
expect(new AppError('boom', { cause }).cause).toBe(cause);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('ValidationError is 400 and carries issues', () => {
|
|
18
|
+
const error = new ValidationError('bad input', ['title: required']);
|
|
19
|
+
expect(error.statusCode).toBe(400);
|
|
20
|
+
expect(error.code).toBe('VALIDATION');
|
|
21
|
+
expect(error.issues).toEqual(['title: required']);
|
|
22
|
+
expect(error).toBeInstanceOf(AppError);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('NotFoundError is 404', () => {
|
|
26
|
+
expect(new NotFoundError('nope').statusCode).toBe(404);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createLogger } from '../src/logger.js';
|
|
3
|
+
|
|
4
|
+
describe('createLogger', () => {
|
|
5
|
+
it('emits one JSON line per call with level, message, base and call fields', () => {
|
|
6
|
+
const lines: string[] = [];
|
|
7
|
+
const log = createLogger({ service: 'api' }, (line) => lines.push(line));
|
|
8
|
+
log.info('started', { port: 3000 });
|
|
9
|
+
log.error('failed');
|
|
10
|
+
expect(lines).toHaveLength(2);
|
|
11
|
+
expect(JSON.parse(lines[0]!)).toEqual({
|
|
12
|
+
level: 'info',
|
|
13
|
+
message: 'started',
|
|
14
|
+
service: 'api',
|
|
15
|
+
port: 3000,
|
|
16
|
+
});
|
|
17
|
+
expect(JSON.parse(lines[1]!)).toMatchObject({ level: 'error', message: 'failed' });
|
|
18
|
+
});
|
|
19
|
+
});
|