Files
2026-07-27 21:52:52 +08:00

171 lines
5.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, beforeEach, expect, test } from "vite-plus/test";
import type { CollectedSessionEvents, ProviderSessionEvent } from "@openagentpack/sdk";
import { BailianError, ExitCode } from "bailian-cli-core";
import {
renderCollectedEvents,
streamAndRenderEvents,
} from "../src/commands/managed-agent/_engine/session-render.ts";
/**
* `--output json` 会话信封契约stdout 恰好一个合法 JSON且信封头部携带
* session_id / provider / agent —— session run 的调用方必须能从 stdout 拿到
* 新建 Session ID 以继续 send/get/events/delete不靠刮 stderr
*/
let stdoutChunks: string[] = [];
let originalStdoutWrite: typeof process.stdout.write;
beforeEach(() => {
stdoutChunks = [];
originalStdoutWrite = process.stdout.write.bind(process.stdout);
process.stdout.write = ((chunk: string | Uint8Array) => {
stdoutChunks.push(String(chunk));
return true;
}) as typeof process.stdout.write;
});
afterEach(() => {
process.stdout.write = originalStdoutWrite;
});
function capturedJson(): Record<string, unknown> {
// 契约:整个 stdout 拼起来是单个合法 JSON
return JSON.parse(stdoutChunks.join("")) as Record<string, unknown>;
}
async function* fakeEventStream(): AsyncIterable<ProviderSessionEvent> {
yield {
type: "message",
role: "assistant",
content: "hi",
} as ProviderSessionEvent;
yield { type: "status", status: "completed" } as ProviderSessionEvent;
}
async function* fakeFailedEventStream(): AsyncIterable<ProviderSessionEvent> {
yield {
type: "error",
content: "provider quota exceeded",
} as ProviderSessionEvent;
yield { type: "status", status: "failed" } as ProviderSessionEvent;
}
function fakeCollected(): CollectedSessionEvents {
return {
terminalStatus: "completed",
result: {
events: [
{
type: "message",
role: "assistant",
content: "hi",
} as ProviderSessionEvent,
],
has_more: false,
next_page: undefined,
},
} as CollectedSessionEvents;
}
function fakeFailedCollected(): CollectedSessionEvents {
return {
terminalStatus: "failed",
result: {
events: [
{
type: "error",
content: "provider quota exceeded",
} as ProviderSessionEvent,
],
has_more: false,
next_page: undefined,
},
} as CollectedSessionEvents;
}
async function catchSessionFailure(run: () => Promise<void> | void): Promise<BailianError> {
try {
await run();
} catch (error) {
expect(error).toBeInstanceOf(BailianError);
return error as BailianError;
}
throw new Error("expected failed session to throw");
}
test("stream json:信封携带 session_id/provider/agent + events", async () => {
await streamAndRenderEvents(fakeEventStream(), true, {
session_id: "sess_stream",
provider: "bailian",
agent: "assistant",
});
const data = capturedJson();
expect(data.session_id).toBe("sess_stream");
expect(data.provider).toBe("bailian");
expect(data.agent).toBe("assistant");
expect(Array.isArray(data.events)).toBe(true);
expect((data.events as unknown[]).length).toBe(2);
});
test("streaming failed:保留 JSON 信封并以服务端 error 消息抛 GENERAL", async () => {
const error = await catchSessionFailure(() =>
streamAndRenderEvents(fakeFailedEventStream(), true, {
session_id: "sess_failed_stream",
provider: "bailian",
agent: "assistant",
}),
);
expect(error.exitCode).toBe(ExitCode.GENERAL);
expect(error.message).toBe("provider quota exceeded");
const data = capturedJson();
expect(data.session_id).toBe("sess_failed_stream");
expect((data.events as unknown[]).length).toBe(2);
});
test("polling json:信封携带 session_id/provider/agent并保留 has_more/next_page", () => {
renderCollectedEvents(fakeCollected(), true, {
session_id: "sess_poll",
provider: "claude",
agent: "assistant",
});
const data = capturedJson();
expect(data.session_id).toBe("sess_poll");
expect(data.provider).toBe("claude");
expect(data.agent).toBe("assistant");
expect(data.has_more).toBe(false);
expect(Array.isArray(data.events)).toBe(true);
});
test("polling failed:保留 JSON 信封并以服务端 error 消息抛 GENERAL", async () => {
const error = await catchSessionFailure(() =>
renderCollectedEvents(fakeFailedCollected(), true, {
session_id: "sess_failed_poll",
provider: "claude",
agent: "assistant",
}),
);
expect(error.exitCode).toBe(ExitCode.GENERAL);
expect(error.message).toBe("provider quota exceeded");
const data = capturedJson();
expect(data.session_id).toBe("sess_failed_poll");
expect((data.events as unknown[]).length).toBe(1);
});
test("json:不传 context 时信封形状不变(无 session_id 键)", () => {
renderCollectedEvents(fakeCollected(), true);
const data = capturedJson();
expect("session_id" in data).toBe(false);
expect(Array.isArray(data.events)).toBe(true);
});
test("text 模式:context 不影响 stdout(仍只输出助手文本)", async () => {
await streamAndRenderEvents(fakeEventStream(), false, {
session_id: "sess_text",
});
const output = stdoutChunks.join("");
expect(output).toBe("hi");
expect(output).not.toContain("sess_text");
});