feat: discover codex app-server capabilities
This commit is contained in:
@@ -50,6 +50,42 @@ test("device detail exposes gui cli capability state and preferred execution mod
|
||||
assert.equal(cards.capabilities.items.preferredExecutionMode, "默认执行模式:CLI");
|
||||
});
|
||||
|
||||
test("device detail exposes Codex App Server discovered model and extension summary", async () => {
|
||||
await setup();
|
||||
|
||||
const state = await readState();
|
||||
const device = state.devices.find((item) => item.id === "mac-studio");
|
||||
assert.ok(device);
|
||||
device!.capabilities = {
|
||||
...(device!.capabilities ?? {}),
|
||||
codexAppServer: {
|
||||
connected: true,
|
||||
lastSeenAt: "2026-05-31T10:00:00.000Z",
|
||||
lastActiveProjectId: "",
|
||||
metadata: {
|
||||
models: [
|
||||
{ id: "gpt-5.4", displayName: "GPT-5.4" },
|
||||
{ id: "gpt-5.4-mini", displayName: "GPT-5.4 mini" },
|
||||
],
|
||||
defaultModelId: "gpt-5.4",
|
||||
fastModelId: "gpt-5.4-mini",
|
||||
deepModelId: "gpt-5.4",
|
||||
skills: [{ name: "image2-ui-prototype" }],
|
||||
plugins: [{ id: "github" }],
|
||||
apps: [{ id: "canva" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
await writeState(state);
|
||||
|
||||
const workspace = getDeviceWorkspaceView(await readState(), "mac-studio");
|
||||
const cards = buildDeviceWorkspaceDetailCards(workspace);
|
||||
|
||||
assert.equal(cards.capabilities.items.codexAppServer, "Codex App Server:已连接");
|
||||
assert.equal(cards.capabilities.items.codexModels, "模型:2 个 · 默认 gpt-5.4 · 快速 gpt-5.4-mini · 深度 gpt-5.4");
|
||||
assert.equal(cards.capabilities.items.codexExtensions, "扩展:Skill 1 个 · Plugin 1 个 · App 1 个");
|
||||
});
|
||||
|
||||
test("device detail exposes folder and project conflict skeleton from workspace policy", async () => {
|
||||
await setup();
|
||||
|
||||
|
||||
74
tests/device-heartbeat-capability-metadata.test.ts
Normal file
74
tests/device-heartbeat-capability-metadata.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
let runtimeRoot = "";
|
||||
let readState: (typeof import("../src/lib/boss-data"))["readState"];
|
||||
let deviceHeartbeatRoute: (typeof import("../src/app/api/device-heartbeat/route"))["POST"];
|
||||
|
||||
async function setup() {
|
||||
if (runtimeRoot) return;
|
||||
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-device-capability-metadata-"));
|
||||
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
||||
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
||||
|
||||
const [data, heartbeatModule] = await Promise.all([
|
||||
import("../src/lib/boss-data.ts"),
|
||||
import("../src/app/api/device-heartbeat/route.ts"),
|
||||
]);
|
||||
readState = data.readState;
|
||||
deviceHeartbeatRoute = heartbeatModule.POST;
|
||||
}
|
||||
|
||||
test.after(async () => {
|
||||
if (runtimeRoot) {
|
||||
await rm(runtimeRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("device heartbeat preserves Codex App Server capability metadata", async () => {
|
||||
await setup();
|
||||
const state = await readState();
|
||||
const device = state.devices.find((item) => item.id === "mac-studio");
|
||||
assert.ok(device, "expected seeded mac-studio device");
|
||||
|
||||
const response = await deviceHeartbeatRoute(
|
||||
new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
deviceId: device!.id,
|
||||
token: device!.token,
|
||||
name: device!.name,
|
||||
avatar: device!.avatar,
|
||||
account: device!.account,
|
||||
status: "online",
|
||||
quota5h: device!.quota5h,
|
||||
quota7d: device!.quota7d,
|
||||
projects: device!.projects,
|
||||
endpoint: device!.endpoint,
|
||||
capabilities: {
|
||||
codexAppServer: {
|
||||
connected: true,
|
||||
lastSeenAt: "2026-05-31T10:00:00.000Z",
|
||||
metadata: {
|
||||
models: [{ id: "gpt-5.4", displayName: "GPT-5.4" }],
|
||||
defaultModelId: "gpt-5.4",
|
||||
fastModelId: "gpt-5.4-mini",
|
||||
providerCapabilities: { webSearch: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const nextState = await readState();
|
||||
const updatedDevice = nextState.devices.find((item) => item.id === device!.id);
|
||||
assert.equal(updatedDevice?.capabilities?.codexAppServer.metadata?.models?.[0]?.id, "gpt-5.4");
|
||||
assert.equal(updatedDevice?.capabilities?.codexAppServer.metadata?.providerCapabilities?.webSearch, true);
|
||||
});
|
||||
125
tests/fixtures/codex-app-server-runtime.mjs
vendored
125
tests/fixtures/codex-app-server-runtime.mjs
vendored
@@ -31,6 +31,131 @@ rl.on("line", (line) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.method === "model/list") {
|
||||
send({
|
||||
id: message.id,
|
||||
result: {
|
||||
data: [
|
||||
{
|
||||
id: "gpt-5.4",
|
||||
model: "gpt-5.4",
|
||||
displayName: "GPT-5.4",
|
||||
description: "Deep reasoning model",
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: ["low", "medium", "high"],
|
||||
defaultReasoningEffort: "medium",
|
||||
inputModalities: ["text", "image"],
|
||||
supportsPersonality: true,
|
||||
serviceTiers: [{ id: "default", displayName: "Default" }],
|
||||
defaultServiceTier: "default",
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.4-mini",
|
||||
model: "gpt-5.4-mini",
|
||||
displayName: "GPT-5.4 mini",
|
||||
description: "Fast response model",
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: ["none", "low"],
|
||||
defaultReasoningEffort: "none",
|
||||
inputModalities: ["text"],
|
||||
supportsPersonality: true,
|
||||
serviceTiers: [],
|
||||
defaultServiceTier: null,
|
||||
isDefault: false,
|
||||
},
|
||||
],
|
||||
nextCursor: null,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.method === "modelProvider/capabilities/read") {
|
||||
send({
|
||||
id: message.id,
|
||||
result: {
|
||||
namespaceTools: true,
|
||||
imageGeneration: true,
|
||||
webSearch: true,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.method === "skills/list") {
|
||||
send({
|
||||
id: message.id,
|
||||
result: {
|
||||
data: [
|
||||
{
|
||||
cwd: "/Users/kris/code/boss",
|
||||
skills: [
|
||||
{
|
||||
name: "image2-ui-prototype",
|
||||
description: "Generate high fidelity UI prototypes",
|
||||
path: "/Users/kris/.codex/skills/image2-ui-prototype/SKILL.md",
|
||||
scope: "user",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
errors: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.method === "plugin/list") {
|
||||
send({
|
||||
id: message.id,
|
||||
result: {
|
||||
marketplaces: [
|
||||
{
|
||||
name: "local",
|
||||
path: "/Users/kris/.codex/plugins/marketplace.json",
|
||||
interface: null,
|
||||
plugins: [
|
||||
{
|
||||
id: "github",
|
||||
remotePluginId: null,
|
||||
localVersion: "1.0.0",
|
||||
name: "GitHub",
|
||||
installed: true,
|
||||
enabled: true,
|
||||
keywords: ["repo"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
marketplaceLoadErrors: [],
|
||||
featuredPluginIds: ["github"],
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.method === "app/list") {
|
||||
send({
|
||||
id: message.id,
|
||||
result: {
|
||||
data: [
|
||||
{
|
||||
id: "canva",
|
||||
name: "Canva",
|
||||
description: "Design app",
|
||||
isAccessible: true,
|
||||
isEnabled: true,
|
||||
pluginDisplayNames: ["Canva"],
|
||||
},
|
||||
],
|
||||
nextCursor: null,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.method === "thread/resume") {
|
||||
send({
|
||||
id: message.id,
|
||||
|
||||
@@ -189,3 +189,85 @@ test("local-agent heartbeat reports Codex App Server capability only when enable
|
||||
await rm(runtimeRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("local-agent heartbeat reports Codex App Server discovered models, skills, plugins, and apps", async () => {
|
||||
const runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-local-agent-app-server-discovery-"));
|
||||
const skillsDir = path.join(runtimeRoot, "skills");
|
||||
await mkdir(skillsDir, { recursive: true });
|
||||
|
||||
const mockControlPlane = await startMockControlPlane();
|
||||
const exampleConfig = JSON.parse(
|
||||
await readFile(path.join(repoRoot, "local-agent", "config.example.json"), "utf8"),
|
||||
);
|
||||
const configPath = path.join(runtimeRoot, "config.json");
|
||||
await writeFile(
|
||||
configPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
...exampleConfig,
|
||||
bindHost: "127.0.0.1",
|
||||
port: 0,
|
||||
controlPlaneUrl: `http://127.0.0.1:${mockControlPlane.port}`,
|
||||
heartbeatIntervalMs: 60_000,
|
||||
masterAgentPollIntervalMs: 60_000,
|
||||
masterAgentEnabled: false,
|
||||
codexSessionDiscoveryEnabled: false,
|
||||
codexAppServerEnabled: true,
|
||||
codexAppServerCommand: process.execPath,
|
||||
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
|
||||
codexAppServerWorkdir: repoRoot,
|
||||
codexAppServerTimeoutMs: 5000,
|
||||
codexAppServerDiscoveryTtlMs: 60_000,
|
||||
projects: [],
|
||||
projectCandidates: [],
|
||||
skillsDir,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const child = spawn(process.execPath, ["local-agent/server.mjs", configPath], {
|
||||
cwd: repoRoot,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stderr = "";
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += String(chunk);
|
||||
});
|
||||
|
||||
try {
|
||||
const heartbeatRequest = await Promise.race([
|
||||
mockControlPlane.heartbeatReceived,
|
||||
new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error(`timed out waiting for heartbeat\n${stderr}`));
|
||||
}, 8000);
|
||||
}),
|
||||
]);
|
||||
|
||||
const payload = JSON.parse(heartbeatRequest.bodyText);
|
||||
const metadata = payload.capabilities.codexAppServer.metadata;
|
||||
|
||||
assert.equal(payload.capabilities.codexAppServer.connected, true);
|
||||
assert.equal(metadata.models[0].id, "gpt-5.4");
|
||||
assert.equal(metadata.models[0].displayName, "GPT-5.4");
|
||||
assert.equal(metadata.defaultModelId, "gpt-5.4");
|
||||
assert.equal(metadata.fastModelId, "gpt-5.4-mini");
|
||||
assert.equal(metadata.providerCapabilities.webSearch, true);
|
||||
assert.equal(metadata.skills[0].name, "image2-ui-prototype");
|
||||
assert.equal(metadata.plugins[0].id, "github");
|
||||
assert.equal(metadata.apps[0].id, "canva");
|
||||
} finally {
|
||||
child.kill("SIGTERM");
|
||||
await new Promise((resolve) => {
|
||||
child.once("close", resolve);
|
||||
}).catch(() => null);
|
||||
await new Promise((resolve) => {
|
||||
mockControlPlane.server.close(resolve);
|
||||
});
|
||||
await rm(runtimeRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user