diff --git a/.env.server.example b/.env.server.example index 35ee0d8..b941df8 100644 --- a/.env.server.example +++ b/.env.server.example @@ -18,3 +18,10 @@ BOSS_SENDMAIL_PATH=/usr/sbin/sendmail # BOSS_CLAW_WORKDIR=/opt/boss # BOSS_CLAW_TIMEOUT_MS=45000 # 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 diff --git a/README.md b/README.md index 7dc0084..ad44b22 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,8 @@ - 当前 Boss 已新增 `src/lib/execution/` 执行底座抽象层;当前生产主链仍然沿用 `local-agent -> codex exec resume`,只是执行责任已开始通过 `ExecutionBackend / PromptAssembler / PermissionPolicy / RemoteRuntimeAdapter / OrchestrationBackend` 默认实现收束 - 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有在显式配置 `BOSS_CLAW_*` 且可用性探测通过时,`master-agent` 当前对话里才会出现并允许选择 `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 -> 异步回流` 整条链 - `GET http://127.0.0.1:4317/api/v1/skills` 正常,已返回本机扫描到的 Codex Skill - `POST http://127.0.0.1:4317/api/v1/heartbeat` 正常,且会顺带触发 `thread-context` 上报 diff --git a/docs/architecture/api_and_service_inventory_cn.md b/docs/architecture/api_and_service_inventory_cn.md index 5fcda43..20472ce 100644 --- a/docs/architecture/api_and_service_inventory_cn.md +++ b/docs/architecture/api_and_service_inventory_cn.md @@ -180,7 +180,8 @@ - 当前已最小接入 `ClawBackendAdapter`,但默认关闭,仅在显式配置且可用性探测通过时才参与执行 - 如果历史 `backendOverride=claw-runtime` 当前不可用,运行时会自动回退到默认后端,并把原因回给前台 - 当前仓库自带 `scripts/claw-runtime-smoke.mjs` 作为兼容 JSON 协议的 smoke runtime,可用于本地和服务器验证 `ClawBackendAdapter` - - 当前尚未接入 `oh-my-codex` + - 当前已最小接入 `OmxTeamBackendAdapter`,但默认关闭,仅提供编排后端骨架、selector 与 smoke runtime + - 当前仓库自带 `scripts/omx-team-smoke.mjs`,可用于本地和服务器验证 `OmxTeamBackendAdapter` ### 3.2 认证相关 diff --git a/docs/architecture/current_runtime_and_deploy_status_cn.md b/docs/architecture/current_runtime_and_deploy_status_cn.md index 5105a25..42ca3e2 100644 --- a/docs/architecture/current_runtime_and_deploy_status_cn.md +++ b/docs/architecture/current_runtime_and_deploy_status_cn.md @@ -31,7 +31,8 @@ - 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有显式配置 `BOSS_CLAW_*` 且可用性探测通过时,`master-agent` 当前对话中才会出现并允许选择 `claw-runtime` - 如果历史上已经保存过 `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` 验证整条链 -- 当前 `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` 验证编排后端骨架 本地已知运行方式: diff --git a/scripts/omx-team-smoke.mjs b/scripts/omx-team-smoke.mjs new file mode 100644 index 0000000..b26027b --- /dev/null +++ b/scripts/omx-team-smoke.mjs @@ -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})`, +}); diff --git a/src/lib/execution/backends/omx-team-backend.ts b/src/lib/execution/backends/omx-team-backend.ts new file mode 100644 index 0000000..0efc2ec --- /dev/null +++ b/src/lib/execution/backends/omx-team-backend.ts @@ -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 { + const availability = await getOmxTeamBackendAvailability(config); + return { + enabled: isOmxTeamBackendConfigured(config), + selectable: availability.selectable, + availability, + }; +} diff --git a/src/lib/execution/backends/omx-team-config.ts b/src/lib/execution/backends/omx-team-config.ts new file mode 100644 index 0000000..9d74b5a --- /dev/null +++ b/src/lib/execution/backends/omx-team-config.ts @@ -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 { + 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; diff --git a/src/lib/execution/orchestration-backend-selector.ts b/src/lib/execution/orchestration-backend-selector.ts new file mode 100644 index 0000000..ddb5f98 --- /dev/null +++ b/src/lib/execution/orchestration-backend-selector.ts @@ -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 { + return (await listOrchestrationBackendChoices(input))[0] ?? BOSS_NATIVE_ORCHESTRATOR; +} + +export async function listOrchestrationBackendChoices( + input: OrchestrationBackendSelectionInput = {}, +): Promise { + const ordered: OrchestrationBackendChoice[] = []; + const seen = new Set(); + + 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; diff --git a/tests/omx-team-backend-config.test.ts b/tests/omx-team-backend-config.test.ts new file mode 100644 index 0000000..f706376 --- /dev/null +++ b/tests/omx-team-backend-config.test.ts @@ -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) { + 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 }); + } +}); diff --git a/tests/omx-team-smoke-script.test.ts b/tests/omx-team-smoke-script.test.ts new file mode 100644 index 0000000..2431c3e --- /dev/null +++ b/tests/omx-team-smoke-script.test.ts @@ -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/); +}); diff --git a/tests/orchestration-backend-selector.test.ts b/tests/orchestration-backend-selector.test.ts new file mode 100644 index 0000000..c58d3c0 --- /dev/null +++ b/tests/orchestration-backend-selector.test.ts @@ -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"); +});