Files
next-tjwater-agent/tests/opencode/learningToolBridge.test.ts
T
jiang 7383ed6d13 feat(agent): migrate OpenCode 2 runtime and sandbox tools
Upgrade the runtime and native plugin API to OpenCode 2.0.24. Restore
session-scoped learning, credential renewal, typed project queries and
controlled frontend actions from the legacy Agent.

Replace host Shell execution with an approved Landlock/seccomp executor
and persistent per-session workspaces. Add typed Bocha search and Tianditu
geocoding bridges using the owning session credential. Require a working
kernel sandbox before startup and persist workspace files in deployments.

Validation completed before this commit: TypeScript checks, 150 Bun tests,
Docker build and UID 10001 sandbox execution, native Shell approval and
session recovery. Tianditu succeeded; Bocha currently reports exhausted
account quota. Unrelated investigation-report contract work is excluded.
2026-10-08 15:46:37 +08:00

56 lines
4.0 KiB
TypeScript

import { afterEach, describe, expect, it } from "bun:test";
import express from "express";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { type Server } from "node:http";
import { registerLearningToolBridge } from "../../src/routes/learningToolBridge.js";
import { MemoryStore } from "../../src/memory/store.js";
import { SkillStore } from "../../src/skills/store.js";
import { getRuntimeSessionContext, removeRuntimeSessionContext, setRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
let server: Server | undefined;
let directory: string | undefined;
afterEach(async () => {
if (server) await new Promise<void>((resolve) => server!.close(() => resolve()));
if (directory) await rm(directory, { recursive: true, force: true });
removeRuntimeSessionContext("learning-bridge-session");
});
describe("learning tools across process boundaries", () => {
it("uses the parent session scope for list-before-write and rejects forged scope or missing context", async () => {
directory = await mkdtemp(join(tmpdir(), "learning-bridge-"));
const memoryStore = new MemoryStore(join(directory, "memory"), join(directory, "backup"));
await memoryStore.initialize();
setRuntimeSessionContext({ sessionId: "learning-bridge-session", clientSessionId: "client", actorKey: "owner", projectKey: "project", traceId: "test", allowLearningWrite: true });
const app = express(); app.use(express.json());
registerLearningToolBridge(app, { internalToken: "TEST_ONLY", memoryStore, skillStore: new SkillStore(join(directory, "skills"), join(directory, "skills-backup")) });
server = app.listen(0, "127.0.0.1");
await new Promise<void>((resolve) => server!.once("listening", resolve));
const address = server.address() as { port: number };
const baseUrl = `http://127.0.0.1:${address.port}`;
const child = Bun.spawn([process.execPath, "-e", [
'import memory from "./.opencode/tools/memory_manager.ts";',
'import skill from "./.opencode/tools/skill_manager.ts";',
'const ctx = { sessionID: "learning-bridge-session" };',
'const call = (action, content) => memory.execute({ action, scope: "user", reason: "test", content }, ctx).then(JSON.parse);',
'const before = await call("add", "用户偏好简洁答复");',
'await call("list"); const added = await call("add", "用户偏好简洁答复");',
'const skills = JSON.parse(await skill.execute({ action: "list", skill_path: "__root__", reason: "test" }, ctx));',
'process.stdout.write(JSON.stringify({ before, added, skills }));',
].join("\n")], { cwd: process.cwd(), env: { ...process.env, AGENT_INTERNAL_TOKEN: "TEST_ONLY", TJWATER_AGENT_INTERNAL_BASE_URL: baseUrl }, stdout: "pipe", stderr: "pipe" });
const [exitCode, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]);
expect({ exitCode, stderr }).toEqual({ exitCode: 0, stderr: "" });
const result = JSON.parse(stdout);
expect(result.before.decision).toBe("rejected");
expect(result.added.decision).toBe("accepted");
expect(result.skills.kind).toBe("skill");
expect(getRuntimeSessionContext("learning-bridge-session")?.memoryListReadScopes?.user).toBeTrue();
expect(await memoryStore.list("user", "owner")).toHaveLength(1);
expect(await memoryStore.list("user", "other-owner")).toHaveLength(0);
const post = (sessionId: string, input: unknown, token = "TEST_ONLY") => fetch(`${baseUrl}/internal/tools/memory-manager`, { method: "POST", headers: { "Content-Type": "application/json", "x-agent-internal-token": token }, body: JSON.stringify({ session_id: sessionId, input }) });
expect((await post("missing", { action: "list", scope: "user" })).status).toBe(404);
expect((await post("learning-bridge-session", { action: "list", scope: "user", actorKey: "other-owner" })).status).toBe(400);
expect((await post("learning-bridge-session", { action: "list", scope: "user" }, "wrong")).status).toBe(403);
});
});