mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix: report unavailable Codex runtime in model status
This commit is contained in:
@@ -23,6 +23,11 @@ import {
|
||||
import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js";
|
||||
import { resolveProfileUnusableUntilForDisplay } from "../../agents/auth-profiles/usage.js";
|
||||
import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js";
|
||||
import {
|
||||
resolveAgentHarnessOwnerPluginIds,
|
||||
resolveAgentHarnessRuntimeAvailability,
|
||||
type AgentHarnessRuntimeAvailability,
|
||||
} from "../../agents/harness/runtime-plugin.js";
|
||||
import {
|
||||
createModelAuthAvailabilityResolver,
|
||||
type ModelAuthAvailabilityEvaluation,
|
||||
@@ -129,17 +134,20 @@ type StatusProviderRouteAuth =
|
||||
kind: "legacy";
|
||||
evaluation: ModelAuthAvailabilityEvaluation;
|
||||
usesCodexRuntimeAuth: boolean;
|
||||
runtimeAvailability?: AgentHarnessRuntimeAvailability;
|
||||
}
|
||||
| {
|
||||
kind: "route";
|
||||
route: ProviderModelRouteCandidate;
|
||||
evaluation: ModelAuthAvailabilityEvaluation;
|
||||
usesCodexRuntimeAuth: boolean;
|
||||
runtimeAvailability?: AgentHarnessRuntimeAvailability;
|
||||
}
|
||||
| {
|
||||
kind: "indeterminate";
|
||||
evaluation: ModelAuthAvailabilityEvaluation;
|
||||
usesCodexRuntimeAuth: boolean;
|
||||
runtimeAvailability?: AgentHarnessRuntimeAvailability;
|
||||
}
|
||||
| {
|
||||
kind: "incompatible";
|
||||
@@ -147,6 +155,7 @@ type StatusProviderRouteAuth =
|
||||
message: string;
|
||||
evaluation: ModelAuthAvailabilityEvaluation;
|
||||
usesCodexRuntimeAuth: false;
|
||||
runtimeAvailability?: undefined;
|
||||
};
|
||||
|
||||
type StatusProviderUseRef = {
|
||||
@@ -164,6 +173,28 @@ type StatusProviderUse = {
|
||||
routeAuth: StatusProviderRouteAuth;
|
||||
};
|
||||
|
||||
type StatusRuntimeAuthStatus = "usable" | "missing" | "indeterminate";
|
||||
|
||||
type StatusRuntimeAuthRouteBase = {
|
||||
provider: string;
|
||||
runtime: string;
|
||||
authProvider: string;
|
||||
effective: ProviderAuthOverview["effective"];
|
||||
};
|
||||
|
||||
type StatusRuntimeAuthRoute =
|
||||
| (StatusRuntimeAuthRouteBase & {
|
||||
status: StatusRuntimeAuthStatus;
|
||||
})
|
||||
| (StatusRuntimeAuthRouteBase & {
|
||||
status: "unavailable";
|
||||
authStatus: StatusRuntimeAuthStatus;
|
||||
runtimeStatus: "unavailable";
|
||||
runtimeReason: Extract<AgentHarnessRuntimeAvailability, { status: "unavailable" }>["reason"];
|
||||
runtimeDetail: string;
|
||||
runtimePluginIds: string[];
|
||||
});
|
||||
|
||||
type StatusModelRouteIssue =
|
||||
| {
|
||||
kind: "incompatible";
|
||||
@@ -362,6 +393,49 @@ export async function modelsStatusCommand(
|
||||
workspaceDir,
|
||||
env: process.env,
|
||||
});
|
||||
const selectedPluginRootDirs = new Map(
|
||||
[...metadataSnapshot.byPluginId].map(([pluginId, plugin]) => [pluginId, plugin.rootDir]),
|
||||
);
|
||||
const { runPluginPayloadSmokeCheckForManifestRecords } =
|
||||
await import("../../cli/update-cli/plugin-payload-validation.js");
|
||||
const codexRuntimeAvailabilityByProvider = new Map<
|
||||
string,
|
||||
Promise<AgentHarnessRuntimeAvailability>
|
||||
>();
|
||||
const resolveCodexRuntimeAvailability = (
|
||||
provider: string,
|
||||
): Promise<AgentHarnessRuntimeAvailability> => {
|
||||
const cached = codexRuntimeAvailabilityByProvider.get(provider);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const pending = (async () => {
|
||||
const ownerPluginIds = resolveAgentHarnessOwnerPluginIds({
|
||||
runtime: "codex",
|
||||
provider,
|
||||
config: cfg,
|
||||
workspaceDir,
|
||||
});
|
||||
const pluginPayloadSmoke = await runPluginPayloadSmokeCheckForManifestRecords({
|
||||
plugins: ownerPluginIds.flatMap((pluginId) => {
|
||||
const plugin = metadataSnapshot.byPluginId.get(pluginId);
|
||||
return plugin ? [plugin] : [];
|
||||
}),
|
||||
env: process.env,
|
||||
});
|
||||
return resolveAgentHarnessRuntimeAvailability({
|
||||
runtime: "codex",
|
||||
provider,
|
||||
config: cfg,
|
||||
workspaceDir,
|
||||
payloadFailures: pluginPayloadSmoke.failures,
|
||||
payloadCheckedPluginIds: pluginPayloadSmoke.checked,
|
||||
selectedPluginRootDirs,
|
||||
});
|
||||
})();
|
||||
codexRuntimeAvailabilityByProvider.set(provider, pending);
|
||||
return pending;
|
||||
};
|
||||
const cleanupPluginMetadataSnapshot = installCommandPluginMetadataSnapshot({
|
||||
snapshot: metadataSnapshot,
|
||||
config: cfg,
|
||||
@@ -571,94 +645,104 @@ export async function modelsStatusCommand(
|
||||
metadataSnapshot,
|
||||
});
|
||||
let authResolver = createStatusAuthResolver(store);
|
||||
const resolveProviderUses = (resolver: ModelAuthAvailabilityResolver): StatusProviderUse[] =>
|
||||
providerUseRefs.map((usage) => {
|
||||
const observedRoutes = routeSourcesByModel.get(
|
||||
modelCatalogLogicalKey({ provider: usage.provider, id: usage.model }),
|
||||
);
|
||||
const ref = {
|
||||
modelId: usage.model,
|
||||
...(observedRoutes ? { observedRoutes } : {}),
|
||||
};
|
||||
// Image tools own their provider auth behavior. The text-route artifact
|
||||
// must not reinterpret image auth as an OpenAI text transport.
|
||||
const rawEvaluation: ModelAuthAvailabilityEvaluation =
|
||||
usage.routeScope === "text"
|
||||
? resolver.evaluateModelAuth(usage.provider, ref)
|
||||
: {
|
||||
availability: resolver.resolveProviderAuthAvailability(usage.provider, ref),
|
||||
routeResolution: null,
|
||||
};
|
||||
const routeAuth: StatusProviderRouteAuth = (() => {
|
||||
if (rawEvaluation.routeResolution?.kind === "incompatible") {
|
||||
return {
|
||||
kind: "incompatible",
|
||||
code: rawEvaluation.routeResolution.code,
|
||||
message: rawEvaluation.routeResolution.message,
|
||||
evaluation: rawEvaluation,
|
||||
usesCodexRuntimeAuth: false,
|
||||
};
|
||||
}
|
||||
const usesCodexRuntimeAuth =
|
||||
usage.allowCodexRuntimeFallback &&
|
||||
resolveAgentHarnessPolicy({
|
||||
provider: usage.provider,
|
||||
modelId: usage.model,
|
||||
...(rawEvaluation.selectedRoute
|
||||
? {
|
||||
modelApi: rawEvaluation.selectedRoute.api,
|
||||
modelBaseUrl: rawEvaluation.selectedRoute.baseUrl,
|
||||
}
|
||||
: {}),
|
||||
config: cfg,
|
||||
agentId: workspaceAgentId,
|
||||
}).runtime === "codex";
|
||||
if (
|
||||
usesCodexRuntimeAuth &&
|
||||
usage.provider !== OPENAI_PROVIDER_ID &&
|
||||
usage.provider !== "codex"
|
||||
) {
|
||||
return {
|
||||
kind: "incompatible",
|
||||
code: "unsupported-codex-runtime-provider",
|
||||
message: `The Codex runtime does not support provider ${usage.provider}.`,
|
||||
evaluation: rawEvaluation,
|
||||
usesCodexRuntimeAuth: false,
|
||||
};
|
||||
}
|
||||
const evaluation = rawEvaluation;
|
||||
if (evaluation.selectedRoute) {
|
||||
return {
|
||||
kind: "route",
|
||||
route: evaluation.selectedRoute,
|
||||
evaluation,
|
||||
usesCodexRuntimeAuth,
|
||||
};
|
||||
}
|
||||
if (
|
||||
evaluation.routeResolution?.kind === "routes" ||
|
||||
evaluation.routeResolution?.kind === "indeterminate"
|
||||
) {
|
||||
return {
|
||||
kind: "indeterminate",
|
||||
evaluation,
|
||||
usesCodexRuntimeAuth,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "legacy",
|
||||
evaluation,
|
||||
usesCodexRuntimeAuth,
|
||||
const resolveProviderUses = async (
|
||||
resolver: ModelAuthAvailabilityResolver,
|
||||
): Promise<StatusProviderUse[]> =>
|
||||
await Promise.all(
|
||||
providerUseRefs.map(async (usage) => {
|
||||
const observedRoutes = routeSourcesByModel.get(
|
||||
modelCatalogLogicalKey({ provider: usage.provider, id: usage.model }),
|
||||
);
|
||||
const ref = {
|
||||
modelId: usage.model,
|
||||
...(observedRoutes ? { observedRoutes } : {}),
|
||||
};
|
||||
})();
|
||||
return {
|
||||
provider: usage.provider,
|
||||
model: usage.model,
|
||||
allowCodexRuntimeFallback: usage.allowCodexRuntimeFallback,
|
||||
routeAuth,
|
||||
};
|
||||
});
|
||||
let providerUses = resolveProviderUses(authResolver);
|
||||
// Image tools own their provider auth behavior. The text-route artifact
|
||||
// must not reinterpret image auth as an OpenAI text transport.
|
||||
const rawEvaluation: ModelAuthAvailabilityEvaluation =
|
||||
usage.routeScope === "text"
|
||||
? resolver.evaluateModelAuth(usage.provider, ref)
|
||||
: {
|
||||
availability: resolver.resolveProviderAuthAvailability(usage.provider, ref),
|
||||
routeResolution: null,
|
||||
};
|
||||
const routeAuth: StatusProviderRouteAuth = await (async () => {
|
||||
if (rawEvaluation.routeResolution?.kind === "incompatible") {
|
||||
return {
|
||||
kind: "incompatible",
|
||||
code: rawEvaluation.routeResolution.code,
|
||||
message: rawEvaluation.routeResolution.message,
|
||||
evaluation: rawEvaluation,
|
||||
usesCodexRuntimeAuth: false,
|
||||
};
|
||||
}
|
||||
const usesCodexRuntimeAuth =
|
||||
usage.allowCodexRuntimeFallback &&
|
||||
resolveAgentHarnessPolicy({
|
||||
provider: usage.provider,
|
||||
modelId: usage.model,
|
||||
...(rawEvaluation.selectedRoute
|
||||
? {
|
||||
modelApi: rawEvaluation.selectedRoute.api,
|
||||
modelBaseUrl: rawEvaluation.selectedRoute.baseUrl,
|
||||
}
|
||||
: {}),
|
||||
config: cfg,
|
||||
agentId: workspaceAgentId,
|
||||
}).runtime === "codex";
|
||||
if (
|
||||
usesCodexRuntimeAuth &&
|
||||
usage.provider !== OPENAI_PROVIDER_ID &&
|
||||
usage.provider !== "codex"
|
||||
) {
|
||||
return {
|
||||
kind: "incompatible",
|
||||
code: "unsupported-codex-runtime-provider",
|
||||
message: `The Codex runtime does not support provider ${usage.provider}.`,
|
||||
evaluation: rawEvaluation,
|
||||
usesCodexRuntimeAuth: false,
|
||||
};
|
||||
}
|
||||
const runtimeAvailability = usesCodexRuntimeAuth
|
||||
? await resolveCodexRuntimeAvailability(usage.provider)
|
||||
: undefined;
|
||||
const evaluation = rawEvaluation;
|
||||
if (evaluation.selectedRoute) {
|
||||
return {
|
||||
kind: "route",
|
||||
route: evaluation.selectedRoute,
|
||||
evaluation,
|
||||
usesCodexRuntimeAuth,
|
||||
runtimeAvailability,
|
||||
};
|
||||
}
|
||||
if (
|
||||
evaluation.routeResolution?.kind === "routes" ||
|
||||
evaluation.routeResolution?.kind === "indeterminate"
|
||||
) {
|
||||
return {
|
||||
kind: "indeterminate",
|
||||
evaluation,
|
||||
usesCodexRuntimeAuth,
|
||||
runtimeAvailability,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "legacy",
|
||||
evaluation,
|
||||
usesCodexRuntimeAuth,
|
||||
runtimeAvailability,
|
||||
};
|
||||
})();
|
||||
return {
|
||||
provider: usage.provider,
|
||||
model: usage.model,
|
||||
allowCodexRuntimeFallback: usage.allowCodexRuntimeFallback,
|
||||
routeAuth,
|
||||
};
|
||||
}),
|
||||
);
|
||||
let providerUses = await resolveProviderUses(authResolver);
|
||||
const syntheticAuthByProvider = new Map<string, StatusSyntheticAuth>();
|
||||
const runtimeSyntheticAuthByProvider = new Map<string, StatusSyntheticAuth>();
|
||||
const cliRuntimeAuthUsages = providerUses
|
||||
@@ -762,7 +846,7 @@ export async function modelsStatusCommand(
|
||||
...store,
|
||||
profiles: { ...store.profiles, ...syntheticProfiles },
|
||||
});
|
||||
providerUses = resolveProviderUses(authResolver);
|
||||
providerUses = await resolveProviderUses(authResolver);
|
||||
codexRuntimeAuthUsages = providerUses.filter((usage) => usage.routeAuth.usesCodexRuntimeAuth);
|
||||
}
|
||||
|
||||
@@ -894,51 +978,66 @@ export async function modelsStatusCommand(
|
||||
usages.push(usage);
|
||||
codexRuntimeUsagesByProvider.set(usage.provider, usages);
|
||||
}
|
||||
const runtimeAuthRoutes = Array.from(
|
||||
new Map([
|
||||
...Array.from(codexRuntimeUsagesByProvider.entries()).map(([provider, usages]) => {
|
||||
const representative =
|
||||
usages.find((usage) => usage.routeAuth.evaluation.availability === true) ?? usages[0];
|
||||
const effective = resolveRuntimeAuthRouteEffective(
|
||||
codexProvider,
|
||||
representative?.routeAuth.evaluation,
|
||||
);
|
||||
const availabilities = usages.map((usage) => usage.routeAuth.evaluation.availability);
|
||||
return [
|
||||
`${provider}:codex:${codexProvider}`,
|
||||
{
|
||||
provider,
|
||||
runtime: "codex",
|
||||
authProvider: codexProvider,
|
||||
status: availabilities.every((availability) => availability === true)
|
||||
const runtimeAuthRouteEntries: Array<readonly [string, StatusRuntimeAuthRoute]> = [
|
||||
...Array.from(codexRuntimeUsagesByProvider.entries()).map(([provider, usages]) => {
|
||||
const representative =
|
||||
usages.find((usage) => usage.routeAuth.evaluation.availability === true) ?? usages[0];
|
||||
const effective = resolveRuntimeAuthRouteEffective(
|
||||
codexProvider,
|
||||
representative?.routeAuth.evaluation,
|
||||
);
|
||||
const availabilities = usages.map((usage) => usage.routeAuth.evaluation.availability);
|
||||
const authStatus = availabilities.every((availability) => availability === true)
|
||||
? "usable"
|
||||
: availabilities.some((availability) => availability === false)
|
||||
? "missing"
|
||||
: "indeterminate";
|
||||
const runtimeAvailability = representative?.routeAuth.runtimeAvailability;
|
||||
const route: StatusRuntimeAuthRoute =
|
||||
runtimeAvailability?.status === "unavailable"
|
||||
? {
|
||||
provider,
|
||||
runtime: "codex",
|
||||
authProvider: codexProvider,
|
||||
status: "unavailable",
|
||||
effective,
|
||||
authStatus,
|
||||
runtimeStatus: runtimeAvailability.status,
|
||||
runtimeReason: runtimeAvailability.reason,
|
||||
runtimeDetail: runtimeAvailability.detail,
|
||||
runtimePluginIds: runtimeAvailability.ownerPluginIds,
|
||||
}
|
||||
: {
|
||||
provider,
|
||||
runtime: "codex",
|
||||
authProvider: codexProvider,
|
||||
status: authStatus,
|
||||
effective,
|
||||
};
|
||||
return [`${provider}:codex:${codexProvider}`, route] as const;
|
||||
}),
|
||||
...cliRuntimeAuthUsages.map((usage) => {
|
||||
const evaluation = authResolver.evaluateModelAuth(usage.runtime);
|
||||
const effective = resolveRuntimeAuthRouteEffective(usage.runtime, evaluation);
|
||||
return [
|
||||
`${usage.provider}:${usage.runtime}:${usage.runtime}`,
|
||||
{
|
||||
provider: usage.provider,
|
||||
runtime: usage.runtime,
|
||||
authProvider: usage.runtime,
|
||||
status:
|
||||
evaluation.availability === true
|
||||
? "usable"
|
||||
: availabilities.some((availability) => availability === false)
|
||||
: evaluation.availability === false
|
||||
? "missing"
|
||||
: "indeterminate",
|
||||
effective,
|
||||
},
|
||||
] as const;
|
||||
}),
|
||||
...cliRuntimeAuthUsages.map((usage) => {
|
||||
const evaluation = authResolver.evaluateModelAuth(usage.runtime);
|
||||
const effective = resolveRuntimeAuthRouteEffective(usage.runtime, evaluation);
|
||||
return [
|
||||
`${usage.provider}:${usage.runtime}:${usage.runtime}`,
|
||||
{
|
||||
provider: usage.provider,
|
||||
runtime: usage.runtime,
|
||||
authProvider: usage.runtime,
|
||||
status:
|
||||
evaluation.availability === true
|
||||
? "usable"
|
||||
: evaluation.availability === false
|
||||
? "missing"
|
||||
: "indeterminate",
|
||||
effective,
|
||||
},
|
||||
] as const;
|
||||
}),
|
||||
]).values(),
|
||||
effective,
|
||||
},
|
||||
] as const;
|
||||
}),
|
||||
];
|
||||
const runtimeAuthRoutes = Array.from(
|
||||
new Map<string, StatusRuntimeAuthRoute>(runtimeAuthRouteEntries).values(),
|
||||
).toSorted((a, b) => a.provider.localeCompare(b.provider));
|
||||
const modelRouteIssues = providerUses.flatMap<StatusModelRouteIssue>((usage) => {
|
||||
const cliRuntimeAuthProvider = resolveCliRuntimeAuthProvider(usage);
|
||||
@@ -1172,6 +1271,7 @@ export async function modelsStatusCommand(
|
||||
) ||
|
||||
routeAuthHealth.has("missing") ||
|
||||
routeAuthHealth.has("indeterminate") ||
|
||||
runtimeAuthRoutes.some((route) => route.status === "unavailable") ||
|
||||
missingProvidersInUse.length > 0;
|
||||
const hasExpiring = routeAuthHealth.has("expiring");
|
||||
if (hasExpiredOrMissing) {
|
||||
@@ -1410,6 +1510,17 @@ export async function modelsStatusCommand(
|
||||
runtime.log("");
|
||||
runtime.log(colorize(rich, theme.heading, "Runtime auth"));
|
||||
for (const route of runtimeAuthRoutes) {
|
||||
const runtimeAvailability =
|
||||
route.status === "unavailable"
|
||||
? `${formatSeparator()}${formatKeyValue(
|
||||
"auth",
|
||||
route.authStatus,
|
||||
)}${formatSeparator()}${formatKeyValue("runtime", route.runtimeStatus)}${
|
||||
route.runtimeDetail
|
||||
? `${formatSeparator()}${colorize(rich, theme.muted, route.runtimeDetail)}`
|
||||
: ""
|
||||
}`
|
||||
: "";
|
||||
runtime.log(
|
||||
`- ${theme.heading(route.provider)} via ${colorize(
|
||||
rich,
|
||||
@@ -1422,7 +1533,7 @@ export async function modelsStatusCommand(
|
||||
theme.muted,
|
||||
route.effective.detail,
|
||||
)}`,
|
||||
)}${formatSeparator()}${formatKeyValue("status", route.status)}`,
|
||||
)}${formatSeparator()}${formatKeyValue("status", route.status)}${runtimeAvailability}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,14 @@ const mocks = vi.hoisted(() => {
|
||||
loadProviderUsageSummary: vi.fn().mockResolvedValue(undefined),
|
||||
resolveRuntimeSyntheticAuthProviderRefs: vi.fn().mockReturnValue([]),
|
||||
resolveProviderSyntheticAuthWithPlugin: vi.fn().mockReturnValue(undefined),
|
||||
resolveAgentHarnessOwnerPluginIds: vi.fn().mockReturnValue(["codex"]),
|
||||
runPluginPayloadSmokeCheckForManifestRecords: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ checked: ["codex"], failures: [] }),
|
||||
resolveAgentHarnessRuntimeAvailability: vi.fn().mockReturnValue({
|
||||
status: "available",
|
||||
ownerPluginIds: ["codex"],
|
||||
}),
|
||||
loadModelCatalog: vi.fn().mockResolvedValue([]),
|
||||
modelCatalogRouteVariants: undefined as unknown[] | undefined,
|
||||
openAIModelRouteOverride: undefined as ((params: unknown) => unknown) | undefined,
|
||||
@@ -276,6 +284,13 @@ vi.mock("../../plugins/synthetic-auth.runtime.js", () => ({
|
||||
vi.mock("../../plugins/provider-runtime.js", () => ({
|
||||
resolveProviderSyntheticAuthWithPlugin: mocks.resolveProviderSyntheticAuthWithPlugin,
|
||||
}));
|
||||
vi.mock("../../agents/harness/runtime-plugin.js", () => ({
|
||||
resolveAgentHarnessOwnerPluginIds: mocks.resolveAgentHarnessOwnerPluginIds,
|
||||
resolveAgentHarnessRuntimeAvailability: mocks.resolveAgentHarnessRuntimeAvailability,
|
||||
}));
|
||||
vi.mock("../../cli/update-cli/plugin-payload-validation.js", () => ({
|
||||
runPluginPayloadSmokeCheckForManifestRecords: mocks.runPluginPayloadSmokeCheckForManifestRecords,
|
||||
}));
|
||||
vi.mock("../../agents/model-catalog.js", () => ({
|
||||
loadModelCatalogSnapshot: async (...args: unknown[]) => {
|
||||
const entries = await mocks.loadModelCatalog(...args);
|
||||
@@ -770,6 +785,90 @@ describe("modelsStatusCommand auth overview", () => {
|
||||
expect(textRuntime.log.mock.calls.flat().join("\n")).not.toContain("set an API key env var");
|
||||
});
|
||||
|
||||
it("reports usable Codex auth as unavailable when its harness plugin is quarantined", async () => {
|
||||
const localRuntime = createRuntime();
|
||||
const textRuntime = createRuntime();
|
||||
const payloadFailure = {
|
||||
pluginId: "codex",
|
||||
installPath: "/private/plugin",
|
||||
reason: "missing-package-dir" as const,
|
||||
detail: "missing",
|
||||
};
|
||||
mocks.runPluginPayloadSmokeCheckForManifestRecords
|
||||
.mockResolvedValueOnce({ checked: ["codex"], failures: [payloadFailure] })
|
||||
.mockResolvedValueOnce({ checked: ["codex"], failures: [payloadFailure] });
|
||||
const resolveAvailability = (params: {
|
||||
payloadFailures: Array<{ pluginId: string; reason: string }>;
|
||||
}) =>
|
||||
params.payloadFailures.some((failure) => failure.pluginId === "codex")
|
||||
? {
|
||||
status: "unavailable",
|
||||
ownerPluginIds: ["codex", "openai"],
|
||||
reason: "owner-plugin-degraded",
|
||||
detail:
|
||||
'Agent harness "codex" owner plugin "codex" is unavailable (missing-package-dir).',
|
||||
}
|
||||
: { status: "available", ownerPluginIds: ["codex", "openai"] };
|
||||
mocks.resolveAgentHarnessRuntimeAvailability
|
||||
.mockImplementationOnce(resolveAvailability)
|
||||
.mockImplementationOnce(resolveAvailability);
|
||||
await withOpenAIStatusFixture(
|
||||
{
|
||||
primary: "openai/gpt-5.5",
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "oauth",
|
||||
provider: "openai",
|
||||
access: "oauth-access",
|
||||
refresh: "oauth-refresh",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
await modelsStatusCommand({ json: true, check: true }, localRuntime as never);
|
||||
await modelsStatusCommand({ check: true }, textRuntime as never);
|
||||
},
|
||||
);
|
||||
|
||||
expect(mocks.runPluginPayloadSmokeCheckForManifestRecords).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ env: process.env }),
|
||||
);
|
||||
expect(mocks.resolveAgentHarnessRuntimeAvailability).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
runtime: "codex",
|
||||
provider: "openai",
|
||||
payloadFailures: [payloadFailure],
|
||||
payloadCheckedPluginIds: ["codex"],
|
||||
selectedPluginRootDirs: expect.any(Map),
|
||||
}),
|
||||
);
|
||||
const payload = parseFirstJsonLog(localRuntime);
|
||||
expect(payload.auth.runtimeAuthRoutes).toEqual([
|
||||
{
|
||||
provider: "openai",
|
||||
runtime: "codex",
|
||||
authProvider: "openai",
|
||||
status: "unavailable",
|
||||
authStatus: "usable",
|
||||
runtimeStatus: "unavailable",
|
||||
runtimeReason: "owner-plugin-degraded",
|
||||
runtimeDetail:
|
||||
'Agent harness "codex" owner plugin "codex" is unavailable (missing-package-dir).',
|
||||
runtimePluginIds: ["codex", "openai"],
|
||||
effective: {
|
||||
kind: "profiles",
|
||||
detail: "/tmp/openclaw-agent/auth-profiles.json",
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(localRuntime.exit).toHaveBeenCalledWith(1);
|
||||
expect(textRuntime.exit).toHaveBeenCalledWith(1);
|
||||
expect(textRuntime.log.mock.calls.flat().join("\n")).toContain("status=unavailable");
|
||||
expect(textRuntime.log.mock.calls.flat().join("\n")).toContain("auth=usable");
|
||||
expect(textRuntime.log.mock.calls.flat().join("\n")).toContain("runtime=unavailable");
|
||||
});
|
||||
|
||||
it("evaluates mixed primary and fallback OpenAI routes independently", async () => {
|
||||
const localRuntime = createRuntime();
|
||||
await withOpenAIStatusFixture(
|
||||
|
||||
Reference in New Issue
Block a user