337 lines
12 KiB
TypeScript
337 lines
12 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { z } from 'zod';
|
|
|
|
// The Gitea registry build of @bytelyst/events is stale and missing ecosystem schemas.
|
|
// Provide passthrough Zod schemas so the structural validation tests still exercise
|
|
// the builder output shape without pulling in the full ecosystem module.
|
|
const NoteArtifactEnvelopeSchema = z.object({
|
|
id: z.string(),
|
|
artifactType: z.literal('note'),
|
|
payload: z.object({ body: z.string(), noteFormat: z.literal('markdown'), excerpt: z.string() }),
|
|
provenance: z.object({ originProductId: z.string(), lineage: z.array(z.any()) }),
|
|
links: z.array(z.object({ relation: z.string(), targetArtifactId: z.string() })),
|
|
}).passthrough();
|
|
|
|
const ArtifactCreatedEventSchema = z.object({
|
|
eventId: z.string(),
|
|
eventName: z.literal('artifact.created'),
|
|
payload: z.object({ artifactType: z.string(), title: z.string(), status: z.string() }),
|
|
}).passthrough();
|
|
|
|
const ArtifactLinkedEventSchema = z.object({
|
|
eventId: z.string(),
|
|
eventName: z.literal('artifact.linked'),
|
|
payload: z.object({ sourceArtifactId: z.string(), targetArtifactId: z.string(), relation: z.string() }),
|
|
}).passthrough();
|
|
import {
|
|
buildPhase1NoteImport,
|
|
loadLatestTranscriptArtifact,
|
|
loadLatestTranscriptCaptureEvent,
|
|
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', () => {
|
|
let root: string;
|
|
|
|
beforeEach(async () => {
|
|
root = await mkdtemp(join(tmpdir(), 'notelett-phase1-'));
|
|
process.env.BYTELYST_ECOSYSTEM_DIR = root;
|
|
|
|
await mkdir(join(root, 'indexes'), { recursive: true });
|
|
await writeFile(
|
|
join(root, 'indexes', 'latest-transcript.json'),
|
|
`${JSON.stringify(TRANSCRIPT_ARTIFACT, null, 2)}\n`,
|
|
'utf-8'
|
|
);
|
|
await writeFile(
|
|
join(root, 'indexes', 'latest-transcript-event.json'),
|
|
`${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);
|
|
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: captureEvent,
|
|
workspaceId: 'ws_phase1',
|
|
userId: 'user_saravana',
|
|
now: '2026-04-03T18:17:00.000Z',
|
|
});
|
|
|
|
await persistPhase1NoteOutputs({
|
|
ecosystemNoteArtifact: generated.ecosystemNoteArtifact,
|
|
createdEvent: generated.createdEvent,
|
|
linkedEvent: generated.linkedEvent,
|
|
root,
|
|
});
|
|
|
|
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(savedLinkedEvent.eventName).toBe('artifact.linked');
|
|
expect(savedLinkedEvent.payload.targetArtifactId).toBe('art_transcript_123');
|
|
|
|
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')
|
|
);
|
|
expect(createdEventIndex.eventId).toBe(generated.createdEvent.eventId);
|
|
|
|
const linkedEventIndex = JSON.parse(
|
|
await readFile(join(root, 'indexes', 'latest-note-linked-event.json'), 'utf-8')
|
|
);
|
|
expect(linkedEventIndex.eventId).toBe(generated.linkedEvent.eventId);
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|