Add Phase 0 test harness (vitest + ESLint) and fix build config
- Add vitest unit tests for Zustand app store state transitions (6 tests) - Add mock speech-engine WebSocket protocol integration test (spawns server.py --mock on a free port) - Add ESLint 10 flat config with typescript-eslint; add typecheck/lint/test scripts to package.json - Fix pre-existing tsc -b breakage via noEmit in tsconfig.node.json; add dedicated tsconfig.tests.json project for Node-side tests - Track src-tauri/Cargo.lock for reproducible Rust builds; enable protocol-asset Tauri feature - Ignore generated artifacts (*.tsbuildinfo, src-tauri/gen/, emitted vite.config.js/.d.ts); remove stale emitted vite config copies - Update AGENTS.md agent guide (demo scope lock, roadmap, verification commands) - Add HANDOFF.md documenting Phase 0 state and remaining steps for the next agent
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { TranscriptSegment } from '../types';
|
||||
import { useAppStore } from './useAppStore';
|
||||
|
||||
function makeSegment(overrides: Partial<TranscriptSegment> = {}): TranscriptSegment {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
speakerId: 'speaker_1',
|
||||
start: 0,
|
||||
end: 2.4,
|
||||
text: 'Hello world.',
|
||||
final: true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function getMeeting(id: string) {
|
||||
const meeting = useAppStore.getState().meetings.find((m) => m.id === id);
|
||||
if (!meeting) throw new Error(`meeting ${id} not found`);
|
||||
return meeting;
|
||||
}
|
||||
|
||||
describe('useAppStore', () => {
|
||||
it('createMeeting prepends a recording meeting and activates it', () => {
|
||||
const before = useAppStore.getState().meetings.length;
|
||||
const id = useAppStore.getState().createMeeting('Test Meeting');
|
||||
|
||||
const state = useAppStore.getState();
|
||||
expect(id).toBeTruthy();
|
||||
expect(state.activeMeetingId).toBe(id);
|
||||
expect(state.meetings.length).toBe(before + 1);
|
||||
expect(state.meetings[0].id).toBe(id);
|
||||
|
||||
const meeting = getMeeting(id);
|
||||
expect(meeting.title).toBe('Test Meeting');
|
||||
expect(meeting.status).toBe('recording');
|
||||
expect(meeting.durationSeconds).toBe(0);
|
||||
expect(meeting.transcript).toEqual([]);
|
||||
expect(meeting.speakerNames).toEqual({});
|
||||
expect(meeting.localOnly).toBe(true);
|
||||
expect(Number.isNaN(Date.parse(meeting.startedAt))).toBe(false);
|
||||
});
|
||||
|
||||
it('appendSegment adds segments to the right meeting only', () => {
|
||||
const id = useAppStore.getState().createMeeting();
|
||||
const otherId = useAppStore.getState().createMeeting();
|
||||
const segment = makeSegment({ text: 'First line.' });
|
||||
|
||||
useAppStore.getState().appendSegment(id, segment);
|
||||
|
||||
expect(getMeeting(id).transcript).toEqual([segment]);
|
||||
expect(getMeeting(otherId).transcript).toEqual([]);
|
||||
});
|
||||
|
||||
it('appendSegment replaces an existing segment with the same id', () => {
|
||||
const id = useAppStore.getState().createMeeting();
|
||||
const original = makeSegment({ text: 'partial text' });
|
||||
useAppStore.getState().appendSegment(id, original);
|
||||
|
||||
const updated = makeSegment({ ...original, text: 'final text', final: true });
|
||||
useAppStore.getState().appendSegment(id, updated);
|
||||
|
||||
expect(getMeeting(id).transcript).toEqual([updated]);
|
||||
});
|
||||
|
||||
it('renameSpeaker stores the trimmed name and falls back to the speaker id for blank names', () => {
|
||||
const id = useAppStore.getState().createMeeting();
|
||||
|
||||
useAppStore.getState().renameSpeaker(id, 'speaker_1', ' Alice ');
|
||||
expect(getMeeting(id).speakerNames.speaker_1).toBe('Alice');
|
||||
|
||||
useAppStore.getState().renameSpeaker(id, 'speaker_2', ' ');
|
||||
expect(getMeeting(id).speakerNames.speaker_2).toBe('speaker_2');
|
||||
});
|
||||
|
||||
it('finishMeeting commits endedAt, duration, processing status and media info', () => {
|
||||
const id = useAppStore.getState().createMeeting();
|
||||
const beforeFinish = Date.now();
|
||||
|
||||
useAppStore.getState().finishMeeting(id, 'meetings/test/recording.webm', 'video/webm');
|
||||
|
||||
const state = useAppStore.getState();
|
||||
const meeting = getMeeting(id);
|
||||
expect(meeting.status).toBe('processing');
|
||||
expect(meeting.endedAt).toBeTruthy();
|
||||
expect(Number.isNaN(Date.parse(meeting.endedAt!))).toBe(false);
|
||||
// A just-created meeting has ~0 elapsed time; the store clamps to at least 1 second.
|
||||
expect(meeting.durationSeconds).toBeGreaterThanOrEqual(1);
|
||||
expect(Date.parse(meeting.endedAt!) - beforeFinish).toBeLessThan(5_000);
|
||||
expect(meeting.mediaPath).toBe('meetings/test/recording.webm');
|
||||
expect(meeting.mediaMime).toBe('video/webm');
|
||||
expect(state.activeMeetingId).toBeUndefined();
|
||||
expect(state.processingMessage).toBe('Processing (The job). Please wait a moment');
|
||||
});
|
||||
|
||||
it('setSettings merges the patch without clobbering other keys', () => {
|
||||
const before = useAppStore.getState().settings;
|
||||
|
||||
useAppStore.getState().setSettings({ theme: 'dark', overlayLines: 6 });
|
||||
|
||||
const settings = useAppStore.getState().settings;
|
||||
expect(settings.theme).toBe('dark');
|
||||
expect(settings.overlayLines).toBe(6);
|
||||
for (const key of Object.keys(before) as Array<keyof typeof before>) {
|
||||
if (key !== 'theme' && key !== 'overlayLines') {
|
||||
expect(settings[key]).toBe(before[key]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
38
Wireframes/Desktop/MeetVault-Desktop-Demo/src/test/setup.ts
Normal file
38
Wireframes/Desktop/MeetVault-Desktop-Demo/src/test/setup.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// Vitest setup: provide a minimal in-memory localStorage so zustand/persist
|
||||
// works under the Node test environment (no DOM available).
|
||||
|
||||
class MemoryStorage implements Storage {
|
||||
private store = new Map<string, string>();
|
||||
|
||||
get length(): number {
|
||||
return this.store.size;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.store.clear();
|
||||
}
|
||||
|
||||
getItem(key: string): string | null {
|
||||
return this.store.has(key) ? (this.store.get(key) as string) : null;
|
||||
}
|
||||
|
||||
key(index: number): string | null {
|
||||
return Array.from(this.store.keys())[index] ?? null;
|
||||
}
|
||||
|
||||
setItem(key: string, value: string): void {
|
||||
this.store.set(key, String(value));
|
||||
}
|
||||
|
||||
removeItem(key: string): void {
|
||||
this.store.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof globalThis.localStorage === 'undefined') {
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
value: new MemoryStorage(),
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user