| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190 |
- import { beforeEach, describe, expect, it, vi } from "vitest"
- import type { LlmConfig } from "@/stores/wiki-store"
- const clientMock = vi.hoisted(() => ({
- call: vi.fn(),
- handler: null as null | { onEnvelope?: (envelope: Record<string, unknown>) => void },
- interrupt: vi.fn(async () => undefined),
- }))
- vi.mock("./codex-app-server-client", () => ({
- getCodexAppServerClient: () => ({
- isolatedCwd: "/tmp/qmai/workspace",
- ensureStarted: vi.fn(async () => undefined),
- call: clientMock.call,
- interrupt: clientMock.interrupt,
- registerThread: (_threadId: string, handler: typeof clientMock.handler) => {
- clientMock.handler = handler
- return () => {
- clientMock.handler = null
- }
- },
- }),
- }))
- import {
- buildCodexTurnInput,
- codexNativeBoundaryError,
- restrictedCodexConfig,
- streamCodexCli,
- } from "./codex-cli-transport"
- const config: LlmConfig = {
- provider: "codex-cli",
- apiKey: "",
- model: "gpt-test",
- ollamaUrl: "",
- customEndpoint: "",
- maxContextSize: 8192,
- codexCliTimeoutMinutes: 10,
- }
- describe("codex app-server transport", () => {
- beforeEach(() => {
- clientMock.call.mockReset()
- clientMock.handler = null
- clientMock.interrupt.mockClear()
- })
- it("disables native extension, shell, web, image, and project-rule surfaces", () => {
- const restricted = restrictedCodexConfig()
- expect(restricted).toEqual(expect.objectContaining({
- web_search: "disabled",
- project_doc_max_bytes: 0,
- project_root_markers: [],
- tools: { view_image: false, web_search: false },
- features: expect.objectContaining({
- apps: false,
- plugins: false,
- shell_tool: false,
- multi_agent: false,
- skill_search: false,
- }),
- }))
- })
- it("serializes trimmed chat history and images into turn input", () => {
- const input = buildCodexTurnInput([
- { role: "system", content: "system" },
- { role: "user", content: [
- { type: "text", text: "看图" },
- { type: "image", mediaType: "image/png", dataBase64: "YWJj" },
- ] },
- { role: "assistant", content: "收到" },
- ])
- expect(input[0].text).toContain("<USER>\n看图")
- expect(input[0].text).toContain("<ASSISTANT>\n收到")
- expect(input).toContainEqual({ type: "image", url: "data:image/png;base64,YWJj" })
- })
- it("allows only QMAI dynamic tool server requests and rejects native MCP notifications", () => {
- const dynamic = { id: 1, method: "item/tool/call", params: {} }
- expect(codexNativeBoundaryError(dynamic, true)).toBeNull()
- expect(codexNativeBoundaryError(dynamic)).toEqual(expect.objectContaining({
- message: expect.stringContaining("禁止 Codex 原生能力"),
- }))
- expect(codexNativeBoundaryError({
- method: "mcpServer/startupStatus/updated",
- params: {},
- })).toEqual(expect.objectContaining({
- message: expect.stringContaining("mcpServer/startupStatus/updated"),
- }))
- expect(codexNativeBoundaryError({
- method: "hook/started",
- params: {},
- })).toEqual(expect.objectContaining({
- message: expect.stringContaining("hook/started"),
- }))
- })
- it("uses app-server for plain streamChat without exposing dynamic tools", async () => {
- clientMock.call.mockImplementation(async (method: string, params: Record<string, unknown>) => {
- if (method === "thread/start") {
- expect(params).not.toHaveProperty("dynamicTools")
- return { thread: { id: "thread-text" }, instructionSources: [] }
- }
- if (method === "turn/start") {
- queueMicrotask(() => {
- clientMock.handler?.onEnvelope?.({
- method: "item/agentMessage/delta",
- params: { threadId: "thread-text", delta: "纯文本结果" },
- })
- clientMock.handler?.onEnvelope?.({
- method: "turn/completed",
- params: { threadId: "thread-text", turn: { status: "completed" } },
- })
- })
- return { turn: { id: "turn-text" } }
- }
- throw new Error(`unexpected method: ${method}`)
- })
- const callbacks = {
- onToken: vi.fn(),
- onDone: vi.fn(),
- onError: vi.fn(),
- }
- await streamCodexCli(config, [
- { role: "system", content: "只输出正文" },
- { role: "user", content: "生成大纲" },
- ], callbacks)
- expect(callbacks.onToken).toHaveBeenCalledWith("纯文本结果")
- expect(callbacks.onDone).toHaveBeenCalledOnce()
- expect(callbacks.onError).not.toHaveBeenCalled()
- const starts = clientMock.call.mock.calls.filter(([method]) =>
- method === "thread/start" || method === "turn/start"
- )
- expect(starts).toHaveLength(2)
- expect(starts[0][1]).not.toHaveProperty("serviceTier")
- expect(starts[1][1]).not.toHaveProperty("serviceTier")
- })
- it("passes Fast mode to both app-server starts as the priority service tier", async () => {
- clientMock.call.mockImplementation(async (method: string) => {
- if (method === "thread/start") {
- return { thread: { id: "thread-fast" }, instructionSources: [] }
- }
- if (method === "turn/start") {
- queueMicrotask(() => {
- clientMock.handler?.onEnvelope?.({
- method: "turn/completed",
- params: { threadId: "thread-fast", turn: { status: "completed" } },
- })
- })
- return { turn: { id: "turn-fast" } }
- }
- throw new Error(`unexpected method: ${method}`)
- })
- await streamCodexCli(
- { ...config, codexSpeedMode: "fast" },
- [{ role: "user", content: "测试快速档位" }],
- { onToken: vi.fn(), onDone: vi.fn(), onError: vi.fn() },
- )
- const threadStart = clientMock.call.mock.calls.find(([method]) => method === "thread/start")
- const turnStart = clientMock.call.mock.calls.find(([method]) => method === "turn/start")
- expect(threadStart?.[1]).toEqual(expect.objectContaining({ serviceTier: "priority" }))
- expect(turnStart?.[1]).toEqual(expect.objectContaining({ serviceTier: "priority" }))
- })
- it("fails closed when app-server reports inherited instruction sources", async () => {
- clientMock.call.mockResolvedValueOnce({
- thread: { id: "thread-bad" },
- instructionSources: ["/Users/test/.codex/AGENTS.md"],
- })
- const callbacks = {
- onToken: vi.fn(),
- onDone: vi.fn(),
- onError: vi.fn(),
- }
- await streamCodexCli(config, [{ role: "user", content: "测试" }], callbacks)
- expect(callbacks.onError).toHaveBeenCalledWith(expect.objectContaining({
- message: expect.stringContaining("禁止 Codex 加载本机或项目规则"),
- }))
- expect(callbacks.onDone).not.toHaveBeenCalled()
- })
- })
|