你在这里。看看这个问题与其他知识怎样相连。
选择节点前往页面 · 展开后可留在地图中阅读
一个问题
检验一个只读 Provider
把报告挂到 /work,再用共享测试和真实读取检查实现。
在已有开发工作区里运行
目标是把一份固定报告暴露为 /work/report。这不是持久化存储,也不修改报告。你需要 Bun 和可用的 @aigne/afs、@aigne/afs-testing 包;本例在本地 Arc 工作区的 2.0.0-beta.54 构建产物上验证,未验证从公共包仓库全新安装。
本实验面向已有、已构建的 Arc 开发工作区的读者。公共 npm 上没有核实到本例要求的 @aigne/afs@2.0.0-beta.54,因此不要把下面步骤当成面向新用户的一键安装。
建立一个空的 afs-provider-example 目录,把本页两段 JavaScript 分别保存为 setup.mjs 和 report.test.js,然后进入该目录。把命令中的路径换成你自己的 Arc 工作区(其中两个包已构建,且版本为 2.0.0-beta.54):
bun setup.mjs /path/to/your/built/arc-checkout
bun test report.test.js把下面的准备脚本保存为 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');准备脚本检查包版本与构建产物,并只在实验目录里连接依赖,不修改 Arc 源码。没有该开发环境时,可以先阅读代码与路径变化,继续沿知识路线理解契约。
把下面保存为 report.test.js,放在能解析这两个包的项目中,运行 bun test report.test.js。这里显式调用路由装饰器注册函数,作用与对应的 @Read、@List 等声明相同;使用普通 JavaScript,不需要 TypeScript 装饰器编译配置。
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',
});
});从四处读懂它
ReportProvider返回 provider 内部路径,例如/report。它不知道外部挂载位置是/work。- 元数据入口提供节点说明,能力清单明确哪些操作可用。空子列表说明报告没有下级资源。
runProviderTests使用真实 provider 实例和已知报告内容做共同契约检查。playground函数为开发调试准备同一个 provider 的可挂载实例。- 最后一条测试真的挂载并读取
/work/report,检查返回的data.path与正文。只导入类或看到测试数量,都不能替代这一步。
这次运行记录为 222 项通过、6 项跳过、0 项失败;跳过的是订阅通知用例。共享套件还包含按能力返回的检查,因此这个数量不代表实现了 222 种能力。证据限于只读例子的当前版本,不能推断持久化、条件写入或跨进程行为。
改一个地方再观察
把 report() 的正文改掉而保留测试预期,最后的读取测试应失败。再把挂载位置改为 /reviews,同时把读取入口和预期路径改为 /reviews/report;provider 内部的 /report 不需要改。最后尝试读取不存在的 /reviews/missing,应得到路径不存在的错误,而不是空报告。
注册表中的 [Read, "/report", "report"] 表示“读取 provider 内部 /report 时调用 report()”。处理器返回条目本身;afs.read() 返回操作结果,所以调用者通过 result.data.path 和 result.data.content 读取条目。最后一个测试还演示了如何捕获不存在资源的异常,检查错误码 AFS_NOT_FOUND。
检查一下理解
测试通过,是否意味着已经支持写入?
不。这个例子只实现读取、列举、元数据与说明。能力清单明确没有写入;还要看实际断言覆盖什么。
沿着学习路径继续
- 实现并检验一个 provider第 5 / 6 步