Andrey Epifancev b126e4bf8b feat(core): save captured audio and initial note; in-memory port fakes
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:32:34 +04:00

91 lines
2.6 KiB
TypeScript

import { readVoiceFields } from '../src/core/note';
import type {
Env,
ListedNote,
NoteStore,
RecordingStore,
TranscribeResult,
Transcriber,
} from '../src/core/ports';
export class MemoryRecordings implements RecordingStore {
files = new Map<string, ArrayBuffer>();
async write(path: string, data: ArrayBuffer): Promise<void> {
this.files.set(path, data.slice(0));
}
async read(path: string): Promise<ArrayBuffer> {
const data = this.files.get(path);
if (!data) throw new Error(`no such file: ${path}`);
return data.slice(0);
}
}
export class MemoryNotes implements NoteStore {
files = new Map<string, string>();
// Имитация отстающего metadataCache: если задано, list() возвращает это.
staleList: ListedNote[] | null = null;
listCalls = 0;
async exists(path: string): Promise<boolean> {
return this.files.has(path);
}
async create(path: string, content: string): Promise<void> {
if (this.files.has(path)) throw new Error(`already exists: ${path}`);
this.files.set(path, content);
}
async list(): Promise<ListedNote[]> {
this.listCalls++;
if (this.staleList) return this.staleList;
return [...this.files]
.map(([path, content]) => ({ path, frontmatter: readVoiceFields(content) as Record<string, unknown> }))
.filter((note) => 'voice-id' in note.frontmatter);
}
async process(path: string, fn: (content: string) => string): Promise<boolean> {
const content = this.files.get(path);
if (content === undefined) return false;
this.files.set(path, fn(content));
return true;
}
}
export type Step =
| TranscribeResult
| Error
| ((audio: ArrayBuffer, mime: string) => Promise<TranscribeResult>);
export class ScriptedTranscriber implements Transcriber {
calls: { audio: ArrayBuffer; mime: string; signal: AbortSignal }[] = [];
constructor(public steps: Step[] = []) {}
async transcribe(audio: ArrayBuffer, mime: string, signal: AbortSignal): Promise<TranscribeResult> {
this.calls.push({ audio, mime, signal });
const step = this.steps.shift();
if (step === undefined) throw new Error('ScriptedTranscriber: no more steps');
if (step instanceof Error) throw step;
if (typeof step === 'function') return step(audio, mime);
return step;
}
}
export class TestEnv implements Env {
constructor(
public time = new Date('2026-09-27T11:15:03.000Z'),
public deviceId = 'test-device',
public random: () => number = () => 0,
) {}
now(): Date {
return new Date(this.time);
}
advance(ms: number): void {
this.time = new Date(this.time.getTime() + ms);
}
}