wiki-viewer 1.4.3 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +5 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/routes-manifest.json +34 -0
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/agent/activity/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js +8 -7
- package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js +17 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js +15 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route.js +15 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route.js +15 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/internal/span/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/settings/route.js +1 -1
- package/.next/standalone/.next/server/app/api/agent/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/assets/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/clear-root/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/root-status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/set-root/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/upload/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/content/route.js +2 -2
- package/.next/standalone/.next/server/app/api/wiki/content/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/file/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/folder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/move/route.js +3 -3
- package/.next/standalone/.next/server/app/api/wiki/move/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/new-file/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/page/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/presence/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route.js +14 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/wiki/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/slugs/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/watch/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +5 -0
- package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0d2jbo5.js +3 -0
- package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0ytral2.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-ao6a_._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0269d2e._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__05f6x0i._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06-dtd0._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06sek6_._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__09s9qkd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d75a5q._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d_da6q._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e7si9p._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0hhhvm7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kk26wc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kur4ro._.js +15 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0p12-6~._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qc2~6l._.js +15 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0s~i040._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w47wil._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xq124z._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0l..bxz._.js → [root-of-the-server]__0xz-_0v._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__10w2q~o._.js +1 -1
- package/.next/standalone/.next/server/chunks/_079xuln._.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_file_[___path]_route_actions_0gb11t~.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_ls_[[___path]]_route_actions_0wfg4nl.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_move_route_actions_0z34mqq.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_search_route_actions_0ut_ug..js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_wiki_presence_route_actions_0x9d7f4.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{0~w0_katex_dist_katex_mjs_055boxx._.js → 0~w0_katex_dist_katex_mjs_00650~e._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_diagram-OG6HWLK6_mjs_0t5sus6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_sankeyDiagram-5OEKKPKP_mjs_11ifrnu._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__134_q7l._.js → [root-of-the-server]__12ahxbl._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0pqaawz._.js +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0rc1vh5._.js → node_modules__pnpm_02gx8km._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0t4unpg._.js → node_modules__pnpm_02mew~s._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_12-ni1n._.js → node_modules__pnpm_03ij5.g._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_12~a4c_._.js → node_modules__pnpm_03v21p-._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0f~_mm3._.js → node_modules__pnpm_05qsply._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0alwr5e._.js → node_modules__pnpm_06pgebo._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_02zxe7o._.js → node_modules__pnpm_08ibh0w._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0hr027i._.js → node_modules__pnpm_099cv.b._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0znncy9._.js → node_modules__pnpm_0cwpt9p._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0-pisfi._.js → node_modules__pnpm_0f9fpy5._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0zhbnni._.js → node_modules__pnpm_0f_uzgy._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0-w-d-.._.js → node_modules__pnpm_0fixtms._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_03z0vkh._.js → node_modules__pnpm_0gofu~q._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_10i6lh6._.js → node_modules__pnpm_0iojvye._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0-hfev1._.js → node_modules__pnpm_0li2vzs._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_13kfy_z._.js → node_modules__pnpm_0m5-azh._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_071f6vl._.js → node_modules__pnpm_0n1kfcp._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_07uh2kc._.js → node_modules__pnpm_0rebktf._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0okw1r.._.js → node_modules__pnpm_0tkiej3._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0xuz.8f._.js → node_modules__pnpm_0v_nwsz._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0zb6573._.js → node_modules__pnpm_0wffzo0._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0d7jxsf._.js → node_modules__pnpm_0wouz19._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0egenev._.js → node_modules__pnpm_0xxwmv2._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0j9bq9s._.js → node_modules__pnpm_0zzzsrj._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0irsd.b._.js → node_modules__pnpm_0~.6dgb._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0erwv_m._.js → node_modules__pnpm_0~2~c30._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0y9k6h~._.js → node_modules__pnpm_10413dq._.js} +2 -2
- package/.next/standalone/.next/server/functions-config-manifest.json +5 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/{11s9zwr7edrs7.js → 17~r3.8awh3qg.js} +12 -12
- package/.next/standalone/README.md +79 -16
- package/.next/standalone/agents/bootstrap-prompt.md +12 -1
- package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +199 -6
- package/.next/standalone/docs/file-vs-collab-authority.md +124 -0
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/packages/wiki-viewer-mcp/README.md +194 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/http-client.js +220 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/index.js +472 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/register.js +94 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/state-cache.js +26 -0
- package/.next/standalone/packages/wiki-viewer-mcp/package-lock.json +1716 -0
- package/.next/standalone/packages/wiki-viewer-mcp/package.json +41 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/http-client.ts +344 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/index.ts +575 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/register.ts +144 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/state-cache.ts +45 -0
- package/.next/standalone/packages/wiki-viewer-mcp/tsconfig.json +19 -0
- package/.next/standalone/pnpm-lock.yaml +9 -9
- package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +11 -1
- package/.next/standalone/src/app/api/agent/fs/file/[...path]/route.ts +344 -0
- package/.next/standalone/src/app/api/agent/fs/ls/[[...path]]/route.ts +147 -0
- package/.next/standalone/src/app/api/agent/fs/move/route.ts +125 -0
- package/.next/standalone/src/app/api/agent/fs/search/route.ts +236 -0
- package/.next/standalone/src/app/api/agent/register/route.ts +3 -3
- package/.next/standalone/src/app/api/agents/install/route.ts +44 -2
- package/.next/standalone/src/app/api/system/auth-settings/route.ts +1 -0
- package/.next/standalone/src/app/api/wiki/move/route.ts +10 -1
- package/.next/standalone/src/app/api/wiki/presence/route.ts +55 -0
- package/.next/standalone/src/components/ai-panel/ai-panel.tsx +98 -103
- package/.next/standalone/src/components/ai-panel/token-section.tsx +8 -7
- package/.next/standalone/src/components/auth-settings-sheet.tsx +81 -22
- package/.next/standalone/src/components/editor/editor.tsx +38 -0
- package/.next/standalone/src/lib/proof/audit.ts +79 -0
- package/.next/standalone/src/lib/proof/auth.ts +2 -2
- package/.next/standalone/src/lib/proof/collab-state.ts +79 -0
- package/.next/standalone/src/lib/proof/lease.ts +104 -0
- package/.next/standalone/src/lib/proof/ops-applier.ts +74 -25
- package/.next/standalone/src/lib/proof/raw-fs.ts +198 -0
- package/.next/standalone/src/lib/proof/registry.ts +1 -1
- package/.next/standalone/src/lib/proof/sidecar.ts +38 -1
- package/.next/standalone/src/lib/proof/types.ts +13 -1
- package/.next/standalone/src/tests/proof/agent-fs.test.ts +824 -0
- package/.next/standalone/src/tests/proof/agents-install.test.ts +53 -0
- package/.next/standalone/src/tests/proof/collab-state.test.ts +499 -0
- package/.next/standalone/src/tests/proof/reconcile-sidecar.test.ts +195 -0
- package/.next/standalone/src/tests/proof/sidecar-lifecycle.test.ts +177 -0
- package/.next/standalone/tsconfig.tsbuildinfo +1 -1
- package/README.md +79 -16
- package/package.json +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__09~~-nd._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w87kiz._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__12ssbhv._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules__pnpm_0g0fhfk._.js +0 -3
- /package/.next/standalone/.next/static/{kzOuSAQArtvD7uiAJ6ZLh → 3G_3qm2a9nOX7oi5k0mPV}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{kzOuSAQArtvD7uiAJ6ZLh → 3G_3qm2a9nOX7oi5k0mPV}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{kzOuSAQArtvD7uiAJ6ZLh → 3G_3qm2a9nOX7oi5k0mPV}/_ssgManifest.js +0 -0
|
@@ -29,6 +29,59 @@ test("GET /api/agents/install returns 200 with expected shape", async () => {
|
|
|
29
29
|
assert.ok(Array.isArray(body.ops) && (body.ops as unknown[]).length > 0);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
test("GET /api/agents/install manifest includes fs/* routes, capabilities, scopeOps, modes, mcpAdapter", async () => {
|
|
33
|
+
const req = makeMockRequest("http://localhost:3000/api/agents/install");
|
|
34
|
+
const res = await installGET(req as Parameters<typeof installGET>[0]);
|
|
35
|
+
const body = await res.json() as Record<string, unknown>;
|
|
36
|
+
|
|
37
|
+
// fs routes present
|
|
38
|
+
const routes = body.routes as Array<Record<string, string>>;
|
|
39
|
+
const fsPaths = routes.map((r) => r.path);
|
|
40
|
+
assert.ok(fsPaths.some((p) => p.includes("/api/agent/fs/file")), "missing GET/PUT/DELETE fs/file route");
|
|
41
|
+
assert.ok(fsPaths.some((p) => p.includes("/api/agent/fs/ls")), "missing fs/ls route");
|
|
42
|
+
assert.ok(fsPaths.some((p) => p.includes("/api/agent/fs/move")), "missing fs/move route");
|
|
43
|
+
assert.ok(fsPaths.some((p) => p.includes("/api/agent/fs/search")), "missing fs/search route");
|
|
44
|
+
|
|
45
|
+
// capabilities block
|
|
46
|
+
const caps = body.capabilities as Record<string, unknown>;
|
|
47
|
+
assert.ok(caps, "capabilities block missing");
|
|
48
|
+
assert.ok(typeof caps.maxFileBytes === "number" && caps.maxFileBytes > 0, "maxFileBytes missing");
|
|
49
|
+
assert.equal(caps.supportsRange, true);
|
|
50
|
+
assert.equal(caps.ifMatchRequired, true);
|
|
51
|
+
assert.equal(caps.forceBypass, true);
|
|
52
|
+
assert.deepEqual(caps.search, ["grep", "glob"]);
|
|
53
|
+
assert.equal(caps.collabPrecondition, "If-Collab-Match");
|
|
54
|
+
|
|
55
|
+
// scopeOps includes delete
|
|
56
|
+
const scopeOps = caps.scopeOps as string[];
|
|
57
|
+
assert.ok(Array.isArray(scopeOps), "scopeOps missing");
|
|
58
|
+
assert.ok(scopeOps.includes("read"), "scopeOps missing read");
|
|
59
|
+
assert.ok(scopeOps.includes("mutate"), "scopeOps missing mutate");
|
|
60
|
+
assert.ok(scopeOps.includes("delete"), "scopeOps missing delete");
|
|
61
|
+
|
|
62
|
+
// collabStates
|
|
63
|
+
const collabStates = caps.collabStates as string[];
|
|
64
|
+
assert.ok(Array.isArray(collabStates));
|
|
65
|
+
assert.ok(collabStates.includes("active"));
|
|
66
|
+
assert.ok(collabStates.includes("not-markdown"));
|
|
67
|
+
|
|
68
|
+
// modes block
|
|
69
|
+
const modes = body.modes as Record<string, unknown>;
|
|
70
|
+
assert.ok(modes, "modes block missing");
|
|
71
|
+
assert.ok(typeof modes.rule === "string" && modes.rule.includes("X-Collab-State"), "modes.rule missing mode distinction");
|
|
72
|
+
const tiers = modes.tiers as Record<string, string>;
|
|
73
|
+
assert.ok(tiers && tiers.tier1 && tiers.tier2, "modes.tiers missing");
|
|
74
|
+
|
|
75
|
+
// mcpAdapter
|
|
76
|
+
const mcp = body.mcpAdapter as Record<string, unknown>;
|
|
77
|
+
assert.ok(mcp, "mcpAdapter missing");
|
|
78
|
+
assert.equal(mcp.package, "wiki-viewer-mcp");
|
|
79
|
+
assert.ok(typeof mcp.invoke === "string" && mcp.invoke.includes("npx"));
|
|
80
|
+
const env = mcp.env as string[];
|
|
81
|
+
assert.ok(env.includes("WIKI_VIEWER_URL"));
|
|
82
|
+
assert.ok(env.includes("WIKI_VIEWER_TOKEN"));
|
|
83
|
+
});
|
|
84
|
+
|
|
32
85
|
test("GET /api/agents/skill returns 200 text/markdown with SKILL.md content", async () => {
|
|
33
86
|
const req = makeMockRequest("http://localhost:3000/api/agents/skill");
|
|
34
87
|
const res = await skillGET(req as Parameters<typeof skillGET>[0]);
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3 tests — collab-state computation, lease store, X-Collab-* headers,
|
|
3
|
+
* and R6 TOCTOU enforcement on raw .md PUT.
|
|
4
|
+
*/
|
|
5
|
+
import { test, before, after, describe } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import {
|
|
8
|
+
mkdtemp, rm, writeFile, mkdir,
|
|
9
|
+
} from "node:fs/promises";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
13
|
+
|
|
14
|
+
import { setRootDir } from "../../lib/root-dir.js";
|
|
15
|
+
import { ensureRegistry, addAgent, hashToken } from "../../lib/proof/registry.js";
|
|
16
|
+
import { writeSidecar, emptySidecar } from "../../lib/proof/sidecar.js";
|
|
17
|
+
import { setLease, clearLease, leaseGeneration, hasActiveLease, _resetLeaseStore, LEASE_TTL_MS } from "../../lib/proof/lease.js";
|
|
18
|
+
import { computeCollabState } from "../../lib/proof/collab-state.js";
|
|
19
|
+
import { _resetAuditDb } from "../../lib/proof/audit.js";
|
|
20
|
+
import type { Sidecar, Suggestion, Comment } from "../../lib/proof/types.js";
|
|
21
|
+
|
|
22
|
+
// ── Route handlers ────────────────────────────────────────────────────────────
|
|
23
|
+
let fileGET: (req: Request, ctx: { params: Promise<{ path: string[] }> }) => Promise<Response>;
|
|
24
|
+
let filePUT: (req: Request, ctx: { params: Promise<{ path: string[] }> }) => Promise<Response>;
|
|
25
|
+
let tier2GET: (req: Request, ctx: { params: Promise<{ path: string[] }> }) => Promise<Response>;
|
|
26
|
+
|
|
27
|
+
let tmpHome: string;
|
|
28
|
+
let tmpRoot: string;
|
|
29
|
+
let MUTATE_TOKEN: string;
|
|
30
|
+
let READ_TOKEN: string;
|
|
31
|
+
|
|
32
|
+
function sha256hex(buf: Buffer | string): string {
|
|
33
|
+
const b = typeof buf === "string" ? Buffer.from(buf) : buf;
|
|
34
|
+
return "sha256:" + createHash("sha256").update(b).digest("hex");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function agentHeaders(token: string, id: string, extra: Record<string, string> = {}): Record<string, string> {
|
|
38
|
+
return { Authorization: `Bearer ${token}`, "X-Agent-Id": id, ...extra };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeCtx(segments: string[]): { params: Promise<{ path: string[] }> } {
|
|
42
|
+
return { params: Promise.resolve({ path: segments }) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fileUrl(rel: string, qs = ""): string {
|
|
46
|
+
return `http://localhost/api/agent/fs/file/${rel}${qs}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function tier2Url(rel: string): string {
|
|
50
|
+
return `http://localhost/api/agent/files/${rel}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
before(async () => {
|
|
54
|
+
tmpHome = await mkdtemp(path.join(tmpdir(), "collab-state-home-"));
|
|
55
|
+
tmpRoot = await mkdtemp(path.join(tmpdir(), "collab-state-root-"));
|
|
56
|
+
|
|
57
|
+
process.env.HOME = tmpHome;
|
|
58
|
+
setRootDir(tmpRoot);
|
|
59
|
+
_resetAuditDb();
|
|
60
|
+
_resetLeaseStore();
|
|
61
|
+
|
|
62
|
+
await ensureRegistry();
|
|
63
|
+
|
|
64
|
+
MUTATE_TOKEN = randomBytes(32).toString("hex");
|
|
65
|
+
READ_TOKEN = randomBytes(32).toString("hex");
|
|
66
|
+
|
|
67
|
+
await addAgent({
|
|
68
|
+
id: "ai:mutate",
|
|
69
|
+
displayName: "Mutate",
|
|
70
|
+
tokenHash: hashToken(MUTATE_TOKEN),
|
|
71
|
+
scope: { paths: ["**/*"], ops: ["read", "mutate", "delete"] },
|
|
72
|
+
createdAt: new Date().toISOString(),
|
|
73
|
+
lastSeen: new Date().toISOString(),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await addAgent({
|
|
77
|
+
id: "ai:reader",
|
|
78
|
+
displayName: "Reader",
|
|
79
|
+
tokenHash: hashToken(READ_TOKEN),
|
|
80
|
+
scope: { paths: ["**/*"], ops: ["read"] },
|
|
81
|
+
createdAt: new Date().toISOString(),
|
|
82
|
+
lastSeen: new Date().toISOString(),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const fileRoute = await import("../../app/api/agent/fs/file/[...path]/route.js");
|
|
86
|
+
fileGET = fileRoute.GET;
|
|
87
|
+
filePUT = fileRoute.PUT;
|
|
88
|
+
|
|
89
|
+
const tier2Route = await import("../../app/api/agent/files/[...path]/route.js");
|
|
90
|
+
tier2GET = tier2Route.GET;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
after(async () => {
|
|
94
|
+
_resetLeaseStore();
|
|
95
|
+
await rm(tmpHome, { recursive: true, force: true });
|
|
96
|
+
await rm(tmpRoot, { recursive: true, force: true });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ── Lease store unit tests ────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
describe("lease store", () => {
|
|
102
|
+
before(() => _resetLeaseStore());
|
|
103
|
+
after(() => _resetLeaseStore());
|
|
104
|
+
|
|
105
|
+
test("new path has generation 0, no active lease", () => {
|
|
106
|
+
_resetLeaseStore();
|
|
107
|
+
assert.equal(leaseGeneration("docs/a.md"), 0);
|
|
108
|
+
assert.equal(hasActiveLease("docs/a.md"), false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("setLease makes lease active, bumps generation 0→1", () => {
|
|
112
|
+
_resetLeaseStore();
|
|
113
|
+
setLease("docs/a.md", "u1");
|
|
114
|
+
assert.equal(hasActiveLease("docs/a.md"), true);
|
|
115
|
+
assert.equal(leaseGeneration("docs/a.md"), 1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("second setLease (heartbeat) does NOT bump generation again", () => {
|
|
119
|
+
_resetLeaseStore();
|
|
120
|
+
setLease("docs/a.md", "u1");
|
|
121
|
+
const gen = leaseGeneration("docs/a.md");
|
|
122
|
+
setLease("docs/a.md", "u1"); // heartbeat
|
|
123
|
+
assert.equal(leaseGeneration("docs/a.md"), gen); // unchanged
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("clearLease removes active lease and bumps generation 1→2", () => {
|
|
127
|
+
_resetLeaseStore();
|
|
128
|
+
setLease("docs/a.md", "u1");
|
|
129
|
+
assert.equal(leaseGeneration("docs/a.md"), 1);
|
|
130
|
+
clearLease("docs/a.md", "u1");
|
|
131
|
+
assert.equal(hasActiveLease("docs/a.md"), false);
|
|
132
|
+
assert.equal(leaseGeneration("docs/a.md"), 2);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("clearLease by different user is no-op", () => {
|
|
136
|
+
_resetLeaseStore();
|
|
137
|
+
setLease("docs/a.md", "u1");
|
|
138
|
+
clearLease("docs/a.md", "u-other");
|
|
139
|
+
assert.equal(hasActiveLease("docs/a.md"), true);
|
|
140
|
+
assert.equal(leaseGeneration("docs/a.md"), 1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("expired lease (via short TTL) returns false and bumps generation", async () => {
|
|
144
|
+
_resetLeaseStore();
|
|
145
|
+
setLease("docs/expire.md", "u1", 10); // 10ms TTL
|
|
146
|
+
assert.equal(hasActiveLease("docs/expire.md"), true);
|
|
147
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
148
|
+
assert.equal(hasActiveLease("docs/expire.md"), false);
|
|
149
|
+
// generation bumped on expiry check
|
|
150
|
+
assert.ok(leaseGeneration("docs/expire.md") >= 2);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("set after expiry = new 0→1 transition, bumps again", async () => {
|
|
154
|
+
_resetLeaseStore();
|
|
155
|
+
setLease("docs/exp2.md", "u1", 10);
|
|
156
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
157
|
+
hasActiveLease("docs/exp2.md"); // triggers sweep
|
|
158
|
+
const genAfterExpiry = leaseGeneration("docs/exp2.md");
|
|
159
|
+
setLease("docs/exp2.md", "u1"); // fresh open
|
|
160
|
+
assert.equal(leaseGeneration("docs/exp2.md"), genAfterExpiry + 1);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ── computeCollabState matrix ─────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
describe("computeCollabState", () => {
|
|
167
|
+
before(() => _resetLeaseStore());
|
|
168
|
+
after(() => _resetLeaseStore());
|
|
169
|
+
|
|
170
|
+
test("not-markdown: txt file", async () => {
|
|
171
|
+
const result = await computeCollabState(tmpRoot, "file.txt");
|
|
172
|
+
assert.equal(result.state, "not-markdown");
|
|
173
|
+
assert.equal(result.revision, 0);
|
|
174
|
+
assert.equal(result.snapshotUrl, null);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("not-markdown: ts file", async () => {
|
|
178
|
+
const result = await computeCollabState(tmpRoot, "src/app.ts");
|
|
179
|
+
assert.equal(result.state, "not-markdown");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("untracked: .md, no sidecar, no lease", async () => {
|
|
183
|
+
_resetLeaseStore();
|
|
184
|
+
await writeFile(path.join(tmpRoot, "untracked.md"), "# hello");
|
|
185
|
+
const result = await computeCollabState(tmpRoot, "untracked.md");
|
|
186
|
+
assert.equal(result.state, "untracked");
|
|
187
|
+
assert.equal(result.snapshotUrl, "/api/agent/files/untracked.md");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("tracked: .md with sidecar, no artifacts, no lease", async () => {
|
|
191
|
+
_resetLeaseStore();
|
|
192
|
+
const mdPath = "tracked.md";
|
|
193
|
+
await writeFile(path.join(tmpRoot, mdPath), "# tracked");
|
|
194
|
+
const sc: Sidecar = {
|
|
195
|
+
...emptySidecar(mdPath),
|
|
196
|
+
revision: 3,
|
|
197
|
+
suggestions: [],
|
|
198
|
+
comments: [],
|
|
199
|
+
blockProvenance: {},
|
|
200
|
+
};
|
|
201
|
+
await writeSidecar(tmpRoot, mdPath, sc);
|
|
202
|
+
const result = await computeCollabState(tmpRoot, mdPath);
|
|
203
|
+
assert.equal(result.state, "tracked");
|
|
204
|
+
assert.equal(result.revision, 3); // sidecar.revision + leaseGen(0)
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("active: .md with pending suggestion", async () => {
|
|
208
|
+
_resetLeaseStore();
|
|
209
|
+
const mdPath = "active-suggestion.md";
|
|
210
|
+
await writeFile(path.join(tmpRoot, mdPath), "# active");
|
|
211
|
+
const sc: Sidecar = {
|
|
212
|
+
...emptySidecar(mdPath),
|
|
213
|
+
revision: 5,
|
|
214
|
+
suggestions: [
|
|
215
|
+
{
|
|
216
|
+
id: "s0001", ref: "b000001", kind: "replace", status: "pending",
|
|
217
|
+
by: "ai:test", createdAt: new Date().toISOString(),
|
|
218
|
+
} as Suggestion,
|
|
219
|
+
],
|
|
220
|
+
comments: [],
|
|
221
|
+
blockProvenance: {},
|
|
222
|
+
};
|
|
223
|
+
await writeSidecar(tmpRoot, mdPath, sc);
|
|
224
|
+
const result = await computeCollabState(tmpRoot, mdPath);
|
|
225
|
+
assert.equal(result.state, "active");
|
|
226
|
+
assert.equal(result.revision, 5);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("active: .md with unresolved comment", async () => {
|
|
230
|
+
_resetLeaseStore();
|
|
231
|
+
const mdPath = "active-comment.md";
|
|
232
|
+
await writeFile(path.join(tmpRoot, mdPath), "# active");
|
|
233
|
+
const sc: Sidecar = {
|
|
234
|
+
...emptySidecar(mdPath),
|
|
235
|
+
revision: 2,
|
|
236
|
+
suggestions: [],
|
|
237
|
+
comments: [
|
|
238
|
+
{
|
|
239
|
+
id: "c0001", ref: "b000002", resolved: false,
|
|
240
|
+
createdAt: new Date().toISOString(), turns: [],
|
|
241
|
+
} as Comment,
|
|
242
|
+
],
|
|
243
|
+
blockProvenance: {},
|
|
244
|
+
};
|
|
245
|
+
await writeSidecar(tmpRoot, mdPath, sc);
|
|
246
|
+
const result = await computeCollabState(tmpRoot, mdPath);
|
|
247
|
+
assert.equal(result.state, "active");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("active: .md via lease only (no sidecar)", async () => {
|
|
251
|
+
_resetLeaseStore();
|
|
252
|
+
const mdPath = "lease-only.md";
|
|
253
|
+
await writeFile(path.join(tmpRoot, mdPath), "# lease only");
|
|
254
|
+
// No sidecar; human opens it
|
|
255
|
+
setLease(mdPath, "human-user-1");
|
|
256
|
+
const result = await computeCollabState(tmpRoot, mdPath);
|
|
257
|
+
assert.equal(result.state, "active");
|
|
258
|
+
assert.equal(result.revision, 1); // 0 + gen(1)
|
|
259
|
+
clearLease(mdPath, "human-user-1");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("revision = sidecar.revision + leaseGeneration", async () => {
|
|
263
|
+
_resetLeaseStore();
|
|
264
|
+
const mdPath = "revision-test.md";
|
|
265
|
+
await writeFile(path.join(tmpRoot, mdPath), "# rev");
|
|
266
|
+
const sc: Sidecar = { ...emptySidecar(mdPath), revision: 7 };
|
|
267
|
+
await writeSidecar(tmpRoot, mdPath, sc);
|
|
268
|
+
// no lease: gen=0, revision=7
|
|
269
|
+
const r1 = await computeCollabState(tmpRoot, mdPath);
|
|
270
|
+
assert.equal(r1.revision, 7);
|
|
271
|
+
// open lease: gen=1, revision=8
|
|
272
|
+
setLease(mdPath, "u1");
|
|
273
|
+
const r2 = await computeCollabState(tmpRoot, mdPath);
|
|
274
|
+
assert.equal(r2.revision, 8);
|
|
275
|
+
clearLease(mdPath, "u1");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("stale pending suggestion does NOT count as active artifact", async () => {
|
|
279
|
+
_resetLeaseStore();
|
|
280
|
+
const mdPath = "stale-sug.md";
|
|
281
|
+
await writeFile(path.join(tmpRoot, mdPath), "# stale");
|
|
282
|
+
const sc: Sidecar = {
|
|
283
|
+
...emptySidecar(mdPath),
|
|
284
|
+
revision: 1,
|
|
285
|
+
suggestions: [
|
|
286
|
+
{
|
|
287
|
+
id: "s0002", ref: "b000003", kind: "replace", status: "pending",
|
|
288
|
+
by: "ai:test", createdAt: new Date().toISOString(),
|
|
289
|
+
stale: true,
|
|
290
|
+
} as Suggestion,
|
|
291
|
+
],
|
|
292
|
+
comments: [],
|
|
293
|
+
blockProvenance: {},
|
|
294
|
+
};
|
|
295
|
+
await writeSidecar(tmpRoot, mdPath, sc);
|
|
296
|
+
const result = await computeCollabState(tmpRoot, mdPath);
|
|
297
|
+
assert.equal(result.state, "tracked"); // stale suggestion = no artifact
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// ── X-Collab-* headers on GET (fs/file) ──────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
describe("X-Collab headers on GET fs/file", () => {
|
|
304
|
+
before(() => _resetLeaseStore());
|
|
305
|
+
after(() => _resetLeaseStore());
|
|
306
|
+
|
|
307
|
+
test("non-markdown returns X-Collab-State: not-markdown, no snapshot header", async () => {
|
|
308
|
+
await writeFile(path.join(tmpRoot, "code.ts"), "export const x = 1;");
|
|
309
|
+
const req = new Request(fileUrl("code.ts"), {
|
|
310
|
+
headers: agentHeaders(READ_TOKEN, "ai:reader"),
|
|
311
|
+
});
|
|
312
|
+
const res = await fileGET(req, makeCtx(["code.ts"]));
|
|
313
|
+
assert.equal(res.status, 200);
|
|
314
|
+
assert.equal(res.headers.get("x-collab-state"), "not-markdown");
|
|
315
|
+
assert.equal(res.headers.get("x-collab-snapshot"), null);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("untracked .md returns X-Collab-State: untracked + snapshot URL", async () => {
|
|
319
|
+
_resetLeaseStore();
|
|
320
|
+
await writeFile(path.join(tmpRoot, "hdr-untracked.md"), "# hello");
|
|
321
|
+
const req = new Request(fileUrl("hdr-untracked.md"), {
|
|
322
|
+
headers: agentHeaders(READ_TOKEN, "ai:reader"),
|
|
323
|
+
});
|
|
324
|
+
const res = await fileGET(req, makeCtx(["hdr-untracked.md"]));
|
|
325
|
+
assert.equal(res.status, 200);
|
|
326
|
+
assert.equal(res.headers.get("x-collab-state"), "untracked");
|
|
327
|
+
assert.ok(res.headers.get("x-collab-snapshot")?.includes("hdr-untracked.md"));
|
|
328
|
+
assert.ok(res.headers.get("x-collab-revision") !== null);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("active lease makes X-Collab-State: active on GET", async () => {
|
|
332
|
+
_resetLeaseStore();
|
|
333
|
+
const mdPath = "hdr-active.md";
|
|
334
|
+
await writeFile(path.join(tmpRoot, mdPath), "# active");
|
|
335
|
+
setLease(mdPath, "human-1");
|
|
336
|
+
const req = new Request(fileUrl(mdPath), {
|
|
337
|
+
headers: agentHeaders(READ_TOKEN, "ai:reader"),
|
|
338
|
+
});
|
|
339
|
+
const res = await fileGET(req, makeCtx([mdPath]));
|
|
340
|
+
assert.equal(res.status, 200);
|
|
341
|
+
assert.equal(res.headers.get("x-collab-state"), "active");
|
|
342
|
+
clearLease(mdPath, "human-1");
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ── X-Collab-* headers on GET agent/files (Tier-2) ────────────────────────────
|
|
347
|
+
|
|
348
|
+
describe("X-Collab headers on Tier-2 GET agent/files", () => {
|
|
349
|
+
before(() => _resetLeaseStore());
|
|
350
|
+
after(() => _resetLeaseStore());
|
|
351
|
+
|
|
352
|
+
test("Tier-2 GET returns X-Collab-State + revision headers", async () => {
|
|
353
|
+
_resetLeaseStore();
|
|
354
|
+
const mdPath = "tier2-hdr.md";
|
|
355
|
+
await writeFile(path.join(tmpRoot, mdPath), "# tier2");
|
|
356
|
+
const req = new Request(tier2Url(mdPath), {
|
|
357
|
+
headers: agentHeaders(READ_TOKEN, "ai:reader"),
|
|
358
|
+
});
|
|
359
|
+
const res = await tier2GET(req, makeCtx([mdPath]));
|
|
360
|
+
assert.equal(res.status, 200);
|
|
361
|
+
assert.ok(res.headers.get("x-collab-state") !== null, "X-Collab-State present");
|
|
362
|
+
assert.ok(res.headers.get("x-collab-revision") !== null, "X-Collab-Revision present");
|
|
363
|
+
assert.ok(res.headers.get("x-collab-snapshot") !== null, "X-Collab-Snapshot present");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("Tier-2 GET returns active when lease set", async () => {
|
|
367
|
+
_resetLeaseStore();
|
|
368
|
+
const mdPath = "tier2-active.md";
|
|
369
|
+
await writeFile(path.join(tmpRoot, mdPath), "# active");
|
|
370
|
+
setLease(mdPath, "h1");
|
|
371
|
+
const req = new Request(tier2Url(mdPath), {
|
|
372
|
+
headers: agentHeaders(READ_TOKEN, "ai:reader"),
|
|
373
|
+
});
|
|
374
|
+
const res = await tier2GET(req, makeCtx([mdPath]));
|
|
375
|
+
assert.equal(res.headers.get("x-collab-state"), "active");
|
|
376
|
+
clearLease(mdPath, "h1");
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// ── R6 TOCTOU enforcement ─────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
describe("R6 TOCTOU enforcement on raw .md PUT", () => {
|
|
383
|
+
before(() => _resetLeaseStore());
|
|
384
|
+
after(() => _resetLeaseStore());
|
|
385
|
+
|
|
386
|
+
test("PUT .md with no lease (untracked) → 200 no If-Collab-Match needed", async () => {
|
|
387
|
+
_resetLeaseStore();
|
|
388
|
+
const mdPath = "r6-untracked.md";
|
|
389
|
+
await writeFile(path.join(tmpRoot, mdPath), "# hello");
|
|
390
|
+
const content = Buffer.from("# hello updated");
|
|
391
|
+
const oldSha = sha256hex(Buffer.from("# hello"));
|
|
392
|
+
const req = new Request(fileUrl(mdPath), {
|
|
393
|
+
method: "PUT",
|
|
394
|
+
headers: agentHeaders(MUTATE_TOKEN, "ai:mutate", {
|
|
395
|
+
"If-Match": oldSha,
|
|
396
|
+
"Content-Type": "text/markdown",
|
|
397
|
+
}),
|
|
398
|
+
body: content,
|
|
399
|
+
});
|
|
400
|
+
const res = await filePUT(req, makeCtx([mdPath]));
|
|
401
|
+
assert.equal(res.status, 200, "untracked md PUT should succeed");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("PUT .md: doc goes active (lease set) between read and write → 409 COLLAB_ACTIVE", async () => {
|
|
405
|
+
_resetLeaseStore();
|
|
406
|
+
const mdPath = "r6-race.md";
|
|
407
|
+
const initial = "# race test";
|
|
408
|
+
await writeFile(path.join(tmpRoot, mdPath), initial);
|
|
409
|
+
const oldSha = sha256hex(Buffer.from(initial));
|
|
410
|
+
|
|
411
|
+
// Simulate: agent read the file, then human opens it (lease set)
|
|
412
|
+
setLease(mdPath, "human-racer");
|
|
413
|
+
|
|
414
|
+
// Agent tries raw PUT without If-Collab-Match
|
|
415
|
+
const req = new Request(fileUrl(mdPath), {
|
|
416
|
+
method: "PUT",
|
|
417
|
+
headers: agentHeaders(MUTATE_TOKEN, "ai:mutate", {
|
|
418
|
+
"If-Match": oldSha,
|
|
419
|
+
"Content-Type": "text/markdown",
|
|
420
|
+
}),
|
|
421
|
+
body: Buffer.from("# overwritten"),
|
|
422
|
+
});
|
|
423
|
+
const res = await filePUT(req, makeCtx([mdPath]));
|
|
424
|
+
assert.equal(res.status, 409, "should 409 when doc went active mid-flight");
|
|
425
|
+
const body = await res.json() as { error: string; snapshotUrl: string; revision: number };
|
|
426
|
+
assert.equal(body.error, "COLLAB_ACTIVE");
|
|
427
|
+
assert.ok(body.snapshotUrl, "snapshotUrl in response");
|
|
428
|
+
assert.ok(typeof body.revision === "number", "revision in response");
|
|
429
|
+
|
|
430
|
+
clearLease(mdPath, "human-racer");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("PUT .md with matching If-Collab-Match → 200", async () => {
|
|
434
|
+
_resetLeaseStore();
|
|
435
|
+
const mdPath = "r6-match.md";
|
|
436
|
+
const initial = "# match test";
|
|
437
|
+
await writeFile(path.join(tmpRoot, mdPath), initial);
|
|
438
|
+
const oldSha = sha256hex(Buffer.from(initial));
|
|
439
|
+
|
|
440
|
+
setLease(mdPath, "human-match");
|
|
441
|
+
// Get the current revision
|
|
442
|
+
const { revision } = await computeCollabState(tmpRoot, mdPath);
|
|
443
|
+
|
|
444
|
+
const req = new Request(fileUrl(mdPath), {
|
|
445
|
+
method: "PUT",
|
|
446
|
+
headers: agentHeaders(MUTATE_TOKEN, "ai:mutate", {
|
|
447
|
+
"If-Match": oldSha,
|
|
448
|
+
"If-Collab-Match": String(revision),
|
|
449
|
+
"Content-Type": "text/markdown",
|
|
450
|
+
}),
|
|
451
|
+
body: Buffer.from("# matched and written"),
|
|
452
|
+
});
|
|
453
|
+
const res = await filePUT(req, makeCtx([mdPath]));
|
|
454
|
+
assert.equal(res.status, 200, "matching If-Collab-Match should allow write");
|
|
455
|
+
|
|
456
|
+
clearLease(mdPath, "human-match");
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("PUT .md with ?force=true bypasses R6 even on active doc → 200", async () => {
|
|
460
|
+
_resetLeaseStore();
|
|
461
|
+
const mdPath = "r6-force.md";
|
|
462
|
+
const initial = "# force test";
|
|
463
|
+
await writeFile(path.join(tmpRoot, mdPath), initial);
|
|
464
|
+
const oldSha = sha256hex(Buffer.from(initial));
|
|
465
|
+
|
|
466
|
+
setLease(mdPath, "human-force");
|
|
467
|
+
|
|
468
|
+
const req = new Request(fileUrl(mdPath, "?force=true"), {
|
|
469
|
+
method: "PUT",
|
|
470
|
+
headers: agentHeaders(MUTATE_TOKEN, "ai:mutate", {
|
|
471
|
+
"If-Match": oldSha,
|
|
472
|
+
"Content-Type": "text/markdown",
|
|
473
|
+
}),
|
|
474
|
+
body: Buffer.from("# forced overwrite"),
|
|
475
|
+
});
|
|
476
|
+
const res = await filePUT(req, makeCtx([mdPath]));
|
|
477
|
+
assert.equal(res.status, 200, "?force=true should bypass R6");
|
|
478
|
+
|
|
479
|
+
clearLease(mdPath, "human-force");
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("PUT non-.md file is never blocked by R6 (no collab state)", async () => {
|
|
483
|
+
_resetLeaseStore();
|
|
484
|
+
const tsPath = "r6-noblock.ts";
|
|
485
|
+
await writeFile(path.join(tmpRoot, tsPath), "const x = 1;");
|
|
486
|
+
const oldSha = sha256hex(Buffer.from("const x = 1;"));
|
|
487
|
+
|
|
488
|
+
const req = new Request(fileUrl(tsPath), {
|
|
489
|
+
method: "PUT",
|
|
490
|
+
headers: agentHeaders(MUTATE_TOKEN, "ai:mutate", {
|
|
491
|
+
"If-Match": oldSha,
|
|
492
|
+
"Content-Type": "text/plain",
|
|
493
|
+
}),
|
|
494
|
+
body: Buffer.from("const x = 2;"),
|
|
495
|
+
});
|
|
496
|
+
const res = await filePUT(req, makeCtx([tsPath]));
|
|
497
|
+
assert.equal(res.status, 200, "non-md should never hit R6");
|
|
498
|
+
});
|
|
499
|
+
});
|