feat(phase3): wire notelett trail import route
This commit is contained in:
parent
4af86b43f7
commit
7ee2151f17
124
backend/src/modules/ecosystem-phase3/routes.test.ts
Normal file
124
backend/src/modules/ecosystem-phase3/routes.test.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
|
||||
const {
|
||||
requireWriterMock,
|
||||
createNoteMock,
|
||||
createNoteArtifactMock,
|
||||
loadLatestTrailReportArtifactMock,
|
||||
loadLatestTrailReportCreatedEventMock,
|
||||
buildPhase3TrailNoteImportMock,
|
||||
persistPhase3NoteOutputsMock,
|
||||
} = vi.hoisted(() => ({
|
||||
requireWriterMock: vi.fn(async () => ({ sub: 'user_1', role: 'editor' })),
|
||||
createNoteMock: vi.fn(async (note: unknown) => note),
|
||||
createNoteArtifactMock: vi.fn(async (artifact: unknown) => artifact),
|
||||
loadLatestTrailReportArtifactMock: vi.fn(async () => ({
|
||||
id: 'art_trail_1',
|
||||
ownership: { orgId: null },
|
||||
provenance: { sessionId: 'sess_1', correlationId: 'corr_1', lineage: [] },
|
||||
payload: { sourceProduct: 'claw-cowork', sourceTaskId: 'task_1', actionCount: 3, safetySignalCount: 1, actionBreakdown: [], entries: [] },
|
||||
title: 'Cowork audit report',
|
||||
summary: '3 audited actions',
|
||||
})),
|
||||
loadLatestTrailReportCreatedEventMock: vi.fn(async () => ({
|
||||
eventId: 'evt_trail_created_1',
|
||||
eventName: 'artifact.created',
|
||||
trace: { correlationId: 'corr_1', causationId: null, parentEventId: null },
|
||||
})),
|
||||
buildPhase3TrailNoteImportMock: vi.fn(() => ({
|
||||
note: { id: 'note_1' },
|
||||
noteArtifactDoc: { id: 'na_1' },
|
||||
ecosystemNoteArtifact: { id: 'art_note_1' },
|
||||
createdEvent: { eventId: 'evt_note_created_1' },
|
||||
linkedEvent: { eventId: 'evt_note_linked_1' },
|
||||
})),
|
||||
persistPhase3NoteOutputsMock: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.mock('../../lib/auth.js', () => ({ requireWriter: requireWriterMock }));
|
||||
vi.mock('zod', () => ({
|
||||
z: {
|
||||
object: (shape: Record<string, { min: () => { max: () => unknown } }>) => ({
|
||||
safeParse: (value: unknown) => {
|
||||
const workspaceId = (value as { workspaceId?: unknown } | undefined)?.workspaceId;
|
||||
if (typeof workspaceId === 'string' && workspaceId.length > 0) {
|
||||
return { success: true, data: { workspaceId } };
|
||||
}
|
||||
return { success: false, error: { issues: [{ message: 'workspaceId is required' }] } };
|
||||
},
|
||||
}),
|
||||
string: () => ({
|
||||
min: () => ({
|
||||
max: () => ({}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
vi.mock('@bytelyst/errors', () => ({
|
||||
BadRequestError: class BadRequestError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'BadRequestError';
|
||||
}
|
||||
},
|
||||
}));
|
||||
vi.mock('../notes/repository.js', () => ({ createNote: createNoteMock }));
|
||||
vi.mock('../note-artifacts/repository.js', () => ({ createNoteArtifact: createNoteArtifactMock }));
|
||||
vi.mock('../../lib/ecosystem-phase3.js', () => ({
|
||||
loadLatestTrailReportArtifact: loadLatestTrailReportArtifactMock,
|
||||
loadLatestTrailReportCreatedEvent: loadLatestTrailReportCreatedEventMock,
|
||||
buildPhase3TrailNoteImport: buildPhase3TrailNoteImportMock,
|
||||
persistPhase3NoteOutputs: persistPhase3NoteOutputsMock,
|
||||
}));
|
||||
|
||||
import { ecosystemPhase3Routes } from './routes.js';
|
||||
|
||||
describe('ecosystemPhase3Routes', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('registers the import handler and returns imported trail report metadata', async () => {
|
||||
let handler: ((req: { body?: unknown }) => Promise<unknown>) | undefined;
|
||||
const app = {
|
||||
post: vi.fn((path: string, cb: typeof handler) => {
|
||||
expect(path).toBe('/ecosystem/phase3/import-latest-trail-report');
|
||||
handler = cb;
|
||||
}),
|
||||
};
|
||||
|
||||
await ecosystemPhase3Routes(app as never);
|
||||
|
||||
const result = await handler?.({ body: { workspaceId: 'ws_1' } });
|
||||
|
||||
expect(requireWriterMock).toHaveBeenCalledOnce();
|
||||
expect(loadLatestTrailReportArtifactMock).toHaveBeenCalledOnce();
|
||||
expect(loadLatestTrailReportCreatedEventMock).toHaveBeenCalledOnce();
|
||||
expect(buildPhase3TrailNoteImportMock).toHaveBeenCalledOnce();
|
||||
expect(createNoteMock).toHaveBeenCalledOnce();
|
||||
expect(createNoteArtifactMock).toHaveBeenCalledOnce();
|
||||
expect(persistPhase3NoteOutputsMock).toHaveBeenCalledOnce();
|
||||
expect(result).toEqual({
|
||||
note: { id: 'note_1' },
|
||||
noteArtifact: { id: 'na_1' },
|
||||
ecosystemArtifactId: 'art_note_1',
|
||||
linkedTrailReportArtifactId: 'art_trail_1',
|
||||
events: ['evt_note_created_1', 'evt_note_linked_1'],
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects invalid request bodies', async () => {
|
||||
let handler: ((req: { body?: unknown }) => Promise<unknown>) | undefined;
|
||||
const app = {
|
||||
post: vi.fn((_path: string, cb: typeof handler) => {
|
||||
handler = cb;
|
||||
}),
|
||||
};
|
||||
|
||||
await ecosystemPhase3Routes(app as never);
|
||||
|
||||
await expect(handler?.({ body: {} })).rejects.toMatchObject({
|
||||
name: 'BadRequestError',
|
||||
});
|
||||
});
|
||||
});
|
||||
47
backend/src/modules/ecosystem-phase3/routes.ts
Normal file
47
backend/src/modules/ecosystem-phase3/routes.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { BadRequestError } from '@bytelyst/errors';
|
||||
import { requireWriter } from '../../lib/auth.js';
|
||||
import { createNote } from '../notes/repository.js';
|
||||
import { createNoteArtifact } from '../note-artifacts/repository.js';
|
||||
import {
|
||||
buildPhase3TrailNoteImport,
|
||||
loadLatestTrailReportArtifact,
|
||||
loadLatestTrailReportCreatedEvent,
|
||||
persistPhase3NoteOutputs,
|
||||
} from '../../lib/ecosystem-phase3.js';
|
||||
|
||||
const ImportLatestTrailReportSchema = z.object({
|
||||
workspaceId: z.string().min(1).max(128),
|
||||
});
|
||||
|
||||
export async function ecosystemPhase3Routes(app: FastifyInstance) {
|
||||
app.post('/ecosystem/phase3/import-latest-trail-report', async req => {
|
||||
const auth = await requireWriter(req);
|
||||
const parsed = ImportLatestTrailReportSchema.safeParse(req.body ?? {});
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestError(parsed.error.issues.map(issue => issue.message).join('; '));
|
||||
}
|
||||
|
||||
const trailReportArtifact = await loadLatestTrailReportArtifact();
|
||||
const trailReportCreatedEvent = await loadLatestTrailReportCreatedEvent();
|
||||
const generated = buildPhase3TrailNoteImport({
|
||||
trailReportArtifact,
|
||||
trailReportCreatedEvent,
|
||||
workspaceId: parsed.data.workspaceId,
|
||||
userId: auth.sub,
|
||||
});
|
||||
|
||||
const note = await createNote(generated.note);
|
||||
const noteArtifactDoc = await createNoteArtifact(generated.noteArtifactDoc);
|
||||
await persistPhase3NoteOutputs(generated);
|
||||
|
||||
return {
|
||||
note,
|
||||
noteArtifact: noteArtifactDoc,
|
||||
ecosystemArtifactId: generated.ecosystemNoteArtifact.id,
|
||||
linkedTrailReportArtifactId: trailReportArtifact.id,
|
||||
events: [generated.createdEvent.eventId, generated.linkedEvent.eventId],
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -30,6 +30,7 @@ vi.mock('./modules/note-tasks/routes.js', () => ({ noteTaskRoutes: vi.fn() }));
|
||||
vi.mock('./modules/saved-views/routes.js', () => ({ savedViewRoutes: vi.fn() }));
|
||||
vi.mock('./modules/workspaces/routes.js', () => ({ workspaceRoutes: vi.fn() }));
|
||||
vi.mock('./modules/ecosystem-phase1/routes.js', () => ({ ecosystemPhase1Routes: vi.fn() }));
|
||||
vi.mock('./modules/ecosystem-phase3/routes.js', () => ({ ecosystemPhase3Routes: vi.fn() }));
|
||||
vi.mock('./lib/cosmos-init.js', () => ({ initCosmosIfNeeded: initCosmosIfNeededMock }));
|
||||
vi.mock('./lib/datastore.js', () => ({ initDatastore: initDatastoreMock }));
|
||||
vi.mock('./lib/config.js', () => ({
|
||||
@ -49,6 +50,8 @@ vi.mock('./lib/product-config.js', () => ({
|
||||
vi.mock('./lib/field-encrypt.js', () => ({ initEncryption: vi.fn(async () => undefined), getEncryptor: vi.fn() }));
|
||||
vi.mock('./lib/feature-flags.js', () => ({ getAllFlags: vi.fn(() => ({})) }));
|
||||
vi.mock('./lib/telemetry.js', () => ({ getBufferedEvents: vi.fn(() => []), flushEvents: vi.fn(() => []) }));
|
||||
vi.mock('./modules/note-shares/repository.js', () => ({ findShareByToken: vi.fn(async () => null) }));
|
||||
vi.mock('./modules/notes/repository.js', () => ({ getNote: vi.fn(async () => null) }));
|
||||
|
||||
describe('server bootstrap', () => {
|
||||
beforeEach(() => {
|
||||
@ -66,7 +69,7 @@ describe('server bootstrap', () => {
|
||||
expect(initDatastoreMock).toHaveBeenCalledOnce();
|
||||
expect(createServiceAppMock).toHaveBeenCalledOnce();
|
||||
expect(registerOptionalJwtContextMock).toHaveBeenCalledOnce();
|
||||
expect(appMock.register).toHaveBeenCalledTimes(8);
|
||||
expect(appMock.register).toHaveBeenCalledTimes(9);
|
||||
expect(startServiceMock).toHaveBeenCalledWith(appMock, { port: 4016, host: '0.0.0.0' });
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@ import { createServiceApp, registerOptionalJwtContext, startService } from '@byt
|
||||
import { jwtVerify } from 'jose';
|
||||
import { noteAgentActionRoutes } from './modules/note-agent-actions/routes.js';
|
||||
import { ecosystemPhase1Routes } from './modules/ecosystem-phase1/routes.js';
|
||||
import { ecosystemPhase3Routes } from './modules/ecosystem-phase3/routes.js';
|
||||
import { noteArtifactRoutes } from './modules/note-artifacts/routes.js';
|
||||
import { noteRoutes } from './modules/notes/routes.js';
|
||||
import { noteRelationshipRoutes } from './modules/note-relationships/routes.js';
|
||||
@ -53,6 +54,7 @@ async function registerApiPlugin(plugin: unknown) {
|
||||
|
||||
await registerApiPlugin(noteAgentActionRoutes);
|
||||
await registerApiPlugin(ecosystemPhase1Routes);
|
||||
await registerApiPlugin(ecosystemPhase3Routes);
|
||||
await registerApiPlugin(noteArtifactRoutes);
|
||||
await registerApiPlugin(noteRoutes);
|
||||
await registerApiPlugin(noteRelationshipRoutes);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user