You are here. See how this question connects to other ideas.
Select a node to open its page · Expand to read within the map
One question
Check a read-only provider
Mount a report at /work and check it with shared tests and an actual read.
Run against an existing development workspace
Expose one fixed report at /work/report. This is neither persistent storage nor a mutable report. You need Bun and resolvable @aigne/afs and @aigne/afs-testing packages. The example was verified against local Arc workspace build artifacts at 2.0.0-beta.54; a fresh public-registry installation was not tested.
This lab requires an existing, built Arc development checkout. The required @aigne/afs@2.0.0-beta.54 version was not found on public npm; these steps are not a fresh-user installation guide.
Create an empty afs-provider-example directory, save this page’s two JavaScript blocks as setup.mjs and report.test.js, then enter that directory. Replace the path below with your own built Arc checkout, whose two packages must be at 2.0.0-beta.54:
bun setup.mjs /path/to/your/built/arc-checkout
bun test report.test.jsSave this preparation script as setup.mjs:
import { readFile, mkdir, symlink, lstat } from 'node:fs/promises';
import { resolve, join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const checkout = process.argv[2];
if (!checkout) throw new Error('Usage: bun setup.mjs /path/to/your/built/arc-checkout');
const root = dirname(fileURLToPath(import.meta.url));
const modules = join(root, 'node_modules', '@aigne');
await mkdir(modules, { recursive: true });
for (const [name, folder] of [['afs', 'core'], ['afs-testing', 'testing']]) {
const target = resolve(checkout, 'packages', folder);
const pkg = JSON.parse(await readFile(join(target, 'package.json'), 'utf8'));
if (pkg.version !== '2.0.0-beta.54') {
throw new Error(`Expected ${name} 2.0.0-beta.54; found ${pkg.version}`);
}
await readFile(join(target, 'dist', 'index.mjs'));
const link = join(modules, name);
const exists = await lstat(link).then(() => true, () => false);
if (exists) throw new Error(`${link} already exists; use a fresh example folder.`);
await symlink(target, link, 'dir');
}
console.log('Ready. Run: bun test report.test.js');The setup script checks package versions and build artifacts, then links dependencies inside the example directory without editing Arc source. Without that environment, read the code and path behavior as a worked example and continue through the conceptual lessons.
Save this as report.test.js inside a project that resolves both packages, then run bun test report.test.js. Explicit decorator registration calls serve the role of the corresponding @Read, @List and related declarations. This is plain JavaScript and needs no TypeScript decorator compiler configuration.
import { expect, test } from 'bun:test';
import { AFS, AFSBaseProvider, Read, List, Explain, Meta } from '@aigne/afs';
import { runProviderTests } from '@aigne/afs-testing';
class ReportProvider extends AFSBaseProvider {
name = 'report-example';
async root() {
return { id: 'root', path: '/', meta: { childrenCount: 1 } };
}
async report() {
return {
id: 'report', path: '/report', meta: { kind: 'afs:file' },
content: 'Prepare the keynote',
};
}
async children() { return { data: [await this.report()] }; }
async noChildren() { return { data: [] }; }
async metadata(ctx) {
return {
id: 'meta', path: ctx.path, meta: { kind: 'afs:metadata' },
content: { title: 'Keynote report' },
};
}
async explainRoot() {
return {
content: 'Read /report for the keynote title. This example is read-only.',
format: 'text',
};
}
async capabilities() {
return {
id: '.capabilities', path: '/.meta/.capabilities',
content: {
schemaVersion: 1, provider: this.name,
operations: {
read: true, list: true, explain: true, stat: false,
write: false, delete: false, search: false, exec: false,
},
},
};
}
}
const routes = [
[Read, '/', 'root'],
[Read, '/report', 'report'],
[List, '/', 'children'],
[List, '/report', 'noChildren'],
[Meta, '/', 'metadata'],
[Meta, '/report', 'metadata'],
[Read, '/.meta/.capabilities', 'capabilities'],
[Explain, '/', 'explainRoot'],
];
for (const [decorator, path, method] of routes) {
decorator(path)(
ReportProvider.prototype, method,
Object.getOwnPropertyDescriptor(ReportProvider.prototype, method),
);
}
runProviderTests({
name: 'ReportProvider learning example',
createProvider: () => new ReportProvider(),
playground: async () => ({
name: 'Report example', mountPath: '/work',
provider: new ReportProvider(), cleanup: async () => {},
}),
structure: {
root: { name: '', children: [
{ name: 'report', content: 'Prepare the keynote' },
] },
},
});
test('mount prefix is applied exactly once', async () => {
const afs = new AFS();
await afs.mount(new ReportProvider(), '/work');
const result = await afs.read('/work/report');
expect(result.data.path).toBe('/work/report');
expect(result.data.content).toBe('Prepare the keynote');
});
test('a second mount changes the caller path, not the provider path', async () => {
const afs = new AFS();
await afs.mount(new ReportProvider(), '/reviews');
const result = await afs.read('/reviews/report');
expect(result.data.path).toBe('/reviews/report');
expect(result.data.content).toBe('Prepare the keynote');
await expect(afs.read('/reviews/missing')).rejects.toMatchObject({
code: 'AFS_NOT_FOUND',
});
});Read it in four parts
- The provider returns its own paths, such as
/report. It does not know the external/workmount location. - Metadata describes entries; the capability manifest declares supported operations. An empty child list means the report has no children.
runProviderTestschecks real provider instances against known report data. Theplaygroundfunction prepares a mountable instance for development inspection.- The final test actually mounts and reads
/work/report, checkingdata.pathand content. Importing a class or counting tests cannot replace this check.
The recorded run produced 222 passes, 6 skips and no failures. The skipped cases concern subscriptions. Shared suites also contain capability-dependent early returns, so the count does not mean 222 capabilities were implemented. This evidence covers the current read-only example, not persistence, conditional writes or cross-process behavior.
Change one thing
Change the content returned by report() while keeping the expected text: the final read test should fail. Then change the mount to /reviews and update the read and expected path to /reviews/report. The provider-relative /report stays unchanged. Reading /reviews/missing should produce a not-found error rather than an empty report.
The registration [Read, "/report", "report"] means “call report() when reading the provider-relative /report.” A handler returns the entry itself; afs.read() returns an operation result, so the caller uses result.data.path and result.data.content. The last test also catches a missing-resource exception and checks its AFS_NOT_FOUND code.
Check your understanding
Does passing establish write support?
No. This example implements reads, listing, metadata and explanations. Its manifest declares no write support; inspect what the assertions actually exercise.
Continue along a learning path
- Build and check a providerStep 5 of 6