1bde755933
- Added process-wide singleton instances for InternalUrlRouter, AsyncJobManager, and MCPManager. - Changed internal URL protocols to resolve through registered sessions and scan all active roots/datasets for matches. - Refactored agent, artifact, memory, rule, skill, jobs, and mcp handlers to use shared manager and rule/skill state. - Removed per-session protocol/tool wiring and switched tests to initialize and reset global singleton state.
114 lines
3.9 KiB
TypeScript
114 lines
3.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
import * as fs from "node:fs/promises";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import {
|
|
InternalUrlRouter,
|
|
LocalProtocolHandler,
|
|
resolveLocalRoot,
|
|
resolveLocalUrlToPath,
|
|
} from "@oh-my-pi/pi-coding-agent/internal-urls";
|
|
|
|
async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "local-protocol-"));
|
|
try {
|
|
return await fn(dir);
|
|
} finally {
|
|
await fs.rm(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
describe("LocalProtocolHandler", () => {
|
|
beforeEach(() => {
|
|
LocalProtocolHandler.resetOverrideForTests();
|
|
InternalUrlRouter.resetForTests();
|
|
});
|
|
|
|
afterEach(() => {
|
|
LocalProtocolHandler.resetOverrideForTests();
|
|
InternalUrlRouter.resetForTests();
|
|
});
|
|
|
|
it("lists files at local://", async () => {
|
|
await withTempDir(async tempDir => {
|
|
const artifactsDir = path.join(tempDir, "artifacts");
|
|
await fs.mkdir(path.join(artifactsDir, "local"), { recursive: true });
|
|
await Bun.write(path.join(artifactsDir, "local", "handoff.json"), '{"ok":true}');
|
|
|
|
LocalProtocolHandler.setOverride({
|
|
getArtifactsDir: () => artifactsDir,
|
|
getSessionId: () => "session-a",
|
|
});
|
|
const router = InternalUrlRouter.instance();
|
|
const resource = await router.resolve("local://");
|
|
|
|
expect(resource.contentType).toBe("text/markdown");
|
|
expect(resource.content).toContain("handoff.json");
|
|
});
|
|
});
|
|
|
|
it("reads a local file from session local root", async () => {
|
|
await withTempDir(async tempDir => {
|
|
const artifactsDir = path.join(tempDir, "artifacts");
|
|
const localFile = path.join(artifactsDir, "local", "subtasks", "trace.txt");
|
|
await fs.mkdir(path.dirname(localFile), { recursive: true });
|
|
await Bun.write(localFile, "trace");
|
|
|
|
LocalProtocolHandler.setOverride({
|
|
getArtifactsDir: () => artifactsDir,
|
|
getSessionId: () => "session-b",
|
|
});
|
|
const router = InternalUrlRouter.instance();
|
|
const resource = await router.resolve("local://subtasks/trace.txt");
|
|
|
|
expect(resource.content).toBe("trace");
|
|
expect(resource.contentType).toBe("text/plain");
|
|
});
|
|
});
|
|
|
|
it("blocks path traversal attempts", async () => {
|
|
await withTempDir(async tempDir => {
|
|
LocalProtocolHandler.setOverride({
|
|
getArtifactsDir: () => path.join(tempDir, "artifacts"),
|
|
getSessionId: () => "session-c",
|
|
});
|
|
const router = InternalUrlRouter.instance();
|
|
await expect(router.resolve("local://../secret.txt")).rejects.toThrow(
|
|
"Path traversal (..) is not allowed in local:// URLs",
|
|
);
|
|
await expect(router.resolve("local://%2E%2E/secret.txt")).rejects.toThrow(
|
|
"Path traversal (..) is not allowed in local:// URLs",
|
|
);
|
|
});
|
|
});
|
|
|
|
it("uses session id fallback root when artifacts dir is unavailable", async () => {
|
|
const root = resolveLocalRoot({ getSessionId: () => "session-fallback", getArtifactsDir: () => null });
|
|
expect(root).toContain(path.join("omp-local", "session-fallback"));
|
|
expect(resolveLocalUrlToPath("local://memo.txt", { getSessionId: () => "session-fallback" })).toBe(
|
|
path.join(root, "memo.txt"),
|
|
);
|
|
});
|
|
|
|
it("blocks symlink escapes outside local root", async () => {
|
|
if (process.platform === "win32") return;
|
|
|
|
await withTempDir(async tempDir => {
|
|
const artifactsDir = path.join(tempDir, "artifacts");
|
|
const localRoot = path.join(artifactsDir, "local");
|
|
const outsideDir = path.join(tempDir, "outside");
|
|
await fs.mkdir(localRoot, { recursive: true });
|
|
await fs.mkdir(outsideDir, { recursive: true });
|
|
await Bun.write(path.join(outsideDir, "secret.txt"), "secret");
|
|
await fs.symlink(outsideDir, path.join(localRoot, "linked"));
|
|
|
|
LocalProtocolHandler.setOverride({
|
|
getArtifactsDir: () => artifactsDir,
|
|
getSessionId: () => "session-d",
|
|
});
|
|
const router = InternalUrlRouter.instance();
|
|
await expect(router.resolve("local://linked/secret.txt")).rejects.toThrow("local:// URL escapes local root");
|
|
});
|
|
});
|
|
});
|