@brew-cms/api 0.2.1 → 0.2.2

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.
@@ -0,0 +1,155 @@
1
+ import { z } from 'zod';
2
+ export declare const CreateDocumentRequestSchema: z.ZodObject<{
3
+ type: z.ZodEnum<["post", "page"]>;
4
+ slug: z.ZodString;
5
+ title: z.ZodString;
6
+ excerpt: z.ZodNullable<z.ZodOptional<z.ZodString>>;
7
+ sourceMarkdown: z.ZodString;
8
+ frontmatter: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
9
+ seo: z.ZodNullable<z.ZodOptional<z.ZodObject<{
10
+ metaTitle: z.ZodOptional<z.ZodString>;
11
+ metaDescription: z.ZodOptional<z.ZodString>;
12
+ ogImage: z.ZodOptional<z.ZodString>;
13
+ noIndex: z.ZodOptional<z.ZodBoolean>;
14
+ }, "strip", z.ZodTypeAny, {
15
+ metaTitle?: string | undefined;
16
+ metaDescription?: string | undefined;
17
+ ogImage?: string | undefined;
18
+ noIndex?: boolean | undefined;
19
+ }, {
20
+ metaTitle?: string | undefined;
21
+ metaDescription?: string | undefined;
22
+ ogImage?: string | undefined;
23
+ noIndex?: boolean | undefined;
24
+ }>>>;
25
+ }, "strip", z.ZodTypeAny, {
26
+ type: "post" | "page";
27
+ slug: string;
28
+ title: string;
29
+ sourceMarkdown: string;
30
+ excerpt?: string | null | undefined;
31
+ frontmatter?: Record<string, unknown> | undefined;
32
+ seo?: {
33
+ metaTitle?: string | undefined;
34
+ metaDescription?: string | undefined;
35
+ ogImage?: string | undefined;
36
+ noIndex?: boolean | undefined;
37
+ } | null | undefined;
38
+ }, {
39
+ type: "post" | "page";
40
+ slug: string;
41
+ title: string;
42
+ sourceMarkdown: string;
43
+ excerpt?: string | null | undefined;
44
+ frontmatter?: Record<string, unknown> | undefined;
45
+ seo?: {
46
+ metaTitle?: string | undefined;
47
+ metaDescription?: string | undefined;
48
+ ogImage?: string | undefined;
49
+ noIndex?: boolean | undefined;
50
+ } | null | undefined;
51
+ }>;
52
+ export declare const UpdateDocumentRequestSchema: z.ZodObject<{
53
+ title: z.ZodOptional<z.ZodString>;
54
+ slug: z.ZodOptional<z.ZodString>;
55
+ excerpt: z.ZodNullable<z.ZodOptional<z.ZodString>>;
56
+ sourceMarkdown: z.ZodOptional<z.ZodString>;
57
+ frontmatter: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
58
+ seo: z.ZodNullable<z.ZodOptional<z.ZodObject<{
59
+ metaTitle: z.ZodOptional<z.ZodString>;
60
+ metaDescription: z.ZodOptional<z.ZodString>;
61
+ ogImage: z.ZodOptional<z.ZodString>;
62
+ noIndex: z.ZodOptional<z.ZodBoolean>;
63
+ }, "strip", z.ZodTypeAny, {
64
+ metaTitle?: string | undefined;
65
+ metaDescription?: string | undefined;
66
+ ogImage?: string | undefined;
67
+ noIndex?: boolean | undefined;
68
+ }, {
69
+ metaTitle?: string | undefined;
70
+ metaDescription?: string | undefined;
71
+ ogImage?: string | undefined;
72
+ noIndex?: boolean | undefined;
73
+ }>>>;
74
+ changeSummary: z.ZodNullable<z.ZodOptional<z.ZodString>>;
75
+ }, "strip", z.ZodTypeAny, {
76
+ slug?: string | undefined;
77
+ title?: string | undefined;
78
+ excerpt?: string | null | undefined;
79
+ sourceMarkdown?: string | undefined;
80
+ frontmatter?: Record<string, unknown> | undefined;
81
+ seo?: {
82
+ metaTitle?: string | undefined;
83
+ metaDescription?: string | undefined;
84
+ ogImage?: string | undefined;
85
+ noIndex?: boolean | undefined;
86
+ } | null | undefined;
87
+ changeSummary?: string | null | undefined;
88
+ }, {
89
+ slug?: string | undefined;
90
+ title?: string | undefined;
91
+ excerpt?: string | null | undefined;
92
+ sourceMarkdown?: string | undefined;
93
+ frontmatter?: Record<string, unknown> | undefined;
94
+ seo?: {
95
+ metaTitle?: string | undefined;
96
+ metaDescription?: string | undefined;
97
+ ogImage?: string | undefined;
98
+ noIndex?: boolean | undefined;
99
+ } | null | undefined;
100
+ changeSummary?: string | null | undefined;
101
+ }>;
102
+ export declare const PublishDocumentRequestSchema: z.ZodObject<{
103
+ revisionId: z.ZodOptional<z.ZodString>;
104
+ }, "strip", z.ZodTypeAny, {
105
+ revisionId?: string | undefined;
106
+ }, {
107
+ revisionId?: string | undefined;
108
+ }>;
109
+ export declare const ScheduleDocumentRequestSchema: z.ZodObject<{
110
+ scheduledAt: z.ZodString;
111
+ revisionId: z.ZodOptional<z.ZodString>;
112
+ }, "strip", z.ZodTypeAny, {
113
+ scheduledAt: string;
114
+ revisionId?: string | undefined;
115
+ }, {
116
+ scheduledAt: string;
117
+ revisionId?: string | undefined;
118
+ }>;
119
+ export declare const CreateTopicRequestSchema: z.ZodObject<{
120
+ slug: z.ZodString;
121
+ name: z.ZodString;
122
+ description: z.ZodNullable<z.ZodOptional<z.ZodString>>;
123
+ }, "strip", z.ZodTypeAny, {
124
+ slug: string;
125
+ name: string;
126
+ description?: string | null | undefined;
127
+ }, {
128
+ slug: string;
129
+ name: string;
130
+ description?: string | null | undefined;
131
+ }>;
132
+ export declare const CreateTagRequestSchema: z.ZodObject<{
133
+ slug: z.ZodString;
134
+ name: z.ZodString;
135
+ }, "strip", z.ZodTypeAny, {
136
+ slug: string;
137
+ name: string;
138
+ }, {
139
+ slug: string;
140
+ name: string;
141
+ }>;
142
+ export declare const CreateSeriesRequestSchema: z.ZodObject<{
143
+ slug: z.ZodString;
144
+ name: z.ZodString;
145
+ description: z.ZodNullable<z.ZodOptional<z.ZodString>>;
146
+ }, "strip", z.ZodTypeAny, {
147
+ slug: string;
148
+ name: string;
149
+ description?: string | null | undefined;
150
+ }, {
151
+ slug: string;
152
+ name: string;
153
+ description?: string | null | undefined;
154
+ }>;
155
+ //# sourceMappingURL=validators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBtC,CAAC;AAEH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBtC,CAAC;AAEH,eAAO,MAAM,4BAA4B;;;;;;EAEvC,CAAC;AAEH,eAAO,MAAM,6BAA6B;;;;;;;;;EAGxC,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;EAInC,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;EAGjC,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;EAIpC,CAAC"}
@@ -0,0 +1,57 @@
1
+ import { z } from 'zod';
2
+ export const CreateDocumentRequestSchema = z.object({
3
+ type: z.enum(['post', 'page']),
4
+ slug: z.string().min(1),
5
+ title: z.string().min(1),
6
+ excerpt: z.string().optional().nullable(),
7
+ sourceMarkdown: z.string(),
8
+ frontmatter: z.record(z.string(), z.unknown()).optional(),
9
+ seo: z
10
+ .object({
11
+ metaTitle: z.string().optional(),
12
+ metaDescription: z.string().optional(),
13
+ ogImage: z.string().optional(),
14
+ noIndex: z.boolean().optional(),
15
+ })
16
+ .optional()
17
+ .nullable(),
18
+ });
19
+ export const UpdateDocumentRequestSchema = z.object({
20
+ title: z.string().optional(),
21
+ slug: z.string().optional(),
22
+ excerpt: z.string().optional().nullable(),
23
+ sourceMarkdown: z.string().optional(),
24
+ frontmatter: z.record(z.string(), z.unknown()).optional(),
25
+ seo: z
26
+ .object({
27
+ metaTitle: z.string().optional(),
28
+ metaDescription: z.string().optional(),
29
+ ogImage: z.string().optional(),
30
+ noIndex: z.boolean().optional(),
31
+ })
32
+ .optional()
33
+ .nullable(),
34
+ changeSummary: z.string().optional().nullable(),
35
+ });
36
+ export const PublishDocumentRequestSchema = z.object({
37
+ revisionId: z.string().optional(),
38
+ });
39
+ export const ScheduleDocumentRequestSchema = z.object({
40
+ scheduledAt: z.string().datetime(),
41
+ revisionId: z.string().optional(),
42
+ });
43
+ export const CreateTopicRequestSchema = z.object({
44
+ slug: z.string().min(1),
45
+ name: z.string().min(1),
46
+ description: z.string().optional().nullable(),
47
+ });
48
+ export const CreateTagRequestSchema = z.object({
49
+ slug: z.string().min(1),
50
+ name: z.string().min(1),
51
+ });
52
+ export const CreateSeriesRequestSchema = z.object({
53
+ slug: z.string().min(1),
54
+ name: z.string().min(1),
55
+ description: z.string().optional().nullable(),
56
+ });
57
+ //# sourceMappingURL=validators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validators.js","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brew-cms/api",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "REST API router, OpenAPI spec, idempotency, and request validation for BrewCMS",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -12,15 +12,19 @@
12
12
  "import": "./dist/index.js"
