@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
266 lines • 10.5 kB
JavaScript
// ========= 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