fix: Codex 自动压缩不触发 —— 接管时链接模型目录(绝对路径)进 config.toml (hot 1.5.83)
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const { collectUpstreamOutputs } = require("../renderer/canvas-inputs.js");
|
||||
|
||||
function nodeWithConnections(connections) {
|
||||
return { inputs: { input_1: { connections } } };
|
||||
}
|
||||
|
||||
test("collects text from a connected text node for image prompts", () => {
|
||||
const result = collectUpstreamOutputs(
|
||||
nodeWithConnections([{ node: 1 }]),
|
||||
() => ({ text: "a cinematic desk lamp", image: "" })
|
||||
);
|
||||
|
||||
assert.equal(result.text, "a cinematic desk lamp");
|
||||
assert.deepEqual(result.images, []);
|
||||
});
|
||||
|
||||
test("exposes the first connected image for first-frame video", () => {
|
||||
const result = collectUpstreamOutputs(
|
||||
nodeWithConnections([{ node: 1 }]),
|
||||
() => ({ image: "data:image/png;base64,first" })
|
||||
);
|
||||
|
||||
assert.equal(result.image, "data:image/png;base64,first");
|
||||
assert.deepEqual(result.images, ["data:image/png;base64,first"]);
|
||||
});
|
||||
|
||||
test("keeps every connected image in connection order and removes duplicates", () => {
|
||||
const outputs = {
|
||||
1: { image: "data:image/png;base64,one" },
|
||||
2: { image: "data:image/png;base64,two" },
|
||||
3: { image: "data:image/png;base64,one" },
|
||||
4: { image: " " },
|
||||
};
|
||||
const result = collectUpstreamOutputs(
|
||||
nodeWithConnections([{ node: 1 }, { node: 2 }, { node: 3 }, { node: 4 }]),
|
||||
(id) => outputs[id]
|
||||
);
|
||||
|
||||
assert.equal(result.image, "data:image/png;base64,one");
|
||||
assert.deepEqual(result.images, [
|
||||
"data:image/png;base64,one",
|
||||
"data:image/png;base64,two",
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const {
|
||||
clmmResolutionFromParamsV2,
|
||||
clmmSizeFromParamsV2,
|
||||
} = require("../electron/lib/canvas.js");
|
||||
|
||||
test("keeps Seedance 2K and 4K resolutions for upstream capability negotiation", () => {
|
||||
assert.equal(clmmResolutionFromParamsV2({ resolution: "2K" }), "2k");
|
||||
assert.equal(clmmResolutionFromParamsV2({ resolution: "4k" }), "4k");
|
||||
assert.equal(clmmResolutionFromParamsV2({ resolution: "unsupported" }), "720p");
|
||||
});
|
||||
|
||||
test("derives standard 2K and 4K video sizes from aspect ratio", () => {
|
||||
assert.equal(clmmSizeFromParamsV2({ ratio: "16:9" }, "2k"), "2560x1440");
|
||||
assert.equal(clmmSizeFromParamsV2({ ratio: "9:16" }, "2k"), "1440x2560");
|
||||
assert.equal(clmmSizeFromParamsV2({ ratio: "1:1" }, "4k"), "2160x2160");
|
||||
});
|
||||
|
||||
test("exposes 2K and 4K only on the Seedance video control", () => {
|
||||
const source = fs.readFileSync(path.resolve(__dirname, "../renderer/app.js"), "utf8");
|
||||
assert.match(source, /value="2k">2K(需上游支持)/);
|
||||
assert.match(source, /value="4k">4K(需上游支持)/);
|
||||
assert.match(source, /const resolutionOptions = isGrok/);
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const Module = require("node:module");
|
||||
|
||||
const DATA_DIR_MODULE = path.resolve(__dirname, "../electron/lib/codex_data_dir.js");
|
||||
const SESSIONS_MODULE = path.resolve(__dirname, "../electron/lib/sessions.js");
|
||||
|
||||
function loadModules(userData) {
|
||||
const originalLoad = Module._load;
|
||||
Module._load = function (request, parent, isMain) {
|
||||
if (request === "electron") return { app: { getPath: () => userData } };
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
delete require.cache[DATA_DIR_MODULE];
|
||||
delete require.cache[SESSIONS_MODULE];
|
||||
try {
|
||||
return { dataDir: require(DATA_DIR_MODULE), sessions: require(SESSIONS_MODULE) };
|
||||
} finally {
|
||||
Module._load = originalLoad;
|
||||
}
|
||||
}
|
||||
|
||||
function writeRollout(root, id, title) {
|
||||
const dir = path.join(root, "sessions", "2026", "08", "06");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, "rollout-test.jsonl"), [
|
||||
JSON.stringify({ type: "session_meta", payload: { session_id: id, cwd: "/work/demo", timestamp: "2026-08-06T10:00:00.000Z" } }),
|
||||
JSON.stringify({ type: "event_msg", payload: { type: "user_message", message: title } }),
|
||||
].join("\n") + "\n");
|
||||
}
|
||||
|
||||
test("会话数据目录切换后列表读取新目录,配置可持久化", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-market-data-dir-"));
|
||||
const userData = path.join(root, "user-data");
|
||||
const first = path.join(root, "first-codex");
|
||||
const second = path.join(root, "second-codex");
|
||||
fs.mkdirSync(userData, { recursive: true });
|
||||
writeRollout(first, "11111111-1111-4111-8111-111111111111", "第一份记录");
|
||||
writeRollout(second, "22222222-2222-4222-8222-222222222222", "第二份记录");
|
||||
|
||||
try {
|
||||
let { dataDir, sessions } = loadModules(userData);
|
||||
assert.throws(() => dataDir.set(path.join(root, "not-codex")), /Codex 数据/);
|
||||
assert.equal(dataDir.set(first).path, first);
|
||||
assert.equal(sessions.getCodexHome(), first);
|
||||
assert.deepEqual(sessions.listConversations().map((row) => row.title), ["第一份记录"]);
|
||||
|
||||
assert.equal(dataDir.set(second).path, second);
|
||||
assert.equal(sessions.getCodexHome(), second);
|
||||
assert.deepEqual(sessions.listConversations().map((row) => row.title), ["第二份记录"]);
|
||||
|
||||
({ dataDir, sessions } = loadModules(userData));
|
||||
assert.equal(dataDir.info().path, second);
|
||||
assert.deepEqual(sessions.listConversations().map((row) => row.title), ["第二份记录"]);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const media = require("../electron/lib/media-understanding");
|
||||
|
||||
test("collectAttachments finds a Responses image part", () => {
|
||||
const attachments = media.collectAttachments({
|
||||
input: [{
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: [{ type: "input_image", image_url: "data:image/png;base64,abc" }],
|
||||
}],
|
||||
});
|
||||
assert.equal(attachments.length, 1);
|
||||
assert.equal(attachments[0].kind, "image");
|
||||
});
|
||||
|
||||
test("collectAttachments finds standard Responses input_audio data", () => {
|
||||
const attachments = media.collectAttachments({
|
||||
input: [{
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: [{
|
||||
type: "input_audio",
|
||||
input_audio: { data: "YXVkaW8=", format: "wav" },
|
||||
}],
|
||||
}],
|
||||
});
|
||||
assert.equal(attachments.length, 1);
|
||||
assert.equal(attachments[0].kind, "audio");
|
||||
assert.equal(attachments[0].name, "audio.wav");
|
||||
assert.equal(attachments[0].value, "YXVkaW8=");
|
||||
});
|
||||
|
||||
test("enrichResponsesRequest injects MiniMax evidence without exposing credentials", async () => {
|
||||
const previousKey = process.env.TD_MULTIMODAL_GATEWAY_KEY;
|
||||
process.env.TD_MULTIMODAL_GATEWAY_KEY = "test-key";
|
||||
const request = {
|
||||
input: [{
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: [{ type: "input_image", image_url: "data:image/png;base64,abc" }],
|
||||
}],
|
||||
};
|
||||
try {
|
||||
const result = await media.enrichResponsesRequest(request, {
|
||||
instruction: "图片里写了什么?",
|
||||
fetchImpl: async () => ({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
choices: [{
|
||||
message: {
|
||||
content: '{"summary":"界面截图","items":[{"kind":"ocr","text":"设置","confidence":0.99}]}',
|
||||
},
|
||||
}],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
assert.equal(result.processed, true);
|
||||
const last = request.input[0].content.at(-1);
|
||||
assert.equal(last.type, "input_text");
|
||||
assert.match(last.text, /界面截图/);
|
||||
assert.doesNotMatch(last.text, /test-key/);
|
||||
} finally {
|
||||
if (previousKey == null) delete process.env.TD_MULTIMODAL_GATEWAY_KEY;
|
||||
else process.env.TD_MULTIMODAL_GATEWAY_KEY = previousKey;
|
||||
}
|
||||
});
|
||||
|
||||
test("enrichResponsesRequest reports an unavailable gateway instead of empty image evidence", async () => {
|
||||
const previousKey = process.env.TD_MULTIMODAL_GATEWAY_KEY;
|
||||
delete process.env.TD_MULTIMODAL_GATEWAY_KEY;
|
||||
const request = {
|
||||
input: [{
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: [{ type: "input_image", image_url: "data:image/png;base64,abc" }],
|
||||
}],
|
||||
};
|
||||
try {
|
||||
const result = await media.enrichResponsesRequest(request);
|
||||
assert.match(result.evidence.summary, /尚未注入访问密钥/);
|
||||
assert.match(request.input[0].content.at(-1).text, /不能识别图片内容/);
|
||||
} finally {
|
||||
if (previousKey == null) delete process.env.TD_MULTIMODAL_GATEWAY_KEY;
|
||||
else process.env.TD_MULTIMODAL_GATEWAY_KEY = previousKey;
|
||||
}
|
||||
});
|
||||
+1238
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const Module = require("node:module");
|
||||
const childProcess = require("node:child_process");
|
||||
const initSqlJs = require("sql.js");
|
||||
|
||||
const PROVIDER_MODULE = path.resolve(__dirname, "../electron/lib/provider_sync.js");
|
||||
const DESKTOP_MODULE = path.resolve(__dirname, "../electron/lib/codex_desktop.js");
|
||||
|
||||
function writeRollout(root, id) {
|
||||
const dir = path.join(root, "sessions", "2026", "08", "09");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, `rollout-${id}.jsonl`), [
|
||||
JSON.stringify({ type: "session_meta", payload: { session_id: id, cwd: "C:/work/demo", timestamp: "2026-08-09T10:00:00.000Z", model_provider: "old" } }),
|
||||
JSON.stringify({ type: "event_msg", payload: { type: "user_message", message: "Windows restore test" } }),
|
||||
].join("\n") + "\n");
|
||||
}
|
||||
|
||||
// subagent 会话的 session_meta.payload.source 是对象({subagent:{thread_spawn:{...}}}),
|
||||
// 不是字符串。曾导致 Windows(无 sqlite3 CLI,走 sql.js 绑定)整批 upsert 抛错回滚,
|
||||
// state 库 0 线程而侧栏 catalog 正常,Codex 启动 full reconcile 后会话"闪现后全部消失"。
|
||||
function writeSubagentRollout(root, id, parentId) {
|
||||
const dir = path.join(root, "sessions", "2026", "08", "09");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, `rollout-${id}.jsonl`), [
|
||||
JSON.stringify({
|
||||
type: "session_meta",
|
||||
payload: {
|
||||
session_id: id,
|
||||
cwd: "C:/work/demo",
|
||||
timestamp: "2026-08-09T11:00:00.000Z",
|
||||
model_provider: "old",
|
||||
source: { subagent: { thread_spawn: { parent_thread_id: parentId, depth: 1, agent_path: null, agent_nickname: "Dirac", agent_role: null } } },
|
||||
thread_source: "subagent",
|
||||
},
|
||||
}),
|
||||
JSON.stringify({ type: "event_msg", payload: { type: "user_message", message: "subagent task" } }),
|
||||
].join("\n") + "\n");
|
||||
}
|
||||
|
||||
test("会话恢复在没有 sqlite3 时插入线程并创建侧栏目录", { concurrency: false }, async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-session-restore-"));
|
||||
const dataDir = path.join(root, "codex");
|
||||
const userData = path.join(root, "app-data");
|
||||
const id = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
|
||||
const originalLoad = Module._load;
|
||||
const originalSpawnSync = childProcess.spawnSync;
|
||||
const originalBundleRoot = global.__BUNDLE_ROOT__;
|
||||
|
||||
try {
|
||||
fs.mkdirSync(userData, { recursive: true });
|
||||
fs.mkdirSync(path.join(dataDir, "sessions"), { recursive: true });
|
||||
fs.writeFileSync(path.join(dataDir, "config.toml"), 'model_provider = "openai"\n');
|
||||
writeRollout(dataDir, id);
|
||||
|
||||
Module._load = function (request, parent, isMain) {
|
||||
if (request === "electron") return { app: { getPath: () => userData } };
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
childProcess.spawnSync = function (command, args, options) {
|
||||
if (command === "sqlite3") return { error: new Error("sqlite3 not installed"), status: null, stdout: "", stderr: "" };
|
||||
return originalSpawnSync.call(this, command, args, options);
|
||||
};
|
||||
delete require.cache[PROVIDER_MODULE];
|
||||
delete require.cache[DESKTOP_MODULE];
|
||||
global.__BUNDLE_ROOT__ = path.resolve(__dirname, "..");
|
||||
const provider = require(PROVIDER_MODULE);
|
||||
const desktop = require(DESKTOP_MODULE);
|
||||
desktop.codexRunning = () => false;
|
||||
provider.setCodexDataDir(dataDir);
|
||||
|
||||
const result = await provider.syncProviderToCurrent(() => {});
|
||||
assert.equal(result.insertedRows, 1);
|
||||
assert.equal(result.catalogRebuilt, 1);
|
||||
assert.equal(result.stateDbCreated, 1);
|
||||
assert.equal(result.stateDbCreateError, "");
|
||||
assert.equal(fs.existsSync(path.join(dataDir, "sqlite", "codex-dev.db")), true);
|
||||
assert.equal(fs.existsSync(path.join(dataDir, "session_index.jsonl")), true);
|
||||
assert.equal(fs.existsSync(path.join(dataDir, "state_5.sqlite")), true);
|
||||
|
||||
const SQL = await initSqlJs({ locateFile: (file) => path.join(__dirname, "../node_modules/sql.js/dist", file) });
|
||||
const restoredState = new SQL.Database(fs.readFileSync(path.join(dataDir, "state_5.sqlite")));
|
||||
assert.equal(restoredState.exec("SELECT COUNT(*) FROM threads")[0].values[0][0], 1);
|
||||
assert.equal(restoredState.exec("SELECT has_user_event FROM threads WHERE id='" + id + "'")[0].values[0][0], 1);
|
||||
assert.equal(restoredState.exec("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN ('thread_sections','thread_dynamic_tools','thread_spawn_edges','backfill_state','external_agent_config_imports','remote_control_enrollments')")[0].values[0][0], 6);
|
||||
assert.equal(restoredState.exec("SELECT COUNT(*) FROM _sqlx_migrations")[0].values[0][0], 45);
|
||||
restoredState.close();
|
||||
|
||||
const catalog = new SQL.Database(fs.readFileSync(path.join(dataDir, "sqlite", "codex-dev.db")));
|
||||
assert.equal(catalog.exec("SELECT COUNT(*) FROM local_thread_catalog")[0].values[0][0], 1);
|
||||
catalog.close();
|
||||
} finally {
|
||||
Module._load = originalLoad;
|
||||
childProcess.spawnSync = originalSpawnSync;
|
||||
global.__BUNDLE_ROOT__ = originalBundleRoot;
|
||||
delete require.cache[PROVIDER_MODULE];
|
||||
delete require.cache[DESKTOP_MODULE];
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("subagent source 对象不再让 Windows(sql.js) 路径整批失败,且自建 catalog 库带 user_version", { concurrency: false }, async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-session-restore-"));
|
||||
const dataDir = path.join(root, "codex");
|
||||
const userData = path.join(root, "app-data");
|
||||
const id = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
|
||||
const subId = "cccccccc-cccc-4ccc-8ccc-cccccccccccc";
|
||||
const originalLoad = Module._load;
|
||||
const originalSpawnSync = childProcess.spawnSync;
|
||||
const originalBundleRoot = global.__BUNDLE_ROOT__;
|
||||
|
||||
try {
|
||||
fs.mkdirSync(userData, { recursive: true });
|
||||
fs.mkdirSync(path.join(dataDir, "sessions"), { recursive: true });
|
||||
fs.writeFileSync(path.join(dataDir, "config.toml"), 'model_provider = "openai"\n');
|
||||
writeRollout(dataDir, id);
|
||||
writeSubagentRollout(dataDir, subId, id);
|
||||
|
||||
Module._load = function (request, parent, isMain) {
|
||||
if (request === "electron") return { app: { getPath: () => userData } };
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
childProcess.spawnSync = function (command, args, options) {
|
||||
if (command === "sqlite3") return { error: new Error("sqlite3 not installed"), status: null, stdout: "", stderr: "" };
|
||||
return originalSpawnSync.call(this, command, args, options);
|
||||
};
|
||||
delete require.cache[PROVIDER_MODULE];
|
||||
delete require.cache[DESKTOP_MODULE];
|
||||
global.__BUNDLE_ROOT__ = path.resolve(__dirname, "..");
|
||||
const provider = require(PROVIDER_MODULE);
|
||||
const desktop = require(DESKTOP_MODULE);
|
||||
desktop.codexRunning = () => false;
|
||||
provider.setCodexDataDir(dataDir);
|
||||
|
||||
const result = await provider.syncProviderToCurrent(() => {}, () => {});
|
||||
// 两条线程都必须进 state 库(修复前 subagent 的 source 对象会让整批回滚,insertedRows=0)
|
||||
assert.equal(result.insertedRows, 2);
|
||||
|
||||
const SQL = await initSqlJs({ locateFile: (file) => path.join(__dirname, "../node_modules/sql.js/dist", file) });
|
||||
const state = new SQL.Database(fs.readFileSync(path.join(dataDir, "state_5.sqlite")));
|
||||
assert.equal(state.exec("SELECT COUNT(*) FROM threads")[0].values[0][0], 2);
|
||||
// subagent 的 source 以 JSON 字符串入库(与 Codex 自身存储一致),而不是 '[object Object]'
|
||||
const subSource = state.exec(`SELECT source FROM threads WHERE id='${subId}'`)[0].values[0][0];
|
||||
assert.ok(subSource.startsWith('{"subagent"'), `source 应为 JSON 字符串,实际: ${subSource}`);
|
||||
assert.ok(subSource.includes("Dirac"));
|
||||
state.close();
|
||||
|
||||
// 自建 codex-dev.db 必须带 user_version(对齐 Codex 迁移链 v30),
|
||||
// 否则 Codex 重跑迁移 v27/28/29 会置空 last_full_reconciled_at 触发 full reconcile
|
||||
const catalog = new SQL.Database(fs.readFileSync(path.join(dataDir, "sqlite", "codex-dev.db")));
|
||||
const uv = catalog.exec("PRAGMA user_version")[0].values[0][0];
|
||||
assert.equal(uv, 30);
|
||||
assert.equal(catalog.exec("SELECT COUNT(*) FROM local_thread_catalog")[0].values[0][0], 2);
|
||||
catalog.close();
|
||||
} finally {
|
||||
Module._load = originalLoad;
|
||||
childProcess.spawnSync = originalSpawnSync;
|
||||
global.__BUNDLE_ROOT__ = originalBundleRoot;
|
||||
delete require.cache[PROVIDER_MODULE];
|
||||
delete require.cache[DESKTOP_MODULE];
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const Module = require("node:module");
|
||||
|
||||
function loadTeam() {
|
||||
const originalLoad = Module._load;
|
||||
Module._load = function load(request, parent, isMain) {
|
||||
if (request === "electron") {
|
||||
return {
|
||||
app: { getPath: () => "/tmp/codex-market-team-test" },
|
||||
safeStorage: { isEncryptionAvailable: () => false },
|
||||
};
|
||||
}
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
try {
|
||||
delete require.cache[require.resolve("../electron/lib/team")];
|
||||
delete require.cache[require.resolve("../electron/lib/router")];
|
||||
return require("../electron/lib/team");
|
||||
} finally {
|
||||
Module._load = originalLoad;
|
||||
}
|
||||
}
|
||||
|
||||
test("team package keeps generation and runtime models separate", () => {
|
||||
const team = loadTeam();
|
||||
const files = team.packFiles({
|
||||
project_name: "model-routing",
|
||||
employees: [{
|
||||
id: "worker",
|
||||
name: "执行 Agent",
|
||||
duty: "实现与测试",
|
||||
model: "deepseek-v4-flash",
|
||||
responsibilities: [],
|
||||
constraints: [],
|
||||
owner_paths: [],
|
||||
review_paths: [],
|
||||
allowed_paths: ["**"],
|
||||
collaborators: [],
|
||||
}],
|
||||
orchestrator: {
|
||||
id: "orchestrator",
|
||||
name: "总控 Agent",
|
||||
duty: "调度与验收",
|
||||
model: "gpt-5.6-luna",
|
||||
responsibilities: [],
|
||||
constraints: [],
|
||||
owner_paths: [],
|
||||
review_paths: ["**"],
|
||||
allowed_paths: [],
|
||||
workflow: [],
|
||||
},
|
||||
model_routing: {
|
||||
generation_model: "gpt-5.4-mini",
|
||||
orchestrator_model: "gpt-5.6-luna",
|
||||
worker_model: "deepseek-v4-flash",
|
||||
},
|
||||
}, "codex");
|
||||
|
||||
const orchestrator = files.find((file) => file.path === "agents/_orchestrator.toml").content;
|
||||
const worker = files.find((file) => file.path === "agents/worker.toml").content;
|
||||
const manifest = JSON.parse(files.find((file) => file.path === "agent-manifest.json").content);
|
||||
assert.match(orchestrator, /model = "gpt-5\.6-luna"/);
|
||||
assert.match(worker, /model = "deepseek-v4-flash"/);
|
||||
assert.match(worker, /TD_MODEL_ROUTE model="deepseek-v4-flash" role="worker" agent="worker"/);
|
||||
assert.doesNotMatch(orchestrator, /gpt-5\.4-mini/);
|
||||
assert.deepEqual(manifest.model_routing, {
|
||||
generation: "gpt-5.4-mini",
|
||||
orchestrator: "gpt-5.6-luna",
|
||||
workers: ["deepseek-v4-flash"],
|
||||
capability_policy: "",
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const { TOKEN_INJECT_SCRIPT } = require("../electron/lib/token_inject");
|
||||
|
||||
test("token injection script remains compilable", () => {
|
||||
assert.doesNotThrow(() => new Function(TOKEN_INJECT_SCRIPT));
|
||||
assert.doesNotMatch(TOKEN_INJECT_SCRIPT, /const VER = "td-token-v17"/);
|
||||
assert.doesNotMatch(TOKEN_INJECT_SCRIPT, /const STYLE_ID = "td-token-style-v17"/);
|
||||
});
|
||||
|
||||
test("token bar waits for a visible composer toolbar instead of falling back to the page root", () => {
|
||||
assert.match(TOKEN_INJECT_SCRIPT, /const EDITOR_SELECTOR = "DIV\.ProseMirror/);
|
||||
assert.match(TOKEN_INJECT_SCRIPT, /if \(!toolbar\) return false;[\s\S]*?const bar = document\.getElementById\(BAR_ID\) \|\| buildBar\(\);/);
|
||||
assert.match(TOKEN_INJECT_SCRIPT, /if \(!toolbar\.contains\(bar\)\) return placeInToolbar\(bar, toolbar\);/);
|
||||
assert.doesNotMatch(TOKEN_INJECT_SCRIPT, /centerUnderEditor\(/);
|
||||
assert.doesNotMatch(TOKEN_INJECT_SCRIPT, /document\.body \|\| document\.documentElement\)\.appendChild\(bar\)/);
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const { responsesToChat } = require("../electron/lib/translate");
|
||||
|
||||
test("chat projection keeps completed custom tool results without forwarding tool protocol", () => {
|
||||
const result = responsesToChat({
|
||||
model: "codex-model",
|
||||
stream: true,
|
||||
tool_choice: "auto",
|
||||
input: [
|
||||
{ type: "additional_tools", role: "system", tools: [{ name: "exec_command" }] },
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "识别这张图" }] },
|
||||
{ type: "custom_tool_call", call_id: "call_1", name: "exec_command", input: "tesseract image.png" },
|
||||
{ type: "custom_tool_call_output", call_id: "call_1", output: [{ type: "text", text: "识别结果:设置" }] },
|
||||
{
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "input_image", image_url: "data:image/png;base64,abc" },
|
||||
{ type: "input_text", text: "[TD 多模态识别证据] 设置页面" },
|
||||
],
|
||||
},
|
||||
],
|
||||
}, "deepseek-chat");
|
||||
|
||||
assert.equal(result.model, "deepseek-chat");
|
||||
assert.equal(result.tools, undefined);
|
||||
assert.equal(result.tool_choice, undefined);
|
||||
assert.match(result.messages[0].content, /不能调用本地工具/);
|
||||
assert.ok(result.messages.some((message) => /工具:exec_command/.test(message.content)));
|
||||
assert.ok(result.messages.some((message) => /识别结果:设置/.test(message.content)));
|
||||
assert.ok(result.messages.some((message) => /TD 多模态识别证据/.test(message.content)));
|
||||
assert.ok(result.messages.every((message) => message.role !== "tool" && !message.tool_calls));
|
||||
});
|
||||
|
||||
test("chat projection forwards native tools only when they are supplied", () => {
|
||||
const withoutTools = responsesToChat({ model: "model", tool_choice: "auto", input: "你好" });
|
||||
assert.equal(withoutTools.tool_choice, undefined);
|
||||
|
||||
const withTools = responsesToChat({
|
||||
model: "model",
|
||||
tool_choice: "auto",
|
||||
tools: [{ type: "function", name: "lookup", parameters: { type: "object" } }],
|
||||
input: "你好",
|
||||
});
|
||||
assert.equal(withTools.tool_choice, "auto");
|
||||
assert.equal(withTools.tools[0].function.name, "lookup");
|
||||
});
|
||||
|
||||
test("chat projection forwards Codex additional_tools so coding agents can execute", () => {
|
||||
const result = responsesToChat({
|
||||
model: "deepseek-chat",
|
||||
tool_choice: "auto",
|
||||
input: [
|
||||
{
|
||||
type: "additional_tools",
|
||||
role: "developer",
|
||||
tools: [{
|
||||
type: "function",
|
||||
name: "exec_command",
|
||||
description: "Run a shell command",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { cmd: { type: "string" } },
|
||||
required: ["cmd"],
|
||||
},
|
||||
}],
|
||||
},
|
||||
{ type: "message", role: "user", content: "发布热更" },
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(result.tool_choice, "auto");
|
||||
assert.equal(result.tools.length, 1);
|
||||
assert.equal(result.tools[0].function.name, "exec_command");
|
||||
assert.doesNotMatch(result.messages[0].content, /不能调用本地工具/);
|
||||
});
|
||||
|
||||
test("multimodal chat projection removes image reading but keeps coding tools", () => {
|
||||
const result = responsesToChat({
|
||||
model: "model",
|
||||
tool_choice: "auto",
|
||||
tools: [
|
||||
{ type: "function", name: "exec_command", parameters: { type: "object" } },
|
||||
{ type: "function", name: "view_image", parameters: { type: "object" } },
|
||||
{ type: "web_search_preview" },
|
||||
],
|
||||
input: "识别图片",
|
||||
}, undefined, { omitVisualTools: true });
|
||||
|
||||
assert.equal(result.tool_choice, "auto");
|
||||
assert.deepEqual(result.tools.map((tool) => tool.function.name), ["exec_command"]);
|
||||
});
|
||||
|
||||
test("chat projection never forwards media Base64 from tool messages", () => {
|
||||
const image = "data:image/png;base64," + "A".repeat(2 * 1024 * 1024);
|
||||
const result = responsesToChat({
|
||||
model: "deepseek-chat",
|
||||
input: [{
|
||||
type: "message",
|
||||
role: "tool",
|
||||
content: [{ type: "input_image", image_url: image }],
|
||||
}],
|
||||
});
|
||||
|
||||
const content = result.messages.map((message) => String(message.content || "")).join("\n");
|
||||
assert.match(content, /图像二进制已省略/);
|
||||
assert.doesNotMatch(content, /data:image/);
|
||||
assert.ok(result.messages.every((message) => message.role !== "tool"));
|
||||
assert.ok(content.length < 1000);
|
||||
});
|
||||
Reference in New Issue
Block a user