13
13
  }
14
14
  },
15
+ "files": [
16
+ "dist",
17
+ "LICENSE"
18
+ ],
15
19
  "dependencies": {
16
- "@brew-cms/content": "0.2.1",
17
- "@brew-cms/core": "0.2.0",
18
- "@brew-cms/intelligence": "0.2.0",
20
+ "@brew-cms/content": "0.2.2",
21
+ "@brew-cms/core": "0.2.1",
22
+ "@brew-cms/intelligence": "0.2.1",
19
23
  "@brew-cms/policy": "0.1.0",
20
24
  "zod": "^3.24.1"
21
25
  },
22
26
  "devDependencies": {
23
- "@brew-cms/db": "0.2.0"
27
+ "@brew-cms/db": "0.2.1"
24
28
  },
25
29
  "scripts": {
26
30
  "build": "tsc -b",
@@ -1,416 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { handleApiRequest, type ApiContext } from '../router.js';
3
- import { InMemoryIdempotencyStore } from '../idempotency.js';
4
- import {
5
- DocumentService,
6
- WorkflowService,
7
- AgentService,
8
- AuditService,
9
- TaxonomyService,
10
- type Actor,
11
- type Clock,
12
- type IdGenerator,
13
- } from '@brew-cms/core';
14
- import { PolicyEngine } from '@brew-cms/policy';
15
- import {
16
- createDatabaseConnection,
17
- seedInitialData,
18
- SQLiteDocumentRepository,
19
- SQLiteRevisionRepository,
20
- SQLiteTaxonomyRepository,
21
- SQLiteAuditEventRepository,
22
- SQLiteAgentRepository,
23
- SQLiteMediaAssetRepository,
24
- } from '@brew-cms/db';
25
- import {
26
- DefaultSourceRegistry,
27
- DeterministicMockEmbeddingProvider,
28
- MemorySemanticIndexAdapter,
29
- MockContentSourceAdapter,
30
- IndexingService,
31
- HybridRetrievalService,
32
- CanonicalSourceResolver,
33
- } from '@brew-cms/intelligence';
34
-
35
- describe('REST API /api/v1 and Router', () => {
36
- let ctx: ApiContext;
37
- const adminActor: Actor = {
38
- id: 'usr_admin_default',
39
- type: 'human',
40
- role: 'Admin',
41
- };
42
-
43
- beforeEach(() => {
44
- const db = createDatabaseConnection({ filePath: ':memory:' });
45
- seedInitialData(db);
46
-
47
- const docRepo = new SQLiteDocumentRepository(db);
48
- const revRepo = new SQLiteRevisionRepository(db);
49
- const taxRepo = new SQLiteTaxonomyRepository(db);
50
- const auditRepo = new SQLiteAuditEventRepository(db);
51
- const agentRepo = new SQLiteAgentRepository(db);
52
-
53
- const policy = new PolicyEngine();
54
- const clock: Clock = { now: () => new Date('2026-09-12T12:00:00Z') };
55
- let idCounter = 1;
56
- const idGen: IdGenerator = { generate: (p = 'id') => `${p}_${idCounter++}` };
57
-
58
- const documentService = new DocumentService(docRepo, revRepo, auditRepo, policy, clock, idGen);
59
- const workflowService = new WorkflowService(docRepo, revRepo, auditRepo, policy, clock, idGen);
60
- const agentService = new AgentService(agentRepo, auditRepo, policy, clock, idGen);
61
- const auditService = new AuditService(auditRepo);
62
- const taxonomyService = new TaxonomyService(taxRepo);
63
-
64
- const mediaRepo = new SQLiteMediaAssetRepository(db);
65
-
66
- ctx = {
67
- documentService,
68
- workflowService,
69
- agentService,
70
- auditService,
71
- taxonomyService,
72
- revisionRepo: revRepo,
73
- mediaRepo,
74
- agentRepo,
75
- idempotencyStore: new InMemoryIdempotencyStore(),
76
- };
77
- });
78
-
79
- it('serves health endpoints', async () => {
80
- const live = await handleApiRequest(
81
- { method: 'GET', path: '/api/health/live', actor: adminActor },
82
- ctx
83
- );
84
- expect(live.status).toBe(200);
85
- expect((live.body as any).status).toBe('live');
86
-
87
- const ready = await handleApiRequest(
88
- { method: 'GET', path: '/api/health/ready', actor: adminActor },
89
- ctx
90
- );
91
- expect(ready.status).toBe(200);
92
- expect((ready.body as any).status).toBe('ready');
93
- });
94
-
95
- it('creates and publishes a document via REST endpoints', async () => {
96
- // 1. Create document
97
- const createRes = await handleApiRequest(
98
- {
99
- method: 'POST',
100
- path: '/api/v1/documents',
101
- actor: adminActor,
102
- body: {
103
- type: 'post',
104
- slug: 'rest-intro',
105
- title: 'REST Architecture in BrewCMS',
106
- sourceMarkdown: '# REST Architecture\n\nContent operating system REST APIs.',
107
- },
108
- },
109
- ctx
110
- );
111
-
112
- expect(createRes.status).toBe(201);
113
- const doc = (createRes.body as any).document;
114
- expect(doc.id).toBeDefined();
115
- expect(doc.status).toBe('DRAFT');
116
-
117
- // 2. Publish document
118
- const publishRes = await handleApiRequest(
119
- {
120
- method: 'POST',
121
- path: `/api/v1/documents/${doc.id}/publish`,
122
- actor: adminActor,
123
- body: {},
124
- },
125
- ctx
126
- );
127
-
128
- expect(publishRes.status).toBe(200);
129
- expect((publishRes.body as any).document.status).toBe('PUBLISHED');
130
- });
131
-
132
- it('enforces idempotency using Idempotency-Key header', async () => {
133
- const key = 'idem_unique_key_123';
134
-
135
- // First mutation
136
- const req1 = await handleApiRequest(
137
- {
138
- method: 'POST',
139
- path: '/api/v1/documents',
140
- headers: { 'idempotency-key': key },
141
- actor: adminActor,
142
- body: {
143
- type: 'post',
144
- slug: 'idempotent-doc',
145
- title: 'Idempotency Post',
146
- sourceMarkdown: '# Test',
147
- },
148
- },
149
- ctx
150
- );
151
-
152
- expect(req1.status).toBe(201);
153
- const initialId = (req1.body as any).document.id;
154
-
155
- // Second identical request with same key
156
- const req2 = await handleApiRequest(
157
- {
158
- method: 'POST',
159
- path: '/api/v1/documents',
160
- headers: { 'idempotency-key': key },
161
- actor: adminActor,
162
- body: {
163
- type: 'post',
164
- slug: 'idempotent-doc',
165
- title: 'Idempotency Post',
166
- sourceMarkdown: '# Test',
167
- },
168
- },
169
- ctx
170
- );
171
-
172
- expect(req2.status).toBe(201);
173
- expect(req2.headers?.['X-Cache-Lookup']).toBe('HIT');
174
- expect((req2.body as any).document.id).toBe(initialId);
175
- });
176
-
177
- it('returns standard machine-readable error envelope on validation failure', async () => {
178
- const res = await handleApiRequest(
179
- {
180
- method: 'POST',
181
- path: '/api/v1/documents',
182
- actor: adminActor,
183
- headers: { 'x-request-id': 'req_xyz' },
184
- body: {
185
- type: 'invalid_type',
186
- },
187
- },
188
- ctx
189
- );
190
-
191
- expect(res.status).toBeGreaterThanOrEqual(400);
192
- const body = res.body as any;
193
- expect(body.error).toBeDefined();
194
- expect(body.error.code).toBeDefined();
195
- expect(body.error.requestId).toBe('req_xyz');
196
- });
197
-
198
- it('handles full editorial lifecycle: submit -> approve -> publish -> unpublish -> restore', async () => {
199
- // 1. Create draft
200
- const createRes = await handleApiRequest(
201
- {
202
- method: 'POST',
203
- path: '/api/v1/documents',
204
- actor: adminActor,
205
- body: {
206
- type: 'post',
207
- slug: 'lifecycle-test',
208
- title: 'Lifecycle Post',
209
- sourceMarkdown: '# Lifecycle Test\n\nInitial version.',
210
- },
211
- },
212
- ctx
213
- );
214
- expect(createRes.status).toBe(201);
215
- const docId = (createRes.body as any).document.id;
216
- const initialRevId = (createRes.body as any).revision.id;
217
-
218
- // 2. Submit for review
219
- const submitRes = await handleApiRequest(
220
- {
221
- method: 'POST',
222
- path: `/api/v1/documents/${docId}/submit`,
223
- actor: adminActor,
224
- body: {},
225
- },
226
- ctx
227
- );
228
- expect(submitRes.status).toBe(200);
229
- expect((submitRes.body as any).status).toBe('IN_REVIEW');
230
-
231
- // 3. Approve
232
- const approveRes = await handleApiRequest(
233
- {
234
- method: 'POST',
235
- path: `/api/v1/documents/${docId}/approve`,
236
- actor: adminActor,
237
- body: {},
238
- },
239
- ctx
240
- );
241
- expect(approveRes.status).toBe(200);
242
- expect((approveRes.body as any).status).toBe('APPROVED');
243
-
244
- // 4. Publish
245
- const publishRes = await handleApiRequest(
246
- {
247
- method: 'POST',
248
- path: `/api/v1/documents/${docId}/publish`,
249
- actor: adminActor,
250
- body: {},
251
- },
252
- ctx
253
- );
254
- expect(publishRes.status).toBe(200);
255
- expect((publishRes.body as any).document.status).toBe('PUBLISHED');
256
-
257
- // 5. Unpublish
258
- const unpublishRes = await handleApiRequest(
259
- {
260
- method: 'POST',
261
- path: `/api/v1/documents/${docId}/unpublish`,
262
- actor: adminActor,
263
- body: {},
264
- },
265
- ctx
266
- );
267
- expect(unpublishRes.status).toBe(200);
268
- expect((unpublishRes.body as any).status).toBe('DRAFT');
269
-
270
- // 6. Restore initial revision
271
- const restoreRes = await handleApiRequest(
272
- {
273
- method: 'POST',
274
- path: `/api/v1/documents/${docId}/restore`,
275
- actor: adminActor,
276
- body: { revisionId: initialRevId },
277
- },
278
- ctx
279
- );
280
- expect(restoreRes.status).toBe(200);
281
- expect((restoreRes.body as any).restoredRevision).toBeDefined();
282
- expect((restoreRes.body as any).restoredRevision.sourceMarkdown).toBe('# Lifecycle Test\n\nInitial version.');
283
- });
284
-
285
- it('manages media and agent endpoints', async () => {
286
- // Media list and create
287
- const mediaCreate = await handleApiRequest(
288
- {
289
- method: 'POST',
290
- path: '/api/v1/media',
291
- actor: adminActor,
292
- body: {
293
- filename: 'banner.png',
294
- mimeType: 'image/png',
295
- sizeBytes: 1024,
296
- altText: 'Banner Image',
297
- },
298
- },
299
- ctx
300
- );
301
- expect(mediaCreate.status).toBe(201);
302
- expect((mediaCreate.body as any).id).toBeDefined();
303
-
304
- const mediaList = await handleApiRequest(
305
- {
306
- method: 'GET',
307
- path: '/api/v1/media',
308
- actor: adminActor,
309
- },
310
- ctx
311
- );
312
- expect(mediaList.status).toBe(200);
313
- expect((mediaList.body as any).items.length).toBeGreaterThan(0);
314
-
315
- // Agent list
316
- const agentList = await handleApiRequest(
317
- {
318
- method: 'GET',
319
- path: '/api/v1/agents',
320
- actor: adminActor,
321
- },
322
- ctx
323
- );
324
- expect(agentList.status).toBe(200);
325
- expect(Array.isArray((agentList.body as any).items)).toBe(true);
326
- });
327
-
328
- it('exposes intelligence status, indexing, and hybrid retrieval endpoints', async () => {
329
- // Set up intelligence services
330
- const registry = new DefaultSourceRegistry();
331
- const mockSource = new MockContentSourceAdapter('brew-sqlite', [
332
- {
333
- sourceId: 'brew-sqlite',
334
- contentId: 'doc-ai-1',
335
- revisionId: 'rev-1',
336
- title: 'Distributed Enterprise Intelligence',
337
- canonicalUrl: '/posts/distributed-enterprise-intelligence',
338
- summary: 'How AI agents work with governed content workflows.',
339
- body: '# Distributed Enterprise Intelligence\n\nHow AI agents work with governed content workflows and retrieval.',
340
- publishedAt: '2026-09-12T00:00:00Z',
341
- },
342
- ]);
343
- registry.register(mockSource);
344
-
345
- const embeddingProvider = new DeterministicMockEmbeddingProvider();
346
- const semanticIndex = new MemorySemanticIndexAdapter();
347
- const indexingService = new IndexingService(registry, embeddingProvider, semanticIndex);
348
- const retrievalService = new HybridRetrievalService(embeddingProvider, semanticIndex);
349
- const canonicalResolver = new CanonicalSourceResolver(registry);
350
-
351
- const intelligenceCtx: ApiContext = {
352
- ...ctx,
353
- sourceRegistry: registry,
354
- indexingService,
355
- retrievalService,
356
- canonicalResolver,
357
- };
358
-
359
- // 1. Status endpoint
360
- const statusRes = await handleApiRequest(
361
- { method: 'GET', path: '/api/v1/intelligence/status', actor: adminActor },
362
- intelligenceCtx
363
- );
364
- expect(statusRes.status).toBe(200);
365
- expect((statusRes.body as any).status).toBe('operational');
366
- expect((statusRes.body as any).sources.length).toBe(1);
367
- expect((statusRes.body as any).sources[0].id).toBe('brew-sqlite');
368
-
369
- // 2. Trigger Indexing
370
- const indexRes = await handleApiRequest(
371
- {
372
- method: 'POST',
373
- path: '/api/v1/intelligence/index',
374
- actor: adminActor,
375
- body: { sourceId: 'brew-sqlite' },
376
- },
377
- intelligenceCtx
378
- );
379
- expect(indexRes.status).toBe(200);
380
- expect((indexRes.body as any).summary.indexed).toBe(1);
381
-
382
- // 3. Search endpoint (references only)
383
- const searchRes = await handleApiRequest(
384
- {
385
- method: 'GET',
386
- path: '/api/v1/intelligence/search',
387
- actor: adminActor,
388
- query: { q: 'enterprise intelligence' },
389
- },
390
- intelligenceCtx
391
- );
392
- expect(searchRes.status).toBe(200);
393
- expect((searchRes.body as any).total).toBeGreaterThan(0);
394
- const item = (searchRes.body as any).items[0];
395
- expect(item.contentId).toBe('doc-ai-1');
396
- expect(item.sourceId).toBe('brew-sqlite');
397
- // Content body must NOT be in search reference result
398
- expect(item.markdown).toBeUndefined();
399
-
400
- // 4. Search with resolve=true (canonical resolution)
401
- const resolvedSearchRes = await handleApiRequest(
402
- {
403
- method: 'GET',
404
- path: '/api/v1/intelligence/search',
405
- actor: adminActor,
406
- query: { q: 'enterprise intelligence', resolve: 'true' },
407
- },
408
- intelligenceCtx
409
- );
410
- expect(resolvedSearchRes.status).toBe(200);
411
- const resolvedItem = (resolvedSearchRes.body as any).items[0];
412
- expect(resolvedItem.reference.contentId).toBe('doc-ai-1');
413
- expect(resolvedItem.canonical.title).toBe('Distributed Enterprise Intelligence');
414
- expect(resolvedItem.canonical.body).toContain('governed content workflows');
415
- });
416
- });