feat(agent): sandbox conversation analysis
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user