import { afterEach, describe, expect, it } from "bun:test"; import { mkdir, readFile, rm, stat } from "node:fs/promises"; import { resolve } from "node:path"; import { config } from "../../src/config.js"; import { type RuntimeSessionContext } from "../../src/runtime/sessionContext.js"; import { buildLargeCliResult, stageLargeToolOutput, } from "../../src/runtime/toolOutputStaging.js"; const createdPaths: string[] = []; afterEach(async () => { await Promise.all( createdPaths.splice(0).map((path) => rm(path, { force: true, recursive: true })), ); }); const createContext = async (): Promise => { const importRoot = resolve(config.RESULT_REF_IMPORT_DIR); await mkdir(importRoot, { recursive: true }); const workspaceDirectory = resolve( importRoot, `conversation-staging-${crypto.randomUUID()}`, ); await mkdir(workspaceDirectory, { mode: 0o700 }); createdPaths.push(workspaceDirectory); return { actorKey: "actor-1", clientSessionId: "client-1", projectKey: "project-1", sessionId: "runtime-1", traceId: "trace-1", workspaceDirectory, }; }; describe("tool output staging", () => { it("keeps small output inline unless storage is forced", async () => { const context = await createContext(); await expect( stageLargeToolOutput(context, '{"ok":true}', { contentType: "application/json", extension: "json", prefix: "cli", }), ).resolves.toBeNull(); const stored = await stageLargeToolOutput(context, '{"ok":true}', { contentType: "application/json", extension: "json", force: true, prefix: "cli", }); expect(stored?.file_path).toContain("/tool-data/cli-"); expect(await readFile(stored!.file_path, "utf8")).toBe('{"ok":true}'); expect((await stat(stored!.file_path)).mode & 0o777).toBe(0o600); }); it("stages output above the inline threshold and returns a compact descriptor", async () => { const context = await createContext(); const content = JSON.stringify({ ok: true, schema_version: "tjwater-cli/v1", data: "x".repeat(config.MAX_INLINE_RESULT_BYTES), }); const dataFile = await stageLargeToolOutput(context, content, { contentType: "application/json", extension: "json", prefix: "cli", }); expect(dataFile?.bytes).toBe(Buffer.byteLength(content)); const stored = dataFile!; expect(buildLargeCliResult(stored)).toEqual({ ok: true, schema_version: "tjwater-cli/v1", summary: "CLI 结果已保存到当前对话工作区", data_file: stored, }); }); it("does not stage large output for legacy sessions without a workspace", async () => { await expect( stageLargeToolOutput( { actorKey: "actor-1", clientSessionId: "client-1", projectKey: "project-1", sessionId: "runtime-1", traceId: "trace-1", }, "x".repeat(config.MAX_INLINE_RESULT_BYTES + 1), { contentType: "text/plain", extension: "txt", prefix: "shell" }, ), ).rejects.toThrow("requires a conversation workspace"); }); });