mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ui): always load live active-chat models
This commit is contained in:
@@ -573,19 +573,11 @@ suite.define(() => {
|
||||
});
|
||||
});
|
||||
|
||||
it("refreshes the configured usable catalog after advertised chat metadata", async () => {
|
||||
it("uses the live usable catalog instead of unavailable metadata models", async () => {
|
||||
await suite.withPage({ viewport: { width: 1280, height: 900 } }, async ({ page }) => {
|
||||
const gateway = await installMockGateway(page, {
|
||||
agentModel: "openai/gpt-5.3-codex-spark",
|
||||
models: [
|
||||
{ id: "gpt-5.5", name: "GPT-5.5", provider: "openai", available: true },
|
||||
{
|
||||
id: "gpt-5.3-codex-spark",
|
||||
name: "GPT-5.3 Codex Spark",
|
||||
provider: "codex",
|
||||
available: false,
|
||||
},
|
||||
],
|
||||
agentModel: "openai/gpt-5.6-sol",
|
||||
models: [{ id: "gpt-5.5", name: "GPT-5.5", provider: "openai", available: true }],
|
||||
methodResponses: {
|
||||
"chat.startup": {
|
||||
agentsList: {
|
||||
@@ -603,9 +595,9 @@ suite.define(() => {
|
||||
models: [
|
||||
{ id: "gpt-5.5", name: "GPT-5.5", provider: "openai", available: true },
|
||||
{
|
||||
id: "gpt-5.3-codex-spark",
|
||||
name: "GPT-5.3 Codex Spark",
|
||||
provider: "codex",
|
||||
id: "gpt-5.6-sol",
|
||||
name: "GPT-5.6 Sol",
|
||||
provider: "openai",
|
||||
available: false,
|
||||
},
|
||||
],
|
||||
@@ -614,7 +606,7 @@ suite.define(() => {
|
||||
count: 1,
|
||||
defaults: {
|
||||
contextTokens: 200_000,
|
||||
model: "gpt-5.3-codex-spark",
|
||||
model: "gpt-5.6-sol",
|
||||
modelProvider: "openai",
|
||||
},
|
||||
path: "",
|
||||
@@ -640,7 +632,10 @@ suite.define(() => {
|
||||
|
||||
await page.goto(`${suite.server.baseUrl}chat`);
|
||||
await gateway.waitForRequest("chat.metadata");
|
||||
expect(await gateway.getRequests("models.list")).toHaveLength(0);
|
||||
await gateway.waitForRequest("models.list");
|
||||
expect(await gateway.getRequests("models.list")).toEqual([
|
||||
expect.objectContaining({ params: { view: "configured" } }),
|
||||
]);
|
||||
|
||||
const composer = page.locator(".agent-chat__input");
|
||||
const providers = composer.locator("[data-chat-model-provider]");
|
||||
@@ -651,12 +646,21 @@ suite.define(() => {
|
||||
.poll(() => composer.locator('[data-chat-model-provider-group="openai"]').textContent())
|
||||
.toContain("GPT-5.5");
|
||||
await expect
|
||||
.poll(() => composer.locator('[data-chat-model-provider-group="codex"]').count())
|
||||
.poll(() => composer.locator('[data-chat-model-option="openai/gpt-5.6-sol"]').count())
|
||||
.toBe(0);
|
||||
// The advertised default is unavailable, so no usable catalog row is
|
||||
// marked as the default and no synthetic empty row is introduced.
|
||||
await expect.poll(() => composer.locator('[data-chat-model-default="true"]').count()).toBe(0);
|
||||
await expect.poll(() => composer.locator('[data-chat-model-option=""]').count()).toBe(0);
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
if (artifactDir) {
|
||||
await composer.locator('[data-chat-model-select="true"]').click();
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
path: `${artifactDir}/usable-model-picker.png`,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -142,9 +142,9 @@ suite.define(() => {
|
||||
expect(response?.status()).toBe(200);
|
||||
await gateway.waitForRequest("agents.list");
|
||||
await gateway.waitForRequest("config.get");
|
||||
const modelRequest = await gateway.waitForRequest("chat.metadata");
|
||||
expect(modelRequest.params).toEqual({ agentId: "main" });
|
||||
expect(await gateway.getRequests("models.list")).toHaveLength(0);
|
||||
const modelRequest = await gateway.waitForRequest("models.list");
|
||||
expect(modelRequest.params).toEqual({ agentId: "main", view: "configured" });
|
||||
expect(await gateway.getRequests("chat.metadata")).toHaveLength(0);
|
||||
|
||||
const select = page.locator("select.settings-select").first();
|
||||
await select.waitFor({ state: "visible", timeout: 10_000 });
|
||||
|
||||
@@ -35,8 +35,8 @@ function requestHasParam(request: { params?: unknown }, key: string, value: unkn
|
||||
}
|
||||
|
||||
const TERMINAL_START_FEATURE_METHODS = [
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"models.list",
|
||||
"sessions.catalog.list",
|
||||
"sessions.catalog.startTerminal",
|
||||
"sessions.create",
|
||||
@@ -78,8 +78,8 @@ suite.define(() => {
|
||||
const gateway = await installMockGateway(page, {
|
||||
cliAgentsEnabled: true,
|
||||
featureMethods: [
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"models.list",
|
||||
"sessions.create",
|
||||
"sessions.dispatch",
|
||||
"sessions.catalog.list",
|
||||
@@ -450,7 +450,7 @@ suite.define(() => {
|
||||
const gateway = await installMockGateway(page, {
|
||||
agentModel: "openai/gpt-5.6-luna",
|
||||
methodResponses: {
|
||||
"chat.metadata": {
|
||||
"models.list": {
|
||||
sequence: [
|
||||
{
|
||||
__mockError: {
|
||||
@@ -458,7 +458,7 @@ suite.define(() => {
|
||||
message: "metadata request timed out",
|
||||
},
|
||||
},
|
||||
{ commands: [], models },
|
||||
{ models },
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -467,7 +467,7 @@ suite.define(() => {
|
||||
|
||||
try {
|
||||
await page.goto(`${suite.server.baseUrl}new`);
|
||||
await gateway.waitForRequest("chat.metadata");
|
||||
await gateway.waitForRequest("models.list");
|
||||
|
||||
const modelSelect = page.locator('[data-chat-model-select="true"]');
|
||||
await expect.poll(() => modelSelect.textContent()).toContain("Models unavailable");
|
||||
@@ -479,7 +479,7 @@ suite.define(() => {
|
||||
|
||||
await page.locator('[data-chat-model-catalog-retry="true"]').click();
|
||||
|
||||
await expect.poll(async () => (await gateway.getRequests("chat.metadata")).length).toBe(2);
|
||||
await expect.poll(async () => (await gateway.getRequests("models.list")).length).toBe(2);
|
||||
await expect.poll(() => page.locator("[data-chat-model-option]").count()).toBe(3);
|
||||
expect(await page.locator('[data-chat-model-catalog-state="error"]').count()).toBe(0);
|
||||
} finally {
|
||||
@@ -503,7 +503,7 @@ suite.define(() => {
|
||||
};
|
||||
const gateway = await installMockGateway(page, {
|
||||
methodResponses: {
|
||||
"chat.metadata": {
|
||||
"models.list": {
|
||||
sequence: [
|
||||
{
|
||||
__mockError: {
|
||||
@@ -514,7 +514,7 @@ suite.define(() => {
|
||||
retryAfterMs: 100,
|
||||
},
|
||||
},
|
||||
{ commands: [], models: [recoveredModel] },
|
||||
{ models: [recoveredModel] },
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -522,7 +522,7 @@ suite.define(() => {
|
||||
|
||||
try {
|
||||
await page.goto(`${suite.server.baseUrl}new`);
|
||||
await expect.poll(async () => (await gateway.getRequests("chat.metadata")).length).toBe(2);
|
||||
await expect.poll(async () => (await gateway.getRequests("models.list")).length).toBe(2);
|
||||
|
||||
const modelSelect = page.locator(
|
||||
'.new-session-page__composer [data-chat-model-select="true"]',
|
||||
@@ -532,9 +532,9 @@ suite.define(() => {
|
||||
.poll(() => page.locator('[data-chat-model-option="openai/gpt-5.6-luna"]').textContent())
|
||||
.toContain(recoveredModel.name);
|
||||
|
||||
expect(await gateway.getRequests("chat.metadata")).toEqual([
|
||||
expect.objectContaining({ params: { agentId: "main" } }),
|
||||
expect.objectContaining({ params: { agentId: "main" } }),
|
||||
expect(await gateway.getRequests("models.list")).toEqual([
|
||||
expect.objectContaining({ params: { agentId: "main", view: "configured" } }),
|
||||
expect.objectContaining({ params: { agentId: "main", view: "configured" } }),
|
||||
]);
|
||||
} finally {
|
||||
await context.close();
|
||||
|
||||
@@ -45,6 +45,7 @@ suite.define(() => {
|
||||
featureMethods: [
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"models.list",
|
||||
"sessions.create",
|
||||
"sessions.dispatch",
|
||||
"sessions.reclaim",
|
||||
|
||||
@@ -13,6 +13,7 @@ async function openDraft(
|
||||
featureMethods = [
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"models.list",
|
||||
"projects.list",
|
||||
"sessions.create",
|
||||
"sessions.dispatch",
|
||||
@@ -41,7 +42,14 @@ suite.define(() => {
|
||||
it("keeps read-scoped operators out of new-session entry and submission paths", async () => {
|
||||
const { context, gateway, page } = await openDraft(
|
||||
["operator.read"],
|
||||
["chat.metadata", "chat.startup", "projects.list", "sessions.create", "sessions.dispatch"],
|
||||
[
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"models.list",
|
||||
"projects.list",
|
||||
"sessions.create",
|
||||
"sessions.dispatch",
|
||||
],
|
||||
);
|
||||
try {
|
||||
const sidebarCreate = page.locator(".sidebar-brand__new-thread");
|
||||
@@ -136,6 +144,7 @@ suite.define(() => {
|
||||
featureMethods: [
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"models.list",
|
||||
"fs.listDir",
|
||||
"sessions.create",
|
||||
"worktrees.branches",
|
||||
@@ -197,6 +206,7 @@ suite.define(() => {
|
||||
featureMethods: [
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"models.list",
|
||||
"fs.listDir",
|
||||
"sessions.create",
|
||||
"worktrees.branches",
|
||||
@@ -264,6 +274,7 @@ suite.define(() => {
|
||||
featureMethods: [
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"models.list",
|
||||
"fs.listDir",
|
||||
"sessions.create",
|
||||
"worktrees.branches",
|
||||
@@ -328,7 +339,7 @@ suite.define(() => {
|
||||
it("blocks creation when the connected Gateway explicitly omits sessions.create", async () => {
|
||||
const { context, gateway, page } = await openDraft(
|
||||
["operator.admin", "operator.read", "operator.write"],
|
||||
["chat.metadata", "chat.startup"],
|
||||
["chat.metadata", "chat.startup", "models.list"],
|
||||
);
|
||||
try {
|
||||
await expect.poll(() => page.locator(".sidebar-brand__new-thread").isDisabled()).toBe(true);
|
||||
|
||||
@@ -359,6 +359,7 @@ suite.define(() => {
|
||||
featureMethods: [
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"models.list",
|
||||
"sessions.create",
|
||||
"sessions.dispatch",
|
||||
"projects.list",
|
||||
@@ -480,6 +481,7 @@ suite.define(() => {
|
||||
featureMethods: [
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"models.list",
|
||||
"fs.listDir",
|
||||
"projects.list",
|
||||
"projects.register",
|
||||
@@ -543,7 +545,13 @@ suite.define(() => {
|
||||
workspace: WORKSPACE,
|
||||
workspaceGit: true,
|
||||
operatorScopes: ["operator.read", "operator.write"],
|
||||
featureMethods: ["chat.metadata", "chat.startup", "projects.list", "sessions.create"],
|
||||
featureMethods: [
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"models.list",
|
||||
"projects.list",
|
||||
"sessions.create",
|
||||
],
|
||||
methodResponses: { "projects.list": { projects: [] } },
|
||||
});
|
||||
|
||||
@@ -567,7 +575,13 @@ suite.define(() => {
|
||||
const gateway = await installMockGateway(page, {
|
||||
workspace: WORKSPACE,
|
||||
workspaceGit: true,
|
||||
featureMethods: ["chat.metadata", "chat.startup", "sessions.create", "system.info"],
|
||||
featureMethods: [
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"models.list",
|
||||
"sessions.create",
|
||||
"system.info",
|
||||
],
|
||||
methodResponses: {
|
||||
"system.info": {
|
||||
machineName: "Peters-Mac-Studio",
|
||||
|
||||
@@ -16,8 +16,8 @@ import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts"
|
||||
export { controlUiSessionPath, controlUiSessionUrl, waitForConfirmModal };
|
||||
|
||||
const NEW_SESSION_FEATURE_METHODS = [
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"models.list",
|
||||
"sessions.create",
|
||||
"sessions.dispatch",
|
||||
] as const;
|
||||
|
||||
@@ -728,15 +728,15 @@ suite.define(() => {
|
||||
|
||||
await navigateInApp(page, "chat");
|
||||
await waitForCommittedChatRoute(page);
|
||||
const metadataRequests = (await gateway.getRequests("chat.metadata")).length;
|
||||
const modelRequests = (await gateway.getRequests("models.list")).length;
|
||||
const branchRequests = (await gateway.getRequests("worktrees.branches")).length;
|
||||
await gateway.deferNext("chat.metadata");
|
||||
await gateway.deferNext("models.list");
|
||||
await gateway.deferNext("worktrees.branches");
|
||||
await navigateInApp(page, "new-session");
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe("/new");
|
||||
await expect
|
||||
.poll(async () => (await gateway.getRequests("chat.metadata")).length)
|
||||
.toBe(metadataRequests + 1);
|
||||
.poll(async () => (await gateway.getRequests("models.list")).length)
|
||||
.toBe(modelRequests + 1);
|
||||
await expect
|
||||
.poll(async () => (await gateway.getRequests("worktrees.branches")).length)
|
||||
.toBe(branchRequests + 1);
|
||||
@@ -745,7 +745,7 @@ suite.define(() => {
|
||||
const start = page.getByRole("button", { name: "Start session" });
|
||||
await expect.poll(() => start.isDisabled()).toBe(true);
|
||||
|
||||
await gateway.resolveDeferred("chat.metadata", { models });
|
||||
await gateway.resolveDeferred("models.list", { models });
|
||||
await expect.poll(() => start.isDisabled()).toBe(true);
|
||||
await gateway.rejectDeferred("worktrees.branches", {
|
||||
code: "UNAVAILABLE",
|
||||
@@ -828,6 +828,7 @@ suite.define(() => {
|
||||
featureMethods: [
|
||||
"chat.metadata",
|
||||
"chat.startup",
|
||||
"models.list",
|
||||
"fs.listDir",
|
||||
"sessions.create",
|
||||
"worktrees.branches",
|
||||
|
||||
@@ -267,7 +267,12 @@ export async function refreshChatMetadata(
|
||||
if (!ownsChatMetadataRequest(request)) {
|
||||
return EMPTY_CHAT_METADATA_APPLY_RESULT;
|
||||
}
|
||||
const metadataApplied = applyChatMetadataResult(host, client, agentId, result);
|
||||
// chat.metadata remains the compatibility source for commands only. Picker inventory must
|
||||
// always come from the live, agent-scoped models.list result so stale static models cannot
|
||||
// reappear when chat.startup omits metadata or an older Gateway serves this fallback path.
|
||||
const metadataApplied = applyChatMetadataResult(host, client, agentId, result, {
|
||||
models: false,
|
||||
});
|
||||
if (!metadataApplied.models || !metadataApplied.commands) {
|
||||
await refreshMissingChatMetadata(request, metadataApplied, opts);
|
||||
}
|
||||
|
||||
@@ -1434,7 +1434,17 @@ describe("refreshChatMetadata", () => {
|
||||
}>((resolve) => {
|
||||
resolveMetadata = resolve;
|
||||
});
|
||||
const liveModel = {
|
||||
id: "work-model",
|
||||
name: "Work Model",
|
||||
provider: "openai",
|
||||
available: true,
|
||||
};
|
||||
const request = vi.fn(async (method: string, params?: unknown) => {
|
||||
if (method === "models.list") {
|
||||
expect(params).toEqual({ agentId: "work", view: "configured" });
|
||||
return { models: [liveModel] };
|
||||
}
|
||||
expect(method).toBe("chat.metadata");
|
||||
expect(params).toEqual({ agentId: "work" });
|
||||
return await metadata;
|
||||
@@ -1445,38 +1455,37 @@ describe("refreshChatMetadata", () => {
|
||||
state.sessionKey = "agent:work:another";
|
||||
resolveMetadata?.({
|
||||
commands: [],
|
||||
models: [{ id: "work-model", name: "Work Model", provider: "openai", available: true }],
|
||||
models: [{ id: "stale-model", name: "Stale Model", provider: "openai", available: false }],
|
||||
});
|
||||
await refresh;
|
||||
|
||||
expect(state.chatModelCatalog).toEqual([
|
||||
{ id: "work-model", name: "Work Model", provider: "openai", available: true },
|
||||
]);
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
expect(state.chatModelCatalog).toEqual([liveModel]);
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("reuses same-agent metadata and fetches a cross-agent catalog", async () => {
|
||||
const request = vi.fn(async (_method: string, params?: { agentId?: string }) => ({
|
||||
commands: [],
|
||||
models: [
|
||||
{
|
||||
id: `${params?.agentId}-model`,
|
||||
name: `${params?.agentId} Model`,
|
||||
provider: "openai",
|
||||
},
|
||||
],
|
||||
}));
|
||||
const request = vi.fn(async (method: string, params?: { agentId?: string }) => {
|
||||
const model = {
|
||||
id: `${params?.agentId}-model`,
|
||||
name: `${params?.agentId} Model`,
|
||||
provider: "openai",
|
||||
};
|
||||
return method === "models.list" ? { models: [model] } : { commands: [], models: [model] };
|
||||
});
|
||||
const state = createMetadataState(request);
|
||||
|
||||
await refreshChatMetadata(state);
|
||||
state.sessionKey = "agent:work:second";
|
||||
await refreshChatMetadata(state);
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
|
||||
state.sessionKey = "agent:other:main";
|
||||
await refreshChatMetadata(state);
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
expect(request).toHaveBeenLastCalledWith("chat.metadata", { agentId: "other" });
|
||||
expect(request).toHaveBeenCalledTimes(4);
|
||||
expect(request).toHaveBeenLastCalledWith("models.list", {
|
||||
agentId: "other",
|
||||
view: "configured",
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores metadata after switching to a different agent", async () => {
|
||||
@@ -1531,10 +1540,21 @@ describe("refreshChatMetadata", () => {
|
||||
}>((resolve) => {
|
||||
resolveOther = resolve;
|
||||
});
|
||||
const request = vi.fn(
|
||||
async (_method: string, params?: { agentId?: string }) =>
|
||||
await (params?.agentId === "work" ? workMetadata : otherMetadata),
|
||||
);
|
||||
const request = vi.fn(async (method: string, params?: { agentId?: string }) => {
|
||||
if (method === "models.list") {
|
||||
const isWork = params?.agentId === "work";
|
||||
return {
|
||||
models: [
|
||||
{
|
||||
id: isWork ? "work-model" : "other-model",
|
||||
name: isWork ? "Work Model" : "Other Model",
|
||||
provider: "openai",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return await (params?.agentId === "work" ? workMetadata : otherMetadata);
|
||||
});
|
||||
const state = createMetadataState(request);
|
||||
|
||||
const workRefresh = refreshChatMetadata(state);
|
||||
@@ -1580,10 +1600,13 @@ describe("refreshChatMetadata", () => {
|
||||
}>((resolve) => {
|
||||
resolveSecond = resolve;
|
||||
});
|
||||
let requestCount = 0;
|
||||
const request = vi.fn(async () => {
|
||||
requestCount += 1;
|
||||
return await (requestCount === 1 ? firstMetadata : secondMetadata);
|
||||
let metadataRequestCount = 0;
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "models.list") {
|
||||
return { models: [{ id: "new-model", name: "New Model", provider: "openai" }] };
|
||||
}
|
||||
metadataRequestCount += 1;
|
||||
return await (metadataRequestCount === 1 ? firstMetadata : secondMetadata);
|
||||
});
|
||||
const state = createMetadataState(request);
|
||||
|
||||
@@ -1728,6 +1751,9 @@ describe("refreshChatMetadata", () => {
|
||||
resolveCommands = resolve;
|
||||
});
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "models.list") {
|
||||
return { models: [] };
|
||||
}
|
||||
expect(method).toBe("commands.list");
|
||||
return await commands;
|
||||
});
|
||||
|
||||
@@ -332,7 +332,7 @@ describe("new-session model runtime", () => {
|
||||
|
||||
it.each([
|
||||
["generic transport error", new Error("metadata unavailable")],
|
||||
["request timeout", new Error("gateway request timeout for chat.metadata")],
|
||||
["request timeout", new Error("gateway request timeout for models.list")],
|
||||
])("renders %s as unavailable instead of a default-only catalog", async (_label, error) => {
|
||||
const { context, request } = contextWith([]);
|
||||
request.mockRejectedValueOnce(error);
|
||||
|
||||
@@ -608,7 +608,7 @@ export class NewSessionModelControl {
|
||||
loading: false,
|
||||
modelCatalog: this.catalog,
|
||||
modelCatalogState: {
|
||||
// chat.metadata and agents.list hydrate independently. Do not expose a
|
||||
// models.list and agents.list hydrate independently. Do not expose a
|
||||
// ready catalog until the selected agent can supply its concrete defaults.
|
||||
hasSnapshot: agentDefaultsAvailable && this.metadataState.hasSnapshot,
|
||||
...(this.metadataState.status === "error" ? { onRetry: this.retryMetadata } : {}),
|
||||
|
||||
Reference in New Issue
Block a user