/** * Test-only in-memory fakes for the vault stack. Imported ONLY by `*.test.ts`. * * These are fakes (real in-memory state), not mocks: assertions are made on * their state — the doc store, on-disk files, the snapshot — rather than on * call order. The repo and the doc session share a single `DocStore`, so edits * made through a session are visible to the engine via the repo and vice-versa, * matching the production relationship where `acquireDocSession` and * `HabitatRepoImpl` wrap the same underlying record. */ import * as nodeFs from 'fs'; import * as Automerge from '@automerge/automerge'; import type { AutomergeDocSession } from '../../habitat/automergeDocSession'; import type { AutomergeDocSchema } from '../../editors/automerge/automergeEditorBinding'; import type { FilesystemApi, FsDirent } from '../../filesystem/types'; import type { DirectoryDocument, FileDocument, HabitatDocHandle, HabitatRepo, HabitatUri, } from '../types'; // --------------------------------------------------------------------------- // Shared in-memory document store // --------------------------------------------------------------------------- export type DocStore = Map>; // --------------------------------------------------------------------------- // Node-native FilesystemApi — a fake of the IPC-backed VaultFilesystemApi that // writes real files to disk so tests can assert on their contents. // --------------------------------------------------------------------------- export class NodeFilesystemApi implements FilesystemApi { async readdir(dirPath: string): Promise { const entries = nodeFs.readdirSync(dirPath, { withFileTypes: true }); return entries.map((e) => ({ name: e.name, isDirectory: e.isDirectory(), isFile: e.isFile(), })); } async readFile(filePath: string): Promise { return nodeFs.readFileSync(filePath, 'utf-8'); } async writeFile(filePath: string, content: string): Promise { nodeFs.writeFileSync(filePath, content, 'utf-8'); } async fileExists(filePath: string): Promise { return nodeFs.existsSync(filePath); } async mkdir(dirPath: string): Promise { nodeFs.mkdirSync(dirPath, { recursive: true }); } async rename(oldPath: string, newPath: string): Promise { nodeFs.renameSync(oldPath, newPath); } async deletePath(filePath: string): Promise { nodeFs.rmSync(filePath, { recursive: true, force: true }); } async copyFile(src: string, dest: string): Promise { nodeFs.copyFileSync(src, dest); } } // --------------------------------------------------------------------------- // In-memory HabitatRepo backed by real Automerge over a shared DocStore // --------------------------------------------------------------------------- class InMemoryDocHandle implements HabitatDocHandle { constructor( private uri: HabitatUri, private store: DocStore, ) {} private get current(): Automerge.Doc { return this.store.get(this.uri) as Automerge.Doc; } doc(): Automerge.Doc { return this.current; } heads(): string[] { return Automerge.getHeads(this.current); } change(fn: (d: T) => void): void { const next = Automerge.change( this.current, fn as Automerge.ChangeFn, ); this.store.set(this.uri, next); } changeAt(heads: string[], fn: (d: T) => void): void { const result = Automerge.changeAt( this.current, heads, fn as Automerge.ChangeFn, ); if (result.newHeads !== null) { this.store.set(this.uri, result.newDoc); } } view(heads: string[]): Automerge.Doc { return Automerge.view(this.current, heads) as Automerge.Doc; } on(_event: 'change', _cb: () => void): () => void { return () => {}; } } export class InMemoryHabitatRepo implements HabitatRepo { private counter = 0; constructor(public readonly store: DocStore = new Map()) {} private makeUri(collection: string): HabitatUri { return `at://did:plc:test/${collection}/${++this.counter}` as HabitatUri; } private handle(uri: HabitatUri): InMemoryDocHandle { return new InMemoryDocHandle(uri, this.store); } async find(uri: HabitatUri): Promise> { if (!this.store.has(uri)) { throw new Error(`[InMemoryHabitatRepo] doc not found: ${uri}`); } return this.handle(uri); } async createDirectory( initial: DirectoryDocument, ): Promise<{ uri: HabitatUri; handle: HabitatDocHandle }> { const uri = this.makeUri('network.habitat.vault.directory'); this.store.set( uri, Automerge.from(initial as unknown as Record), ); return { uri, handle: this.handle(uri) }; } async createFile( initial: FileDocument, ): Promise<{ uri: HabitatUri; handle: HabitatDocHandle }> { const uri = this.makeUri('network.habitat.vault.file'); this.store.set( uri, Automerge.from(initial as unknown as Record), ); return { uri, handle: this.handle(uri) }; } async delete(uri: HabitatUri): Promise { this.store.delete(uri); } /** Test helper: directly read a doc from the store. */ getDoc(uri: HabitatUri): Automerge.Doc | undefined { return this.store.get(uri) as Automerge.Doc | undefined; } /** Test helper: count docs in the store. */ docCount(): number { return this.store.size; } } // --------------------------------------------------------------------------- // Fake live document session backed by the same DocStore // --------------------------------------------------------------------------- export class FakeDocSession implements AutomergeDocSession { released = false; readonly provider = null; readonly node = null; readonly ownerRecord = {} as AutomergeDocSession['ownerRecord']; readonly loadPromise = Promise.resolve(); private listeners = new Set<() => void>(); constructor( public readonly uri: string, private store: DocStore, ) {} get doc(): Automerge.Doc { return this.store.get( this.uri as HabitatUri, ) as Automerge.Doc; } getTitle = (): string => { const doc = this.doc as { name?: unknown } | undefined; return typeof doc?.name === 'string' ? doc.name : ''; }; applyChange = (fn: (doc: AutomergeDocSchema) => void): void => { this.store.set( this.uri as HabitatUri, Automerge.change(this.doc, fn as Automerge.ChangeFn), ); this.notify(); }; changeAt = (heads: string[], fn: (doc: AutomergeDocSchema) => void): void => { const result = Automerge.changeAt( this.doc, heads, fn as Automerge.ChangeFn, ); if (result.newHeads !== null) { this.store.set(this.uri as HabitatUri, result.newDoc); this.notify(); } }; subscribe = (cb: () => void): (() => void) => { this.listeners.add(cb); return () => this.listeners.delete(cb); }; resync = async (): Promise => {}; release = (): void => { this.released = true; }; private notify(): void { for (const listener of this.listeners) listener(); } } /** * Build a fake session factory over a shared `DocStore`. One `FakeDocSession` * is reused per uri so tests can read its state after opening a document. */ export function makeFakeSessions(store: DocStore) { const sessions = new Map(); return { acquire: (uri: string): FakeDocSession => { let session = sessions.get(uri); if (!session) { session = new FakeDocSession(uri, store); sessions.set(uri, session); } return session; }, get: (uri: string): FakeDocSession | undefined => sessions.get(uri), }; }