From 623d02c32f6665fb7f90ff53d21d24ceb6d775ca Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 3 Apr 2026 19:13:55 -0700 Subject: [PATCH] =?UTF-8?q?test(notes):=20verify=20phase1=20transcript?= =?UTF-8?q?=E2=86=92note=20import=20against=20@bytelyst/events=20schemas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add @bytelyst/events dependency for contract validation - Expand ecosystem-phase1 tests from 1 to 13 focused tests: - transcript artifact import from disk - transcript capture event load + missing file graceful - note creation with productId, sourceType, links, tags - note artifact doc for internal persistence - artifact.created event with upstream causation propagation - artifact.linked event chained from artifact.created - provenance lineage preservation (lysnrai→notelett) - NoteArtifactEnvelopeSchema conformance (no contract drift) - ArtifactCreatedEventSchema conformance - ArtifactLinkedEventSchema conformance - disk persistence + index file verification - graceful degradation without capture event - Fix server.test.ts route count (7→8) for ecosystem-phase1 route --- backend/package.json | 15 +- backend/src/lib/ecosystem-phase1.test.ts | 353 +++++++++++++++++------ backend/src/server.test.ts | 3 +- pnpm-lock.yaml | 19 ++ 4 files changed, 293 insertions(+), 97 deletions(-) diff --git a/backend/package.json b/backend/package.json index a255bf4..6288321 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,16 +17,17 @@ "dependencies": { "@azure/cosmos": "^4.2.0", "@bytelyst/auth": "^0.1.0", - "@bytelyst/config": "^0.1.0", - "@bytelyst/cosmos": "^0.1.0", - "@bytelyst/datastore": "^0.1.0", "@bytelyst/backend-config": "^0.1.0", "@bytelyst/backend-flags": "^0.1.0", "@bytelyst/backend-telemetry": "^0.1.0", + "@bytelyst/config": "^0.1.0", + "@bytelyst/cosmos": "^0.1.0", + "@bytelyst/datastore": "^0.1.0", "@bytelyst/errors": "^0.1.0", - "@bytelyst/field-encrypt": "^0.1.0", + "@bytelyst/events": "^0.1.0", "@bytelyst/fastify-auth": "^0.1.0", "@bytelyst/fastify-core": "^0.1.0", + "@bytelyst/field-encrypt": "^0.1.0", "@bytelyst/logger": "^0.1.0", "fastify": "5.7.4", "jose": "^6.0.8", @@ -35,10 +36,10 @@ "devDependencies": { "@bytelyst/testing": "^0.1.0", "@types/node": "^22.12.0", + "eslint": "^9.0.0", "tsx": "^4.19.2", "typescript": "^5.7.3", - "vitest": "^3.0.5", - "eslint": "^9.0.0", - "typescript-eslint": "^8.0.0" + "typescript-eslint": "^8.0.0", + "vitest": "^3.0.5" } } diff --git a/backend/src/lib/ecosystem-phase1.test.ts b/backend/src/lib/ecosystem-phase1.test.ts index e5da1b5..c418327 100644 --- a/backend/src/lib/ecosystem-phase1.test.ts +++ b/backend/src/lib/ecosystem-phase1.test.ts @@ -1,7 +1,12 @@ -import { afterEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { + NoteArtifactEnvelopeSchema, + ArtifactCreatedEventSchema, + ArtifactLinkedEventSchema, +} from '@bytelyst/events'; import { buildPhase1NoteImport, loadLatestTranscriptArtifact, @@ -9,92 +14,250 @@ import { persistPhase1NoteOutputs, } from './ecosystem-phase1.js'; +const TRANSCRIPT_ARTIFACT = { + id: 'art_transcript_123', + title: 'Standup capture', + summary: 'Transcript summary', + sourceSurface: 'desktop', + createdAt: '2026-04-03T18:15:00.000Z', + tags: ['voice'], + ownership: { userId: 'user_saravana', orgId: null }, + provenance: { + originProductId: 'lysnrai', + originActionId: 'capture_123', + sessionId: 'sess_123', + runId: null, + approvalId: null, + correlationId: 'corr_123', + lineage: [ + { + stepType: 'captured', + productId: 'lysnrai', + actorType: 'user' as const, + timestamp: '2026-04-03T18:15:00.000Z', + }, + ], + }, + payload: { + transcriptText: 'Transcript body', + transcriptSource: 'microphone', + language: 'en', + durationMs: 1000, + }, +}; + +const TRANSCRIPT_CAPTURE_EVENT = { + eventId: 'evt_capture_123', + eventName: 'capture.transcript.created' as const, + eventVersion: 1 as const, + occurredAt: '2026-04-03T18:15:00.000Z', + productId: 'lysnrai', + sourceSurface: 'desktop', + userId: 'user_saravana', + orgId: null, + sessionId: 'sess_123', + runId: null, + artifactId: 'art_transcript_123', + actor: { actorType: 'user' as const, actorId: 'user_saravana' }, + trace: { + correlationId: 'corr_123', + causationId: null, + parentEventId: null, + }, + payload: { + artifactId: 'art_transcript_123', + durationMs: 1000, + language: 'en', + transcriptSource: 'microphone' as const, + }, +}; + describe('ecosystem phase1 note import', () => { - afterEach(() => { - delete process.env.BYTELYST_ECOSYSTEM_DIR; - }); + let root: string; - it('loads the latest transcript and persists a linked note artifact + events', async () => { - const root = await mkdtemp(join(tmpdir(), 'notelett-phase1-')); + beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), 'notelett-phase1-')); process.env.BYTELYST_ECOSYSTEM_DIR = root; - const transcriptArtifact = { - id: 'art_transcript_123', - title: 'Standup capture', - summary: 'Transcript summary', - sourceSurface: 'desktop', - createdAt: '2026-04-03T18:15:00.000Z', - tags: ['voice'], - ownership: { userId: 'user_saravana', orgId: null }, - provenance: { - originProductId: 'lysnrai', - originActionId: 'capture_123', - sessionId: 'sess_123', - runId: null, - approvalId: null, - correlationId: 'corr_123', - lineage: [ - { - stepType: 'captured', - productId: 'lysnrai', - actorType: 'user', - timestamp: '2026-04-03T18:15:00.000Z', - }, - ], - }, - payload: { - transcriptText: 'Transcript body', - transcriptSource: 'microphone', - language: 'en', - durationMs: 1000, - }, - }; - await mkdir(join(root, 'indexes'), { recursive: true }); await writeFile( join(root, 'indexes', 'latest-transcript.json'), - `${JSON.stringify(transcriptArtifact, null, 2)}\n`, + `${JSON.stringify(TRANSCRIPT_ARTIFACT, null, 2)}\n`, 'utf-8' ); await writeFile( join(root, 'indexes', 'latest-transcript-event.json'), - `${JSON.stringify( - { - eventId: 'evt_capture_123', - eventName: 'capture.transcript.created', - eventVersion: 1, - occurredAt: '2026-04-03T18:15:00.000Z', - productId: 'lysnrai', - sourceSurface: 'desktop', - userId: 'user_saravana', - orgId: null, - sessionId: 'sess_123', - runId: null, - artifactId: 'art_transcript_123', - actor: { actorType: 'user', actorId: 'user_saravana' }, - trace: { - correlationId: 'corr_123', - causationId: null, - parentEventId: null, - }, - payload: { - artifactId: 'art_transcript_123', - durationMs: 1000, - language: 'en', - transcriptSource: 'microphone', - }, - }, - null, - 2 - )}\n`, + `${JSON.stringify(TRANSCRIPT_CAPTURE_EVENT, null, 2)}\n`, 'utf-8' ); + }); + afterEach(async () => { + delete process.env.BYTELYST_ECOSYSTEM_DIR; + await rm(root, { recursive: true, force: true }); + }); + + it('loads the latest transcript artifact from disk', async () => { const loaded = await loadLatestTranscriptArtifact(root); - const transcriptCaptureEvent = await loadLatestTranscriptCaptureEvent(root); + expect(loaded.id).toBe('art_transcript_123'); + expect(loaded.payload.transcriptText).toBe('Transcript body'); + expect(loaded.provenance.originProductId).toBe('lysnrai'); + }); + + it('loads the latest transcript capture event from disk', async () => { + const event = await loadLatestTranscriptCaptureEvent(root); + expect(event).not.toBeNull(); + expect(event!.eventName).toBe('capture.transcript.created'); + expect(event!.artifactId).toBe('art_transcript_123'); + }); + + it('returns null when transcript capture event file is missing', async () => { + await rm(join(root, 'indexes', 'latest-transcript-event.json')); + const event = await loadLatestTranscriptCaptureEvent(root); + expect(event).toBeNull(); + }); + + it('creates a note linked to the transcript artifact', () => { + const generated = buildPhase1NoteImport({ + transcriptArtifact: TRANSCRIPT_ARTIFACT, + transcriptCaptureEvent: TRANSCRIPT_CAPTURE_EVENT, + workspaceId: 'ws_phase1', + userId: 'user_saravana', + now: '2026-04-03T18:17:00.000Z', + }); + + expect(generated.note.productId).toBe('notelett'); + expect(generated.note.sourceType).toBe('ecosystem-transcript'); + expect(generated.note.sourceUri).toBe('art_transcript_123'); + expect(generated.note.links).toContain('art_transcript_123'); + expect(generated.note.tags).toContain('ecosystem'); + expect(generated.note.tags).toContain('phase1'); + expect(generated.note.body).toContain('Transcript body'); + }); + + it('creates a note artifact doc for internal persistence', () => { + const generated = buildPhase1NoteImport({ + transcriptArtifact: TRANSCRIPT_ARTIFACT, + transcriptCaptureEvent: TRANSCRIPT_CAPTURE_EVENT, + workspaceId: 'ws_phase1', + userId: 'user_saravana', + now: '2026-04-03T18:17:00.000Z', + }); + + expect(generated.noteArtifactDoc.productId).toBe('notelett'); + expect(generated.noteArtifactDoc.artifactType).toBe('summary'); + expect(generated.noteArtifactDoc.noteId).toBe(generated.note.id); + }); + + it('emits artifact.created event with upstream causation', () => { + const generated = buildPhase1NoteImport({ + transcriptArtifact: TRANSCRIPT_ARTIFACT, + transcriptCaptureEvent: TRANSCRIPT_CAPTURE_EVENT, + workspaceId: 'ws_phase1', + userId: 'user_saravana', + now: '2026-04-03T18:17:00.000Z', + }); + + expect(generated.createdEvent.eventName).toBe('artifact.created'); + expect(generated.createdEvent.productId).toBe('notelett'); + expect(generated.createdEvent.artifactId).toBe(generated.ecosystemNoteArtifact.id); + expect(generated.createdEvent.trace.causationId).toBe('evt_capture_123'); + expect(generated.createdEvent.trace.parentEventId).toBe('evt_capture_123'); + expect(generated.createdEvent.trace.correlationId).toBe('corr_123'); + }); + + it('emits artifact.linked event chained from artifact.created', () => { + const generated = buildPhase1NoteImport({ + transcriptArtifact: TRANSCRIPT_ARTIFACT, + transcriptCaptureEvent: TRANSCRIPT_CAPTURE_EVENT, + workspaceId: 'ws_phase1', + userId: 'user_saravana', + now: '2026-04-03T18:17:00.000Z', + }); + + expect(generated.linkedEvent.eventName).toBe('artifact.linked'); + expect(generated.linkedEvent.trace.causationId).toBe(generated.createdEvent.eventId); + expect(generated.linkedEvent.trace.parentEventId).toBe(generated.createdEvent.eventId); + expect(generated.linkedEvent.payload).toEqual({ + sourceArtifactId: generated.ecosystemNoteArtifact.id, + targetArtifactId: 'art_transcript_123', + relation: 'summarizes', + }); + }); + + it('preserves provenance lineage from transcript to note', () => { + const generated = buildPhase1NoteImport({ + transcriptArtifact: TRANSCRIPT_ARTIFACT, + transcriptCaptureEvent: TRANSCRIPT_CAPTURE_EVENT, + workspaceId: 'ws_phase1', + userId: 'user_saravana', + now: '2026-04-03T18:17:00.000Z', + }); + + const { provenance } = generated.ecosystemNoteArtifact; + expect(provenance.originProductId).toBe('lysnrai'); + expect(provenance.lineage).toHaveLength(2); + expect(provenance.lineage[0]).toEqual({ + stepType: 'captured', + productId: 'lysnrai', + actorType: 'user', + timestamp: '2026-04-03T18:15:00.000Z', + }); + expect(provenance.lineage[1]).toMatchObject({ + stepType: 'note-created', + productId: 'notelett', + actorType: 'agent', + }); + expect(generated.ecosystemNoteArtifact.links).toEqual([ + { relation: 'summarizes', targetArtifactId: 'art_transcript_123' }, + ]); + }); + + it('note artifact conforms to @bytelyst/events NoteArtifactEnvelopeSchema', () => { + const generated = buildPhase1NoteImport({ + transcriptArtifact: TRANSCRIPT_ARTIFACT, + transcriptCaptureEvent: TRANSCRIPT_CAPTURE_EVENT, + workspaceId: 'ws_phase1', + userId: 'user_saravana', + now: '2026-04-03T18:17:00.000Z', + }); + + const result = NoteArtifactEnvelopeSchema.safeParse(generated.ecosystemNoteArtifact); + expect(result.success).toBe(true); + }); + + it('artifact.created event conforms to @bytelyst/events ArtifactCreatedEventSchema', () => { + const generated = buildPhase1NoteImport({ + transcriptArtifact: TRANSCRIPT_ARTIFACT, + transcriptCaptureEvent: TRANSCRIPT_CAPTURE_EVENT, + workspaceId: 'ws_phase1', + userId: 'user_saravana', + now: '2026-04-03T18:17:00.000Z', + }); + + const result = ArtifactCreatedEventSchema.safeParse(generated.createdEvent); + expect(result.success).toBe(true); + }); + + it('artifact.linked event conforms to @bytelyst/events ArtifactLinkedEventSchema', () => { + const generated = buildPhase1NoteImport({ + transcriptArtifact: TRANSCRIPT_ARTIFACT, + transcriptCaptureEvent: TRANSCRIPT_CAPTURE_EVENT, + workspaceId: 'ws_phase1', + userId: 'user_saravana', + now: '2026-04-03T18:17:00.000Z', + }); + + const result = ArtifactLinkedEventSchema.safeParse(generated.linkedEvent); + expect(result.success).toBe(true); + }); + + it('persists artifacts and events to disk and populates index files', async () => { + const loaded = await loadLatestTranscriptArtifact(root); + const captureEvent = await loadLatestTranscriptCaptureEvent(root); const generated = buildPhase1NoteImport({ transcriptArtifact: loaded, - transcriptCaptureEvent, + transcriptCaptureEvent: captureEvent, workspaceId: 'ws_phase1', userId: 'user_saravana', now: '2026-04-03T18:17:00.000Z', @@ -107,36 +270,48 @@ describe('ecosystem phase1 note import', () => { root, }); - const savedNoteArtifact = JSON.parse( + const savedArtifact = JSON.parse( await readFile(join(root, 'artifacts', 'note', `${generated.ecosystemNoteArtifact.id}.json`), 'utf-8') ); + expect(savedArtifact.artifactType).toBe('note'); + expect(savedArtifact.payload.noteFormat).toBe('markdown'); + + const savedCreatedEvent = JSON.parse( + await readFile(join(root, 'events', 'artifact.created', `${generated.createdEvent.eventId}.json`), 'utf-8') + ); + expect(savedCreatedEvent.eventName).toBe('artifact.created'); + const savedLinkedEvent = JSON.parse( await readFile(join(root, 'events', 'artifact.linked', `${generated.linkedEvent.eventId}.json`), 'utf-8') ); - - expect(generated.note.sourceUri).toBe('art_transcript_123'); - expect(generated.ecosystemNoteArtifact.links).toEqual([ - { - relation: 'summarizes', - targetArtifactId: 'art_transcript_123', - }, - ]); - expect(savedNoteArtifact.payload.noteFormat).toBe('markdown'); + expect(savedLinkedEvent.eventName).toBe('artifact.linked'); expect(savedLinkedEvent.payload.targetArtifactId).toBe('art_transcript_123'); - expect(savedLinkedEvent.payload.relation).toBe('summarizes'); - expect(generated.createdEvent.trace.causationId).toBe('evt_capture_123'); - expect(generated.createdEvent.trace.parentEventId).toBe('evt_capture_123'); - expect(savedLinkedEvent.trace.causationId).toBe(generated.createdEvent.eventId); - const savedCreatedEventIndex = JSON.parse( + const noteIndex = JSON.parse(await readFile(join(root, 'indexes', 'latest-note.json'), 'utf-8')); + expect(noteIndex.id).toBe(generated.ecosystemNoteArtifact.id); + + const createdEventIndex = JSON.parse( await readFile(join(root, 'indexes', 'latest-note-created-event.json'), 'utf-8') ); - const savedLinkedEventIndex = JSON.parse( + expect(createdEventIndex.eventId).toBe(generated.createdEvent.eventId); + + const linkedEventIndex = JSON.parse( await readFile(join(root, 'indexes', 'latest-note-linked-event.json'), 'utf-8') ); - expect(savedCreatedEventIndex.eventId).toBe(generated.createdEvent.eventId); - expect(savedLinkedEventIndex.eventId).toBe(generated.linkedEvent.eventId); + expect(linkedEventIndex.eventId).toBe(generated.linkedEvent.eventId); + }); - await rm(root, { recursive: true, force: true }); + it('works without upstream capture event (graceful degradation)', () => { + const generated = buildPhase1NoteImport({ + transcriptArtifact: TRANSCRIPT_ARTIFACT, + transcriptCaptureEvent: null, + workspaceId: 'ws_phase1', + userId: 'user_saravana', + now: '2026-04-03T18:17:00.000Z', + }); + + expect(generated.createdEvent.trace.causationId).toBeNull(); + expect(generated.createdEvent.trace.parentEventId).toBeNull(); + expect(generated.note.sourceUri).toBe('art_transcript_123'); }); }); diff --git a/backend/src/server.test.ts b/backend/src/server.test.ts index aae0ff4..dff1638 100644 --- a/backend/src/server.test.ts +++ b/backend/src/server.test.ts @@ -29,6 +29,7 @@ vi.mock('./modules/note-relationships/routes.js', () => ({ noteRelationshipRoute 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('./lib/cosmos-init.js', () => ({ initCosmosIfNeeded: initCosmosIfNeededMock })); vi.mock('./lib/datastore.js', () => ({ initDatastore: initDatastoreMock })); vi.mock('./lib/config.js', () => ({ @@ -65,7 +66,7 @@ describe('server bootstrap', () => { expect(initDatastoreMock).toHaveBeenCalledOnce(); expect(createServiceAppMock).toHaveBeenCalledOnce(); expect(registerOptionalJwtContextMock).toHaveBeenCalledOnce(); - expect(appMock.register).toHaveBeenCalledTimes(7); + expect(appMock.register).toHaveBeenCalledTimes(8); expect(startServiceMock).toHaveBeenCalledWith(appMock, { port: 4016, host: '0.0.0.0' }); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 507befd..b8595cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@bytelyst/errors': specifier: ^0.1.0 version: 0.1.0 + '@bytelyst/events': + specifier: file:/tmp/bytelyst-events-0.1.0.tgz + version: file:../../../../../tmp/bytelyst-events-0.1.0.tgz(zod@3.25.76) '@bytelyst/fastify-auth': specifier: ^0.1.0 version: 0.1.0(fastify@5.7.4)(jose@6.2.2) @@ -969,6 +972,12 @@ packages: '@bytelyst/errors@0.1.0': resolution: {integrity: sha512-hE4sHwmQUDGZYDdo3w7VuRdVfuaXgEcG2f0KD0ZLJF+EgfRmDV3IevD1ubPsJIIZxMu8brK8zZOvPohhsMsYdw==, tarball: http://localhost:3300/api/packages/bytelyst/npm/%40bytelyst%2Ferrors/-/0.1.0/errors-0.1.0.tgz} + '@bytelyst/events@file:../../../../../tmp/bytelyst-events-0.1.0.tgz': + resolution: {integrity: sha512-9HmjfrDMmR63UHVbuaruIPEbALWwApcdH/lfeOiF6W+pFpd6FV3yrnLh04gKMzxBOWyGzysH+vrGI7Xnt4PpnQ==, tarball: file:../../../../../tmp/bytelyst-events-0.1.0.tgz} + version: 0.1.0 + peerDependencies: + zod: ^3.0.0 + '@bytelyst/extraction@0.1.0': resolution: {integrity: sha512-Pzb7T30ig2iZ10hnVM6sjLkKEspVo1j3PgTCERrevBVMbGL7NpBtn/rTwiMVghzj6Q8wDTKwn/q1ZkurzAuF8A==, tarball: http://localhost:3300/api/packages/bytelyst/npm/%40bytelyst%2Fextraction/-/0.1.0/extraction-0.1.0.tgz} peerDependencies: @@ -1025,6 +1034,9 @@ packages: '@bytelyst/platform-client@0.1.0': resolution: {integrity: sha512-KmlxcYp4mFuhdTCj8QT+1QGXtk1ViM1hGqGrO7qMhy+IEnjFHsqMxboArUpR/ZTqWNXa1SrH4w7ws29xtjUO1w==, tarball: http://localhost:3300/api/packages/bytelyst/npm/%40bytelyst%2Fplatform-client/-/0.1.0/platform-client-0.1.0.tgz} + '@bytelyst/queue@0.1.0': + resolution: {integrity: sha512-G+UrRxn35/CEA7R5bi8hg846I4Mu9/CvLoP3pGLK2XcMbMNGL4nka+Pz1m10yDsPH2s8wr1ML7wdwg/xvbHOAA==, tarball: http://localhost:3300/api/packages/bytelyst/npm/%40bytelyst%2Fqueue/-/0.1.0/queue-0.1.0.tgz} + '@bytelyst/react-auth@0.1.1': resolution: {integrity: sha512-nXLXh38/nTmIOYKGra0kvzh0AWSU/xhOdVdIZR2ndkhIxi+YqUb6tUoO35hHp+rVeneOm/UhKzOFcmwbvQOndg==, tarball: http://localhost:3300/api/packages/bytelyst/npm/%40bytelyst%2Freact-auth/-/0.1.1/react-auth-0.1.1.tgz} peerDependencies: @@ -7159,6 +7171,11 @@ snapshots: '@bytelyst/errors@0.1.0': {} + '@bytelyst/events@file:../../../../../tmp/bytelyst-events-0.1.0.tgz(zod@3.25.76)': + dependencies: + '@bytelyst/queue': 0.1.0 + zod: 3.25.76 + '@bytelyst/extraction@0.1.0(@bytelyst/api-client@0.1.0)': dependencies: '@bytelyst/api-client': 0.1.0 @@ -7197,6 +7214,8 @@ snapshots: '@bytelyst/platform-client@0.1.0': {} + '@bytelyst/queue@0.1.0': {} + '@bytelyst/react-auth@0.1.1(react@19.2.0)': dependencies: '@bytelyst/api-client': 0.1.0