create-agent-rig 0.9.1 → 0.10.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/CHANGELOG.md +419 -3
- package/README.md +267 -305
- package/package.json +8 -14
- package/packages/cli/dist/commands/create.js +56 -88
- package/packages/cli/dist/commands/doctor.js +213 -0
- package/packages/cli/dist/commands/init.js +111 -50
- package/packages/cli/dist/commands/integrations.js +468 -0
- package/packages/cli/dist/commands/setup-wizard.js +61 -0
- package/packages/cli/dist/commands/uninstall.js +1223 -0
- package/packages/cli/dist/commands/upgrade.js +472 -90
- package/packages/cli/dist/index.js +651 -46
- package/packages/cli/dist/integrations/declaration.js +158 -0
- package/packages/cli/dist/integrations/doctor-guards.js +126 -0
- package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
- package/packages/cli/dist/integrations/mcp-json.js +107 -0
- package/packages/cli/dist/integrations/memory-doctor.js +124 -0
- package/packages/cli/dist/integrations/registry.js +22 -0
- package/packages/cli/dist/integrations/spawn.js +228 -0
- package/packages/cli/dist/integrations/spec-kit.js +280 -0
- package/packages/cli/dist/integrations/verify.js +193 -0
- package/packages/cli/dist/integrations/windows-job.js +275 -0
- package/packages/cli/dist/lib/elevated-paths.js +74 -0
- package/packages/cli/dist/lib/install-set.js +1 -44
- package/packages/cli/dist/lib/manifest.js +64 -5
- package/packages/cli/dist/lib/prompts.js +0 -23
- package/packages/cli/dist/lib/safe-path.js +193 -2
- package/packages/cli/dist/lib/safe-text.js +78 -0
- package/packages/cli/dist/lib/substitute.js +1 -52
- package/packages/cli/dist/lib/summary.js +5 -5
- package/packages/cli/dist/templates.js +4 -14
- package/scripts/prepare.mjs +28 -12
- package/templates/agent-os/subagent-routing.json +4 -4
- package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +13 -9
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
- package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
- package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -2
- package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
- package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
- package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
- package/templates/agent-os/universal/.claude/rules/workflow.md +47 -22
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
- package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
- package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
- package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
- package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
- package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +4 -3
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
- package/templates/agent-os/universal/.claude/settings.json +0 -8
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +13 -9
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
- package/templates/agent-os/universal/.codex/hooks.json +0 -10
- package/templates/agent-os/universal/AGENTS.md +208 -107
- package/templates/agent-os/universal/CLAUDE.md +15 -162
- package/templates/agent-os/universal/PLAN.md +17 -10
- package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
- package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
- package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
- package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
- package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
- package/templates/agent-os/universal/layers.json +25 -30
- package/templates/hash-history.json +8 -4
- package/templates/release-ledger.json +2 -1
- package/packages/cli/dist/lib/composition.js +0 -20
- package/packages/cli/dist/lib/targets.js +0 -28
- package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
- package/packages/cli/dist/policy/core/adapter.js +0 -18
- package/packages/cli/dist/policy/core/coverage.js +0 -253
- package/packages/cli/dist/policy/core/decision-record.js +0 -287
- package/packages/cli/dist/policy/core/declaration.js +0 -127
- package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
- package/packages/cli/dist/policy/core/probe.js +0 -442
- package/packages/cli/dist/policy/core/registry.js +0 -115
- package/packages/cli/dist/policy/core/validation.js +0 -275
- package/packages/cli/dist/policy/core/vocabulary.js +0 -123
- package/packages/cli/dist/policy/harness/claude.js +0 -47
- package/packages/cli/dist/policy/harness/codex.js +0 -87
- package/packages/cli/dist/policy/harness/index.js +0 -15
- package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
- package/packages/cli/dist/policy/index.js +0 -17
- package/templates/agent-os/init/AGENTS.md +0 -201
- package/templates/agent-os/init/CLAUDE.md +0 -201
- package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
- package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
- package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
- package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
- package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
- package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
- package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
- package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
- package/templates/skeleton/aws-serverless/README.md +0 -181
- package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
- package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
- package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
- package/templates/skeleton/aws-serverless/gitignore +0 -56
- package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
- package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
- package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
- package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
- package/templates/skeleton/aws-serverless/infra/package.json +0 -18
- package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
- package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
- package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
- package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
- package/templates/skeleton/aws-serverless/package.json +0 -30
- package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
- package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
- package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
- package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
- package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
- package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
- package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
- package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
- package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
- package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
- package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
- package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
- package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
- package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
- package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
- package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
- package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
- package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
- package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
- package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
- package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
- package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
- package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
- package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
- package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
- package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
- package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
- package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
- package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
- package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
- package/templates/skeleton/node-service/README.md +0 -104
- package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/node-service/apps/web/package.json +0 -19
- package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
- package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/node-service/eslint.config.mjs +0 -20
- package/templates/skeleton/node-service/gitignore +0 -54
- package/templates/skeleton/node-service/package.json +0 -30
- package/templates/skeleton/node-service/packages/core/package.json +0 -11
- package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
- package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
- package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
- package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/node-service/packages/db/package.json +0 -12
- package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
- package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
- package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
- package/templates/skeleton/node-service/packages/shared/package.json +0 -11
- package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
- package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
- package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
- package/templates/skeleton/node-service/services/api/package.json +0 -17
- package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
- package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
- package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
- package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
- package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
- package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
- package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
- package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
- package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
- package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
- package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
- package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
- package/templates/skeleton/node-service/services/worker/package.json +0 -16
- package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
- package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
- package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
- package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
- package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
- package/templates/skeleton/node-service/tsconfig.base.json +0 -15
- package/templates/skeleton/node-service/tsconfig.json +0 -13
- package/templates/skeleton/node-service/vitest.config.ts +0 -12
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
NewNoteSchema,
|
|
3
|
-
NoteSchema,
|
|
4
|
-
InvalidNoteError,
|
|
5
|
-
createNote,
|
|
6
|
-
slugify,
|
|
7
|
-
type NewNote,
|
|
8
|
-
type Note,
|
|
9
|
-
type NoteIdentity,
|
|
10
|
-
} from './note.js';
|
|
11
|
-
export {
|
|
12
|
-
NoteCreatedEventSchema,
|
|
13
|
-
makeNoteCreatedEvent,
|
|
14
|
-
type NoteCreatedEvent,
|
|
15
|
-
} from './events.js';
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// The domain core is pure: no I/O, no clock, no randomness, no environment.
|
|
2
|
-
// "Now" and "a new id" are decided by the caller (the usecase layer) and enter
|
|
3
|
-
// as arguments. The guard-core-purity hook enforces this at the tool layer.
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
export const NewNoteSchema = z.object({
|
|
7
|
-
title: z.string().trim().min(1, 'title must not be empty').max(200, 'title too long'),
|
|
8
|
-
tags: z.array(z.string().trim().min(1).max(40)).max(10).default([]),
|
|
9
|
-
});
|
|
10
|
-
export type NewNote = z.infer<typeof NewNoteSchema>;
|
|
11
|
-
|
|
12
|
-
export const NoteSchema = z.object({
|
|
13
|
-
id: z.string().min(1),
|
|
14
|
-
title: z.string().min(1),
|
|
15
|
-
slug: z.string().min(1),
|
|
16
|
-
tags: z.array(z.string()),
|
|
17
|
-
createdAt: z.string().min(1),
|
|
18
|
-
});
|
|
19
|
-
export type Note = z.infer<typeof NoteSchema>;
|
|
20
|
-
|
|
21
|
-
/** Thrown when input fails domain validation. The core owns its own error type. */
|
|
22
|
-
export class InvalidNoteError extends Error {
|
|
23
|
-
readonly issues: readonly string[];
|
|
24
|
-
|
|
25
|
-
constructor(issues: readonly string[]) {
|
|
26
|
-
super(`invalid note: ${issues.join('; ')}`);
|
|
27
|
-
this.name = 'InvalidNoteError';
|
|
28
|
-
this.issues = issues;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface NoteIdentity {
|
|
33
|
-
/** Generated by the caller — the core never invents ids. */
|
|
34
|
-
id: string;
|
|
35
|
-
/** ISO-8601 timestamp decided by the caller — the core never reads the clock. */
|
|
36
|
-
createdAt: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* The one domain function: validate raw input and construct a Note.
|
|
41
|
-
* Deterministic — identical arguments always yield an identical note.
|
|
42
|
-
*/
|
|
43
|
-
export function createNote(input: unknown, identity: NoteIdentity): Note {
|
|
44
|
-
const parsed = NewNoteSchema.safeParse(input);
|
|
45
|
-
if (!parsed.success) {
|
|
46
|
-
throw new InvalidNoteError(
|
|
47
|
-
parsed.error.issues.map((issue) => `${issue.path.join('.') || 'input'}: ${issue.message}`),
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
const { title, tags } = parsed.data;
|
|
51
|
-
return {
|
|
52
|
-
id: identity.id,
|
|
53
|
-
title,
|
|
54
|
-
slug: slugify(title),
|
|
55
|
-
tags: [...new Set(tags)],
|
|
56
|
-
createdAt: identity.createdAt,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Lowercased, hyphen-separated, ascii-ish slug. Pure string work. */
|
|
61
|
-
export function slugify(title: string): string {
|
|
62
|
-
const slug = title
|
|
63
|
-
.toLowerCase()
|
|
64
|
-
.normalize('NFKD')
|
|
65
|
-
.replace(/[\u0300-\u036f]/g, '') // strip combining marks left by NFKD
|
|
66
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
67
|
-
.replace(/^-+|-+$/g, '');
|
|
68
|
-
return slug.length > 0 ? slug : 'note';
|
|
69
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { NoteCreatedEventSchema, makeNoteCreatedEvent } from '../src/events.js';
|
|
3
|
-
import { createNote } from '../src/note.js';
|
|
4
|
-
|
|
5
|
-
const note = createNote({ title: 'Hello' }, { id: 'n1', createdAt: '2024-01-01T00:00:00.000Z' });
|
|
6
|
-
|
|
7
|
-
describe('note.created event', () => {
|
|
8
|
-
it('wraps a note and round-trips through its own schema', () => {
|
|
9
|
-
const event = makeNoteCreatedEvent(note);
|
|
10
|
-
expect(event.type).toBe('note.created');
|
|
11
|
-
expect(NoteCreatedEventSchema.parse(JSON.parse(JSON.stringify(event)))).toEqual(event);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('rejects a payload with the wrong type tag', () => {
|
|
15
|
-
expect(NoteCreatedEventSchema.safeParse({ type: 'other', note }).success).toBe(false);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('rejects a payload with a malformed note', () => {
|
|
19
|
-
expect(
|
|
20
|
-
NoteCreatedEventSchema.safeParse({ type: 'note.created', note: { id: '' } }).success,
|
|
21
|
-
).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { InvalidNoteError, createNote, slugify } from '../src/note.js';
|
|
3
|
-
|
|
4
|
-
const identity = { id: 'note-1', createdAt: '2024-01-01T00:00:00.000Z' };
|
|
5
|
-
|
|
6
|
-
describe('createNote', () => {
|
|
7
|
-
it('builds a note from valid input', () => {
|
|
8
|
-
const note = createNote({ title: 'Hello World', tags: ['a', 'b'] }, identity);
|
|
9
|
-
expect(note).toEqual({
|
|
10
|
-
id: 'note-1',
|
|
11
|
-
title: 'Hello World',
|
|
12
|
-
slug: 'hello-world',
|
|
13
|
-
tags: ['a', 'b'],
|
|
14
|
-
createdAt: '2024-01-01T00:00:00.000Z',
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('is deterministic: identical arguments yield an identical note', () => {
|
|
19
|
-
const a = createNote({ title: 'Same' }, identity);
|
|
20
|
-
const b = createNote({ title: 'Same' }, identity);
|
|
21
|
-
expect(a).toEqual(b);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('defaults tags to an empty array', () => {
|
|
25
|
-
expect(createNote({ title: 'No tags' }, identity).tags).toEqual([]);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('trims the title', () => {
|
|
29
|
-
expect(createNote({ title: ' padded ' }, identity).title).toBe('padded');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('deduplicates tags', () => {
|
|
33
|
-
expect(createNote({ title: 'T', tags: ['x', 'x', 'y'] }, identity).tags).toEqual(['x', 'y']);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('refuses an empty or whitespace-only title', () => {
|
|
37
|
-
expect(() => createNote({ title: '' }, identity)).toThrow(InvalidNoteError);
|
|
38
|
-
expect(() => createNote({ title: ' ' }, identity)).toThrow(InvalidNoteError);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('refuses a title over 200 characters', () => {
|
|
42
|
-
expect(() => createNote({ title: 'x'.repeat(201) }, identity)).toThrow(InvalidNoteError);
|
|
43
|
-
expect(() => createNote({ title: 'x'.repeat(200) }, identity)).not.toThrow();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('refuses more than 10 tags', () => {
|
|
47
|
-
const tags = Array.from({ length: 11 }, (_, i) => `t${i}`);
|
|
48
|
-
expect(() => createNote({ title: 'T', tags }, identity)).toThrow(InvalidNoteError);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('refuses empty or oversized tags', () => {
|
|
52
|
-
expect(() => createNote({ title: 'T', tags: [''] }, identity)).toThrow(InvalidNoteError);
|
|
53
|
-
expect(() => createNote({ title: 'T', tags: ['x'.repeat(41)] }, identity)).toThrow(
|
|
54
|
-
InvalidNoteError,
|
|
55
|
-
);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('refuses non-object input', () => {
|
|
59
|
-
for (const bad of [null, undefined, 42, 'title', []]) {
|
|
60
|
-
expect(() => createNote(bad, identity)).toThrow(InvalidNoteError);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('refuses wrongly-typed fields', () => {
|
|
65
|
-
expect(() => createNote({ title: 42 }, identity)).toThrow(InvalidNoteError);
|
|
66
|
-
expect(() => createNote({ title: 'T', tags: 'not-an-array' }, identity)).toThrow(
|
|
67
|
-
InvalidNoteError,
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('reports every issue with its path', () => {
|
|
72
|
-
try {
|
|
73
|
-
createNote({ title: '', tags: [''] }, identity);
|
|
74
|
-
expect.unreachable('should have thrown');
|
|
75
|
-
} catch (error) {
|
|
76
|
-
const invalid = error as InvalidNoteError;
|
|
77
|
-
expect(invalid.issues.length).toBeGreaterThanOrEqual(2);
|
|
78
|
-
expect(invalid.issues.join('\n')).toMatch(/title/);
|
|
79
|
-
expect(invalid.issues.join('\n')).toMatch(/tags/);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('slugify', () => {
|
|
85
|
-
it('lowercases and hyphenates', () => {
|
|
86
|
-
expect(slugify('Hello World')).toBe('hello-world');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('collapses runs of non-alphanumerics and trims hyphens', () => {
|
|
90
|
-
expect(slugify(' a -- b!! c ')).toBe('a-b-c');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('strips diacritics', () => {
|
|
94
|
-
expect(slugify('Crème Brûlée')).toBe('creme-brulee');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('falls back to "note" when nothing survives', () => {
|
|
98
|
-
expect(slugify('!!!')).toBe('note');
|
|
99
|
-
expect(slugify('日本語')).toBe('note');
|
|
100
|
-
});
|
|
101
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@app/db",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "src/index.ts",
|
|
7
|
-
"types": "src/index.ts",
|
|
8
|
-
"dependencies": {
|
|
9
|
-
"@app/core": "workspace:*",
|
|
10
|
-
"@app/shared": "workspace:*",
|
|
11
|
-
"@aws-sdk/client-dynamodb": "^3.1092.0",
|
|
12
|
-
"@aws-sdk/lib-dynamodb": "^3.1092.0"
|
|
13
|
-
}
|
|
14
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// The ONLY file in the project that constructs the storage SDK client.
|
|
2
|
-
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
3
|
-
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Structural view of the document client — models depend on this, tests stub it.
|
|
7
|
-
* (Method parameters are bivariant, so the real client satisfies it.)
|
|
8
|
-
*/
|
|
9
|
-
export interface DocumentClient {
|
|
10
|
-
send(command: unknown): Promise<unknown>;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function createDocumentClient(): DocumentClient {
|
|
14
|
-
return DynamoDBDocumentClient.from(new DynamoDBClient({}), {
|
|
15
|
-
marshallOptions: { removeUndefinedValues: true },
|
|
16
|
-
}) as DocumentClient;
|
|
17
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// Single-table layout: pk = NOTE#<id>, sk = META. One model, one table, one
|
|
2
|
-
// place that knows the key schema.
|
|
3
|
-
import { GetCommand, PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb';
|
|
4
|
-
import { NoteSchema, type Note } from '@app/core';
|
|
5
|
-
import { NotFoundError } from '@app/shared';
|
|
6
|
-
import type { DocumentClient } from './client.js';
|
|
7
|
-
|
|
8
|
-
const noteKey = (id: string) => ({ pk: `NOTE#${id}`, sk: 'META' });
|
|
9
|
-
|
|
10
|
-
export class NoteModel {
|
|
11
|
-
constructor(
|
|
12
|
-
private readonly client: DocumentClient,
|
|
13
|
-
private readonly tableName: string,
|
|
14
|
-
) {}
|
|
15
|
-
|
|
16
|
-
async put(note: Note): Promise<void> {
|
|
17
|
-
await this.client.send(
|
|
18
|
-
new PutCommand({
|
|
19
|
-
TableName: this.tableName,
|
|
20
|
-
Item: { ...noteKey(note.id), ...note },
|
|
21
|
-
ConditionExpression: 'attribute_not_exists(pk)',
|
|
22
|
-
}),
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async get(id: string): Promise<Note> {
|
|
27
|
-
const result = (await this.client.send(
|
|
28
|
-
new GetCommand({ TableName: this.tableName, Key: noteKey(id) }),
|
|
29
|
-
)) as { Item?: Record<string, unknown> };
|
|
30
|
-
if (!result.Item) {
|
|
31
|
-
throw new NotFoundError(`note ${id} not found`);
|
|
32
|
-
}
|
|
33
|
-
// Validate on the way out too: the database is an external system.
|
|
34
|
-
return NoteSchema.parse(result.Item);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async list(): Promise<Note[]> {
|
|
38
|
-
// A filtered Scan is fine at skeleton scale; swap for a GSI Query when
|
|
39
|
-
// real data volume arrives (a Tier-2 change — it touches the schema).
|
|
40
|
-
const result = (await this.client.send(
|
|
41
|
-
new ScanCommand({
|
|
42
|
-
TableName: this.tableName,
|
|
43
|
-
FilterExpression: 'sk = :meta',
|
|
44
|
-
ExpressionAttributeValues: { ':meta': 'META' },
|
|
45
|
-
}),
|
|
46
|
-
)) as { Items?: Array<Record<string, unknown>> };
|
|
47
|
-
// Validate every entry — silently skipping corruption would hide data loss.
|
|
48
|
-
return (result.Items ?? [])
|
|
49
|
-
.map((item) => NoteSchema.parse(item))
|
|
50
|
-
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
51
|
-
}
|
|
52
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
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
|
-
});
|