feat: add omx team adapter skeleton
This commit is contained in:
@@ -18,3 +18,10 @@ BOSS_SENDMAIL_PATH=/usr/sbin/sendmail
|
|||||||
# BOSS_CLAW_WORKDIR=/opt/boss
|
# BOSS_CLAW_WORKDIR=/opt/boss
|
||||||
# BOSS_CLAW_TIMEOUT_MS=45000
|
# BOSS_CLAW_TIMEOUT_MS=45000
|
||||||
# BOSS_CLAW_DEFAULT_MODEL=gpt-5.4
|
# BOSS_CLAW_DEFAULT_MODEL=gpt-5.4
|
||||||
|
|
||||||
|
# 可选:启用 OmxTeamBackendAdapter(默认关闭)
|
||||||
|
# BOSS_OMX_ENABLED=true
|
||||||
|
# BOSS_OMX_COMMAND=node
|
||||||
|
# BOSS_OMX_ARGS=scripts/omx-team-smoke.mjs
|
||||||
|
# BOSS_OMX_WORKDIR=/opt/boss
|
||||||
|
# BOSS_OMX_TIMEOUT_MS=45000
|
||||||
|
|||||||
@@ -59,7 +59,8 @@
|
|||||||
- 当前 Boss 已新增 `src/lib/execution/` 执行底座抽象层;当前生产主链仍然沿用 `local-agent -> codex exec resume`,只是执行责任已开始通过 `ExecutionBackend / PromptAssembler / PermissionPolicy / RemoteRuntimeAdapter / OrchestrationBackend` 默认实现收束
|
- 当前 Boss 已新增 `src/lib/execution/` 执行底座抽象层;当前生产主链仍然沿用 `local-agent -> codex exec resume`,只是执行责任已开始通过 `ExecutionBackend / PromptAssembler / PermissionPolicy / RemoteRuntimeAdapter / OrchestrationBackend` 默认实现收束
|
||||||
- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有在显式配置 `BOSS_CLAW_*` 且可用性探测通过时,`master-agent` 当前对话里才会出现并允许选择 `claw-runtime`
|
- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有在显式配置 `BOSS_CLAW_*` 且可用性探测通过时,`master-agent` 当前对话里才会出现并允许选择 `claw-runtime`
|
||||||
- 如果历史上已经保存过 `backendOverride=claw-runtime`,但当前 `Claw Runtime` 不可用,运行时会自动回退到默认后端,并在前台显示明确原因
|
- 如果历史上已经保存过 `backendOverride=claw-runtime`,但当前 `Claw Runtime` 不可用,运行时会自动回退到默认后端,并在前台显示明确原因
|
||||||
- 当前 `oh-my-codex` 仍未正式接入生产执行链;当前状态是 orchestration-ready,后续将通过独立 adapter 接入
|
- 当前 `oh-my-codex` 已以最小 `OmxTeamBackendAdapter` 形式接入执行底座,但默认关闭;目前只完成编排后端骨架、selector 与 smoke runtime,还没有接入生产群聊/审批主链
|
||||||
|
- 当前仓库已自带一个本地 OMX smoke runtime:`scripts/omx-team-smoke.mjs`。在还没有真实 `oh-my-codex` 可执行文件时,可以先用它验证 `OmxTeamBackendAdapter -> selector -> fallback` 这条骨架链
|
||||||
- 当前仓库已自带一个本地 smoke runtime:`scripts/claw-runtime-smoke.mjs`。在还没有真实 `claw-code` 可执行文件时,可以先用它验证 `ClawBackendAdapter -> backendOverride -> 异步回流` 整条链
|
- 当前仓库已自带一个本地 smoke runtime:`scripts/claw-runtime-smoke.mjs`。在还没有真实 `claw-code` 可执行文件时,可以先用它验证 `ClawBackendAdapter -> backendOverride -> 异步回流` 整条链
|
||||||
- `GET http://127.0.0.1:4317/api/v1/skills` 正常,已返回本机扫描到的 Codex Skill
|
- `GET http://127.0.0.1:4317/api/v1/skills` 正常,已返回本机扫描到的 Codex Skill
|
||||||
- `POST http://127.0.0.1:4317/api/v1/heartbeat` 正常,且会顺带触发 `thread-context` 上报
|
- `POST http://127.0.0.1:4317/api/v1/heartbeat` 正常,且会顺带触发 `thread-context` 上报
|
||||||
|
|||||||
@@ -180,7 +180,8 @@
|
|||||||
- 当前已最小接入 `ClawBackendAdapter`,但默认关闭,仅在显式配置且可用性探测通过时才参与执行
|
- 当前已最小接入 `ClawBackendAdapter`,但默认关闭,仅在显式配置且可用性探测通过时才参与执行
|
||||||
- 如果历史 `backendOverride=claw-runtime` 当前不可用,运行时会自动回退到默认后端,并把原因回给前台
|
- 如果历史 `backendOverride=claw-runtime` 当前不可用,运行时会自动回退到默认后端,并把原因回给前台
|
||||||
- 当前仓库自带 `scripts/claw-runtime-smoke.mjs` 作为兼容 JSON 协议的 smoke runtime,可用于本地和服务器验证 `ClawBackendAdapter`
|
- 当前仓库自带 `scripts/claw-runtime-smoke.mjs` 作为兼容 JSON 协议的 smoke runtime,可用于本地和服务器验证 `ClawBackendAdapter`
|
||||||
- 当前尚未接入 `oh-my-codex`
|
- 当前已最小接入 `OmxTeamBackendAdapter`,但默认关闭,仅提供编排后端骨架、selector 与 smoke runtime
|
||||||
|
- 当前仓库自带 `scripts/omx-team-smoke.mjs`,可用于本地和服务器验证 `OmxTeamBackendAdapter`
|
||||||
|
|
||||||
### 3.2 认证相关
|
### 3.2 认证相关
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有显式配置 `BOSS_CLAW_*` 且可用性探测通过时,`master-agent` 当前对话中才会出现并允许选择 `claw-runtime`
|
- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有显式配置 `BOSS_CLAW_*` 且可用性探测通过时,`master-agent` 当前对话中才会出现并允许选择 `claw-runtime`
|
||||||
- 如果历史上已经保存过 `backendOverride=claw-runtime`,但当前 `Claw Runtime` 不可用,运行时会自动回退到默认后端,并在 Web/Android 前台给出明确原因
|
- 如果历史上已经保存过 `backendOverride=claw-runtime`,但当前 `Claw Runtime` 不可用,运行时会自动回退到默认后端,并在 Web/Android 前台给出明确原因
|
||||||
- 当前仓库已自带 `scripts/claw-runtime-smoke.mjs` 作为本地 smoke runtime;在没有真实 `claw-code` 可执行文件时,可先用 `BOSS_CLAW_COMMAND=node` 与 `BOSS_CLAW_ARGS=scripts/claw-runtime-smoke.mjs` 验证整条链
|
- 当前仓库已自带 `scripts/claw-runtime-smoke.mjs` 作为本地 smoke runtime;在没有真实 `claw-code` 可执行文件时,可先用 `BOSS_CLAW_COMMAND=node` 与 `BOSS_CLAW_ARGS=scripts/claw-runtime-smoke.mjs` 验证整条链
|
||||||
- 当前 `oh-my-codex` 还未正式接入生产链,只是已经具备 orchestration adapter-ready 的 contract 基础
|
- 当前 `oh-my-codex` 已以最小 `OmxTeamBackendAdapter` 形式接入执行底座,但默认关闭;当前只完成编排后端骨架、selector 与 smoke runtime,还没有接入生产群聊/审批主链
|
||||||
|
- 当前仓库已自带 `scripts/omx-team-smoke.mjs` 作为本地 OMX smoke runtime;在没有真实 `oh-my-codex` 可执行文件时,可先用 `BOSS_OMX_COMMAND=node` 与 `BOSS_OMX_ARGS=scripts/omx-team-smoke.mjs` 验证编排后端骨架
|
||||||
|
|
||||||
本地已知运行方式:
|
本地已知运行方式:
|
||||||
|
|
||||||
|
|||||||
62
scripts/omx-team-smoke.mjs
Normal file
62
scripts/omx-team-smoke.mjs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
function writeJson(payload) {
|
||||||
|
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readStdin() {
|
||||||
|
const chunks = [];
|
||||||
|
for await (const chunk of process.stdin) {
|
||||||
|
chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
||||||
|
}
|
||||||
|
return chunks.join("").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePayload(raw) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "INVALID_OMX_PAYLOAD: expected object",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
payload: parsed,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "INVALID_OMX_PAYLOAD: invalid json",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = await readStdin();
|
||||||
|
const normalized = normalizePayload(raw);
|
||||||
|
|
||||||
|
if (!normalized.ok) {
|
||||||
|
writeJson({
|
||||||
|
status: "failed",
|
||||||
|
error: normalized.error,
|
||||||
|
});
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = normalized.payload;
|
||||||
|
const requestKind = typeof payload.requestKind === "string" ? payload.requestKind : "unknown";
|
||||||
|
const workersRequested =
|
||||||
|
typeof payload.workersRequested === "number" && Number.isFinite(payload.workersRequested)
|
||||||
|
? payload.workersRequested
|
||||||
|
: 1;
|
||||||
|
const objective =
|
||||||
|
typeof payload.objective === "string" && payload.objective.trim()
|
||||||
|
? payload.objective.trim()
|
||||||
|
: "OMX Team 链路正常";
|
||||||
|
|
||||||
|
writeJson({
|
||||||
|
status: "ready",
|
||||||
|
backendId: "omx-team",
|
||||||
|
summary: `OMX smoke ready: ${objective} (kind=${requestKind}, workers=${workersRequested})`,
|
||||||
|
});
|
||||||
37
src/lib/execution/backends/omx-team-backend.ts
Normal file
37
src/lib/execution/backends/omx-team-backend.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { OrchestrationBackend } from "@/lib/execution/orchestration-backend";
|
||||||
|
import {
|
||||||
|
getOmxTeamBackendAvailability,
|
||||||
|
getOmxTeamBackendConfig,
|
||||||
|
isOmxTeamBackendConfigured,
|
||||||
|
type OmxTeamBackendAvailability,
|
||||||
|
type OmxTeamBackendConfig,
|
||||||
|
} from "@/lib/execution/backends/omx-team-config";
|
||||||
|
|
||||||
|
export const OMX_TEAM_BACKEND_ID = "omx-team";
|
||||||
|
|
||||||
|
export const OMX_TEAM_BACKEND: OrchestrationBackend = {
|
||||||
|
backendId: OMX_TEAM_BACKEND_ID,
|
||||||
|
async describe() {
|
||||||
|
return {
|
||||||
|
backendId: OMX_TEAM_BACKEND_ID,
|
||||||
|
label: "OMX Team Runtime",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface OmxTeamBackendSelectionState {
|
||||||
|
enabled: boolean;
|
||||||
|
selectable: boolean;
|
||||||
|
availability: OmxTeamBackendAvailability;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOmxTeamBackendSelectionState(
|
||||||
|
config: OmxTeamBackendConfig = getOmxTeamBackendConfig(),
|
||||||
|
): Promise<OmxTeamBackendSelectionState> {
|
||||||
|
const availability = await getOmxTeamBackendAvailability(config);
|
||||||
|
return {
|
||||||
|
enabled: isOmxTeamBackendConfigured(config),
|
||||||
|
selectable: availability.selectable,
|
||||||
|
availability,
|
||||||
|
};
|
||||||
|
}
|
||||||
184
src/lib/execution/backends/omx-team-config.ts
Normal file
184
src/lib/execution/backends/omx-team-config.ts
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import { constants } from "node:fs";
|
||||||
|
import { access } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export interface OmxTeamBackendConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
command?: string;
|
||||||
|
args: string[];
|
||||||
|
cwd?: string;
|
||||||
|
timeoutMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OmxTeamBackendAvailabilityStatus = "disabled" | "misconfigured" | "ready";
|
||||||
|
|
||||||
|
export interface OmxTeamBackendAvailability {
|
||||||
|
status: OmxTeamBackendAvailabilityStatus;
|
||||||
|
selectable: boolean;
|
||||||
|
configured: boolean;
|
||||||
|
reason:
|
||||||
|
| "disabled"
|
||||||
|
| "command_not_set"
|
||||||
|
| "command_not_found"
|
||||||
|
| "workdir_not_found"
|
||||||
|
| "script_not_found"
|
||||||
|
| "ready";
|
||||||
|
reasonLabel: string;
|
||||||
|
command?: string;
|
||||||
|
cwd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBoolean(value: string | undefined) {
|
||||||
|
return value?.trim().toLowerCase() === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(value: string | undefined) {
|
||||||
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTimeoutMs(value: string | undefined) {
|
||||||
|
const parsed = Number.parseInt(value || "", 10);
|
||||||
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 45000;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOmxTeamBackendConfig(): OmxTeamBackendConfig {
|
||||||
|
return {
|
||||||
|
enabled: parseBoolean(process.env.BOSS_OMX_ENABLED),
|
||||||
|
command: process.env.BOSS_OMX_COMMAND?.trim() || undefined,
|
||||||
|
args: parseArgs(process.env.BOSS_OMX_ARGS),
|
||||||
|
cwd: process.env.BOSS_OMX_WORKDIR?.trim() || undefined,
|
||||||
|
timeoutMs: parseTimeoutMs(process.env.BOSS_OMX_TIMEOUT_MS),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOmxTeamBackendConfigured(config: OmxTeamBackendConfig) {
|
||||||
|
return config.enabled && Boolean(config.command);
|
||||||
|
}
|
||||||
|
|
||||||
|
function commandLooksLikePath(command: string) {
|
||||||
|
return command.includes("/") || command.includes("\\");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileExists(filePath: string, mode = constants.F_OK) {
|
||||||
|
try {
|
||||||
|
await access(filePath, mode);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveScriptCandidate(config: OmxTeamBackendConfig) {
|
||||||
|
if (!config.command || config.args.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandName = path.basename(config.command).toLowerCase();
|
||||||
|
const scriptRuntimes = new Set(["node", "node.exe", "tsx", "tsx.cmd", "bun", "bun.exe", "deno", "deno.exe"]);
|
||||||
|
if (!scriptRuntimes.has(commandName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidate = config.args[0];
|
||||||
|
if (!candidate || candidate.startsWith("-")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.isAbsolute(candidate)
|
||||||
|
? candidate
|
||||||
|
: path.resolve(config.cwd?.trim() || process.cwd(), candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isCommandReachable(command: string) {
|
||||||
|
if (commandLooksLikePath(command)) {
|
||||||
|
return fileExists(path.resolve(command), constants.X_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchPaths = (process.env.PATH || "")
|
||||||
|
.split(path.delimiter)
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
for (const entry of searchPaths) {
|
||||||
|
const candidate = path.join(entry, command);
|
||||||
|
if (await fileExists(candidate, constants.X_OK)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOmxTeamBackendAvailability(
|
||||||
|
config: OmxTeamBackendConfig = getOmxTeamBackendConfig(),
|
||||||
|
): Promise<OmxTeamBackendAvailability> {
|
||||||
|
const base = {
|
||||||
|
command: config.command,
|
||||||
|
cwd: config.cwd,
|
||||||
|
configured: isOmxTeamBackendConfigured(config),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!config.enabled) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
status: "disabled",
|
||||||
|
selectable: false,
|
||||||
|
reason: "disabled",
|
||||||
|
reasonLabel: "OMX Team Runtime 当前未启用。",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.command) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
status: "misconfigured",
|
||||||
|
selectable: false,
|
||||||
|
reason: "command_not_set",
|
||||||
|
reasonLabel: "OMX Team Runtime 缺少启动命令。",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await isCommandReachable(config.command))) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
status: "misconfigured",
|
||||||
|
selectable: false,
|
||||||
|
reason: "command_not_found",
|
||||||
|
reasonLabel: "未检测到可执行的 OMX Team 启动命令。",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.cwd && !(await fileExists(config.cwd, constants.F_OK))) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
status: "misconfigured",
|
||||||
|
selectable: false,
|
||||||
|
reason: "workdir_not_found",
|
||||||
|
reasonLabel: "OMX Team Runtime 工作目录不存在。",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const scriptCandidate = resolveScriptCandidate(config);
|
||||||
|
if (scriptCandidate && !(await fileExists(scriptCandidate, constants.F_OK))) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
status: "misconfigured",
|
||||||
|
selectable: false,
|
||||||
|
reason: "script_not_found",
|
||||||
|
reasonLabel: "未检测到有效的 OMX Team 启动脚本,将自动回退到 Boss Native Orchestrator。",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
status: "ready",
|
||||||
|
selectable: true,
|
||||||
|
reason: "ready",
|
||||||
|
reasonLabel: "OMX Team Runtime 可用。",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOmxTeamBackendConfigForTesting = getOmxTeamBackendConfig;
|
||||||
|
export const isOmxTeamBackendConfiguredForTesting = isOmxTeamBackendConfigured;
|
||||||
|
export const getOmxTeamBackendAvailabilityForTesting = getOmxTeamBackendAvailability;
|
||||||
63
src/lib/execution/orchestration-backend-selector.ts
Normal file
63
src/lib/execution/orchestration-backend-selector.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { BOSS_NATIVE_ORCHESTRATOR } from "@/lib/execution/backends/boss-native-orchestrator";
|
||||||
|
import {
|
||||||
|
OMX_TEAM_BACKEND,
|
||||||
|
type OmxTeamBackendSelectionState,
|
||||||
|
} from "@/lib/execution/backends/omx-team-backend";
|
||||||
|
import type { OrchestrationBackend } from "@/lib/execution/orchestration-backend";
|
||||||
|
|
||||||
|
export interface OrchestrationBackendSelectionInput {
|
||||||
|
requestedBackendId?: string;
|
||||||
|
omx?: OmxTeamBackendSelectionState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrchestrationBackendChoice =
|
||||||
|
| typeof BOSS_NATIVE_ORCHESTRATOR
|
||||||
|
| typeof OMX_TEAM_BACKEND;
|
||||||
|
|
||||||
|
function isReadyBackend(
|
||||||
|
backend: OrchestrationBackend,
|
||||||
|
input: OrchestrationBackendSelectionInput,
|
||||||
|
) {
|
||||||
|
if (backend.backendId === OMX_TEAM_BACKEND.backendId) {
|
||||||
|
return input.omx?.selectable ?? false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function selectOrchestrationBackend(
|
||||||
|
input: OrchestrationBackendSelectionInput = {},
|
||||||
|
): Promise<OrchestrationBackendChoice> {
|
||||||
|
return (await listOrchestrationBackendChoices(input))[0] ?? BOSS_NATIVE_ORCHESTRATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listOrchestrationBackendChoices(
|
||||||
|
input: OrchestrationBackendSelectionInput = {},
|
||||||
|
): Promise<OrchestrationBackendChoice[]> {
|
||||||
|
const ordered: OrchestrationBackendChoice[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
const pushBackend = (backend: OrchestrationBackendChoice) => {
|
||||||
|
if (seen.has(backend.backendId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ordered.push(backend);
|
||||||
|
seen.add(backend.backendId);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
input.requestedBackendId === OMX_TEAM_BACKEND.backendId &&
|
||||||
|
isReadyBackend(OMX_TEAM_BACKEND, input)
|
||||||
|
) {
|
||||||
|
pushBackend(OMX_TEAM_BACKEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
pushBackend(BOSS_NATIVE_ORCHESTRATOR);
|
||||||
|
|
||||||
|
if (isReadyBackend(OMX_TEAM_BACKEND, input)) {
|
||||||
|
pushBackend(OMX_TEAM_BACKEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectOrchestrationBackendForTesting = selectOrchestrationBackend;
|
||||||
110
tests/omx-team-backend-config.test.ts
Normal file
110
tests/omx-team-backend-config.test.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||||
|
import {
|
||||||
|
getOmxTeamBackendAvailabilityForTesting,
|
||||||
|
getOmxTeamBackendConfigForTesting,
|
||||||
|
isOmxTeamBackendConfiguredForTesting,
|
||||||
|
} from "@/lib/execution/backends/omx-team-config";
|
||||||
|
|
||||||
|
function snapshotEnv() {
|
||||||
|
return {
|
||||||
|
BOSS_OMX_ENABLED: process.env.BOSS_OMX_ENABLED,
|
||||||
|
BOSS_OMX_COMMAND: process.env.BOSS_OMX_COMMAND,
|
||||||
|
BOSS_OMX_ARGS: process.env.BOSS_OMX_ARGS,
|
||||||
|
BOSS_OMX_WORKDIR: process.env.BOSS_OMX_WORKDIR,
|
||||||
|
BOSS_OMX_TIMEOUT_MS: process.env.BOSS_OMX_TIMEOUT_MS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreEnv(snapshot: ReturnType<typeof snapshotEnv>) {
|
||||||
|
for (const [key, value] of Object.entries(snapshot)) {
|
||||||
|
if (value === undefined) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("OMX backend 默认关闭", () => {
|
||||||
|
const previous = snapshotEnv();
|
||||||
|
delete process.env.BOSS_OMX_ENABLED;
|
||||||
|
delete process.env.BOSS_OMX_COMMAND;
|
||||||
|
delete process.env.BOSS_OMX_ARGS;
|
||||||
|
delete process.env.BOSS_OMX_WORKDIR;
|
||||||
|
delete process.env.BOSS_OMX_TIMEOUT_MS;
|
||||||
|
|
||||||
|
const config = getOmxTeamBackendConfigForTesting();
|
||||||
|
assert.equal(config.enabled, false);
|
||||||
|
assert.equal(config.command, undefined);
|
||||||
|
assert.deepEqual(config.args, []);
|
||||||
|
assert.equal(config.timeoutMs, 45000);
|
||||||
|
assert.equal(isOmxTeamBackendConfiguredForTesting(config), false);
|
||||||
|
|
||||||
|
restoreEnv(previous);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("OMX backend 在配置完整时返回 command、args 和 timeout", () => {
|
||||||
|
const previous = snapshotEnv();
|
||||||
|
process.env.BOSS_OMX_ENABLED = "true";
|
||||||
|
process.env.BOSS_OMX_COMMAND = "node";
|
||||||
|
process.env.BOSS_OMX_ARGS = "scripts/omx-team-smoke.mjs --smoke";
|
||||||
|
process.env.BOSS_OMX_WORKDIR = "/tmp/boss-omx";
|
||||||
|
process.env.BOSS_OMX_TIMEOUT_MS = "120000";
|
||||||
|
|
||||||
|
const config = getOmxTeamBackendConfigForTesting();
|
||||||
|
assert.equal(config.enabled, true);
|
||||||
|
assert.equal(config.command, "node");
|
||||||
|
assert.deepEqual(config.args, ["scripts/omx-team-smoke.mjs", "--smoke"]);
|
||||||
|
assert.equal(config.cwd, "/tmp/boss-omx");
|
||||||
|
assert.equal(config.timeoutMs, 120000);
|
||||||
|
assert.equal(isOmxTeamBackendConfiguredForTesting(config), true);
|
||||||
|
|
||||||
|
restoreEnv(previous);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("OMX backend availability 会在可执行命令和脚本都存在时返回 ready", async () => {
|
||||||
|
const previous = snapshotEnv();
|
||||||
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), "boss-omx-config-"));
|
||||||
|
const scriptPath = path.join(tempDir, "omx-team-smoke.mjs");
|
||||||
|
await writeFile(scriptPath, "console.log('ok');\n", "utf8");
|
||||||
|
|
||||||
|
process.env.BOSS_OMX_ENABLED = "true";
|
||||||
|
process.env.BOSS_OMX_COMMAND = process.execPath;
|
||||||
|
process.env.BOSS_OMX_ARGS = scriptPath;
|
||||||
|
process.env.BOSS_OMX_WORKDIR = tempDir;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const availability = await getOmxTeamBackendAvailabilityForTesting();
|
||||||
|
assert.equal(availability.status, "ready");
|
||||||
|
assert.equal(availability.selectable, true);
|
||||||
|
assert.equal(availability.reason, "ready");
|
||||||
|
} finally {
|
||||||
|
restoreEnv(previous);
|
||||||
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("OMX backend availability 会在脚本参数不存在时返回不可选", async () => {
|
||||||
|
const previous = snapshotEnv();
|
||||||
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), "boss-omx-config-"));
|
||||||
|
const missingScript = path.join(tempDir, "missing-omx-script.mjs");
|
||||||
|
|
||||||
|
process.env.BOSS_OMX_ENABLED = "true";
|
||||||
|
process.env.BOSS_OMX_COMMAND = process.execPath;
|
||||||
|
process.env.BOSS_OMX_ARGS = missingScript;
|
||||||
|
process.env.BOSS_OMX_WORKDIR = tempDir;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const availability = await getOmxTeamBackendAvailabilityForTesting();
|
||||||
|
assert.equal(availability.status, "misconfigured");
|
||||||
|
assert.equal(availability.selectable, false);
|
||||||
|
assert.equal(availability.reason, "script_not_found");
|
||||||
|
} finally {
|
||||||
|
restoreEnv(previous);
|
||||||
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
60
tests/omx-team-smoke-script.test.ts
Normal file
60
tests/omx-team-smoke-script.test.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import path from "node:path";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
|
||||||
|
function runSmoke(payload: unknown) {
|
||||||
|
return new Promise<{
|
||||||
|
exitCode: number | null;
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
}>((resolve, reject) => {
|
||||||
|
const scriptPath = path.resolve("scripts/omx-team-smoke.mjs");
|
||||||
|
const child = spawn(process.execPath, [scriptPath], {
|
||||||
|
cwd: "/Users/kris/code/boss",
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
child.stdout.setEncoding("utf8");
|
||||||
|
child.stderr.setEncoding("utf8");
|
||||||
|
child.stdout.on("data", (chunk) => {
|
||||||
|
stdout += chunk;
|
||||||
|
});
|
||||||
|
child.stderr.on("data", (chunk) => {
|
||||||
|
stderr += chunk;
|
||||||
|
});
|
||||||
|
child.on("error", reject);
|
||||||
|
child.on("close", (exitCode) => {
|
||||||
|
resolve({ exitCode, stdout, stderr });
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdin.write(JSON.stringify(payload));
|
||||||
|
child.stdin.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("omx team smoke script emits ready JSON for valid payload", async () => {
|
||||||
|
const result = await runSmoke({
|
||||||
|
requestKind: "dispatch_execution",
|
||||||
|
workersRequested: 2,
|
||||||
|
objective: "并行协作链路 smoke",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.exitCode, 0);
|
||||||
|
assert.equal(result.stderr, "");
|
||||||
|
const parsed = JSON.parse(result.stdout);
|
||||||
|
assert.equal(parsed.status, "ready");
|
||||||
|
assert.equal(parsed.backendId, "omx-team");
|
||||||
|
assert.match(parsed.summary, /并行协作链路 smoke/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("omx team smoke script emits failed JSON for invalid payload", async () => {
|
||||||
|
const result = await runSmoke("not-an-object");
|
||||||
|
|
||||||
|
assert.equal(result.exitCode, 0);
|
||||||
|
const parsed = JSON.parse(result.stdout);
|
||||||
|
assert.equal(parsed.status, "failed");
|
||||||
|
assert.match(parsed.error, /INVALID_OMX_PAYLOAD/);
|
||||||
|
});
|
||||||
53
tests/orchestration-backend-selector.test.ts
Normal file
53
tests/orchestration-backend-selector.test.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import {
|
||||||
|
listOrchestrationBackendChoices,
|
||||||
|
selectOrchestrationBackendForTesting,
|
||||||
|
} from "@/lib/execution/orchestration-backend-selector";
|
||||||
|
|
||||||
|
test("listOrchestrationBackendChoices keeps omx disabled by default", async () => {
|
||||||
|
const backends = await listOrchestrationBackendChoices();
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
backends.map((backend) => backend.backendId),
|
||||||
|
["boss-native-orchestrator"],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selectOrchestrationBackendForTesting honors explicit omx request when selectable", async () => {
|
||||||
|
const backend = await selectOrchestrationBackendForTesting({
|
||||||
|
requestedBackendId: "omx-team",
|
||||||
|
omx: {
|
||||||
|
enabled: true,
|
||||||
|
selectable: true,
|
||||||
|
availability: {
|
||||||
|
status: "ready",
|
||||||
|
selectable: true,
|
||||||
|
configured: true,
|
||||||
|
reason: "ready",
|
||||||
|
reasonLabel: "OMX Team Runtime 可用。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(backend.backendId, "omx-team");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selectOrchestrationBackendForTesting falls back when omx is requested but unavailable", async () => {
|
||||||
|
const backend = await selectOrchestrationBackendForTesting({
|
||||||
|
requestedBackendId: "omx-team",
|
||||||
|
omx: {
|
||||||
|
enabled: false,
|
||||||
|
selectable: false,
|
||||||
|
availability: {
|
||||||
|
status: "disabled",
|
||||||
|
selectable: false,
|
||||||
|
configured: false,
|
||||||
|
reason: "disabled",
|
||||||
|
reasonLabel: "OMX Team Runtime 当前未启用。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(backend.backendId, "boss-native-orchestrator");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user