99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
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<RuntimeSessionContext> => {
|
|
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");
|
|
});
|
|
});
|