feat(phase3): wire notelett trail import route

This commit is contained in:
Saravana Achu Mac 2026-04-04 00:33:38 -07:00
parent 4af86b43f7
commit 7ee2151f17
4 changed files with 177 additions and 1 deletions

View 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',
});
});
});

View 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],
};
});
}

View File

@ -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' });
});
});

View File

@ -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);