UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

266 lines • 10.5 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { RAMIndexCacheStore } from "./cache/index/ram.js"; import { MountMode, PathSpec } from "./types.js"; import { BaseVFS } from "./vfs/base.js"; import { RAMVFS } from "./vfs/ram/ram.js"; import { stripSlash } from "./utils/slash.js"; import { makeResolveGlob } from "./utils/glob_walk.js"; import { runWithMountGate } from "./context/session_context.js"; import { MountEntry } from "./workspace/mount/mount.js"; import { Workspace } from "./workspace/workspace/workspace.js"; import { Files } from "./workspace/files.js"; import { NamespaceReader } from "./workspace/dispatcher/reader.js"; import { enotsup } from "./errors/fs.js"; import { IOResult } from "./io/types.js"; /** A RAM accessor whose store holds at most `cap` bytes. */ export function capped(cap, files = {}) { const vfs = new RAMVFS({ capacity: cap }); for (const [key, text] of Object.entries(files)) vfs.store.files.set(key, new TextEncoder().encode(text)); if (vfs.usage === null) throw new Error('a capped store has a usage'); return [vfs.accessor, vfs.usage]; } /** A RAM mount path for core calls: `/a/b` keyed as the store keys it. */ export function ramPath(virtual) { return new PathSpec({ virtual, directory: virtual, vfsPath: stripSlash(virtual) }); } /** The bytes a RAM accessor's table holds. */ export function tableBytes(accessor) { let total = 0; for (const data of accessor.store.files.values()) total += data.byteLength; return total; } /** * A VFS called the way a mount calls it: through the dispatcher, one index * store per instance, so a driver can be exercised without a Workspace. A * verb the VFS does not answer is a `no op registered` error, the same * answer a mount gives. */ class MountedVFS { vfs; index; mount; constructor(vfs) { this.vfs = vfs; this.index = new RAMIndexCacheStore({ ttl: vfs.indexTtl }); this.mount = new MountEntry({ prefix: '/', vfs, mode: MountMode.WRITE }); } /** Whether the dispatcher answers `name` on this VFS. */ has(name) { return this.mount.answers(name); } /** Call op `name` on `path`; `index` defaults to this instance's store. */ call(name, path, args = [], kwargs = {}) { return this.mount.callKeyed(name, path, args, { index: this.index, ...kwargs }); } read(path, kwargs = {}) { return this.call('read', path, [], kwargs); } readdir(path) { return this.call('readdir', path); } stat(path) { return this.call('stat', path); } /** Expand a pattern over this VFS's listings, as a shell glob walks them. */ glob(path) { return makeResolveGlob((p) => this.readdir(p), this.vfs.walkLimit)([path]); } async write(path, data) { await this.call('write', path, [data]); } async append(path, data) { await this.call('append', path, [data]); } async create(path) { await this.call('create', path); } async mkdir(path, parents = false) { await this.call('mkdir', path, [], parents ? { parents: true } : {}); } async unlink(path) { await this.call('unlink', path); } async rmdir(path) { await this.call('rmdir', path); } async rename(src, dst) { await this.call('rename', src, [dst]); } async truncate(path, length) { await this.call('truncate', path, [length]); } } const TABLES = new WeakMap(); /** The dispatcher of `vfs`, bound once per instance so its index store persists across calls. */ export function ops(vfs) { let table = TABLES.get(vfs); if (table === undefined) { table = new MountedVFS(vfs); TABLES.set(vfs, table); } return table; } /** * Have this one `vfs` render reads of `filetype` with `fn`, which takes * `read`'s arguments, window included, as a renderer method does. */ export function render(vfs, filetype, fn) { const name = 'render' + filetype.replaceAll('.', '_'); Object.assign(vfs, { [name]: fn, renderers: { ...vfs.renderers, [filetype]: name } }); return vfs; } /** * A `cls` VFS over `accessor`, its constructor skipped: the class's functions * with the base facts, so a test drives a backend's functions with an * accessor of its own. A fact the class declares is not set; pass the ones a * test relies on. */ export function vfsOver(cls, accessor, facts = {}) { const vfs = new BaseVFS({ accessor }); Object.setPrototypeOf(vfs, cls.prototype); return Object.assign(vfs, facts); } /** * Files whose dispatcher answers each op `answers` names, and refuses any * other the way a mount without that function does (ENOTSUP), except * `before_full_walk`, which passes as it does on a mount with no * `walkLimit`: a command's files over a fake backend, for a test that * drives a builder alone. `rows` * is the mount table (`readers`), `links` the namespace's link table. Mirrors * Python's `tests/fixtures/answers.Answers`. */ export function answeringFiles(answers, options = {}, rows = [], links = null) { const dispatch = async (name, path, args = [], kwargs = {}) => { const answer = answers[name]; if (answer === undefined && name === 'before_full_walk') return [null, new IOResult()]; if (answer === undefined) throw enotsup('fake', name, path); return [await answer(path, args, kwargs), new IOResult()]; }; return new Files(dispatch, rows.length === 0 && links === null ? null : readers(rows, links), null, { ...options, ledger: false }); } /** * A namespace's readers over a fixed mount table and link table, for files * that stand outside a workspace; a prefix alone stands for a RAM mount * there. Mirrors Python's `tests.fixtures.readers`. */ export function readers(rows = [], links = null, visibility = null) { const table = rows.map((row) => typeof row === 'string' ? { prefix: row, kind: 'ram', mode: MountMode.WRITE } : row); return (sessionId) => new NamespaceReader(links, () => table, sessionId, () => visibility); } const WORKSPACES = new WeakMap(); /** * Run `fn` the way a command on `vfs` runs: `vfs` mounted alone at `prefix` * in a workspace of its own (one per VFS), `fn` handed the workspace's files, * which a command reads and writes through (`ctx.vfs`), inside the mount's * command scope. */ export function onMount(vfs, fn, prefix = '/') { let ws = WORKSPACES.get(vfs); if (ws === undefined) { ws = new Workspace({ [prefix]: vfs }, { mode: MountMode.WRITE }); WORKSPACES.set(vfs, ws); } const files = ws.vfs; const at = prefix.endsWith('/') ? prefix : `${prefix}/`; return runWithMountGate(at, MountMode.WRITE, async () => fn(files)); } /** * A write context whose store holds `s0` and records what it keeps, for a * refusal test where the store's version, the write's and the one sent all * differ. Mirrors Python's `tests/fixtures/write_context.KeptVersions`. */ export function keptVersions(vfs) { const kept = []; const context = { vfs, conditions: ['put', 'copy', 'delete'], readVersion: () => Promise.resolve('s0'), readVersions: (paths) => Promise.resolve(paths.map(() => 's0')), drop: () => Promise.resolve(), keep: (_path, version) => { kept.push(version); return Promise.resolve(); }, }; return { context, kept }; } /** * Record every call the workspace's dispatcher serves from now on, by op * name and virtual path: where a line's own I/O is judged. `before` runs * ahead of each call, so a test can hold one mid-command. Mirrors the * Python `dispatched` test fixture. */ export function dispatched(ws, before) { const seen = []; const dispatcher = ws.dispatcher; // Every view the dispatcher makes for a session or a caller is watched. const watch = (bound) => { const target = bound; const real = target.dispatchAdmitted; target.dispatchAdmitted = async (name, path, args, kwargs, report) => { seen.push([name, path.virtual]); if (before !== undefined) await before(name, path); return real(name, path, args, kwargs, report); }; const forCaller = target.forCaller.bind(bound); target.forCaller = (caller) => watch(forCaller(caller)); return bound; }; const bind = dispatcher.forSession.bind(dispatcher); dispatcher.forSession = (session) => watch(bind(session)); return seen; } /** * A RAM mount standing in for a remote store: it keeps its reads, and reads a * batch in one call where `batched`, counting both. Mirrors the Python * `BatchedRAM` test fixture. */ export class BatchedRAM extends RAMVFS { batched; cachesReads = true; local = false; reads = []; batches = []; constructor(batched = true) { super(); this.batched = batched; this.store.dirs.add('/'); this.store.dirs.add('/secret'); for (const name of ['a', 'b', 'secret/c']) { this.store.files.set(`/${name}`, new TextEncoder().encode(`${name.slice(-1)}\n`)); } } supports(name) { return name === 'readMany' ? this.batched : super.supports(name); } read(path, handed, offset = 0, size = null) { this.reads.push(path.virtual); return super.read(path, handed, offset, size); } readMany(paths) { this.batches.push(paths.map((path) => path.virtual)); return Promise.resolve(new Map(paths.map((path) => [ path.virtual, this.store.files.get(`/${stripSlash(path.vfsPath)}`) ?? new Uint8Array(), ]))); } } //# sourceMappingURL=test-utils.js.map