mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix: avoid sidebar catalog errors for session-only users (#162944)
Keep ordinary session browsing available without unauthorized external catalog requests, and retire catalog state when read authority is lost. Fixes #162900.
This commit is contained in:
parent
d77cdd2af6
commit
898f7f49b9
20 changed files with 231 additions and 19 deletions
|
|
@ -8,7 +8,7 @@ import { GatewayRequestError, type GatewayBrowserClient } from "../api/gateway.t
|
|||
import type { ApplicationGatewaySnapshot } from "../app/gateway.ts";
|
||||
import { formatUiError } from "../lib/format-error.ts";
|
||||
import { isAwaitingGatewayFailure } from "../lib/gateway-availability.ts";
|
||||
import { isGatewayMethodAdvertised } from "../lib/gateway-methods.ts";
|
||||
import { canCallGatewayMethod } from "../lib/gateway-methods.ts";
|
||||
import { createSessionEventRefreshCoordinator } from "../lib/sessions/event-refresh-coordinator.ts";
|
||||
import { normalizeAgentId } from "../lib/sessions/session-key.ts";
|
||||
import { generateUUID } from "../lib/uuid.ts";
|
||||
|
|
@ -28,9 +28,8 @@ export function sessionCatalogListClient(
|
|||
): GatewayBrowserClient | null {
|
||||
if (
|
||||
!connected ||
|
||||
snapshot?.phase !== "connected" ||
|
||||
!snapshot.client ||
|
||||
isGatewayMethodAdvertised(snapshot, "sessions.catalog.list") !== true
|
||||
!snapshot?.client ||
|
||||
!canCallGatewayMethod(snapshot, "sessions.catalog.list", "operator.read")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ async function mountDiscovery(
|
|||
const gateway = createGatewayHarness(createTestGatewayClient(request));
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -84,6 +85,7 @@ describe("AppSidebar hidden catalog discovery", () => {
|
|||
const gateway = createGatewayHarness(createTestGatewayClient(currentRequest));
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: {
|
||||
methods: ["sessions.catalog.list"],
|
||||
events: ["sessions.catalog.changed"],
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ async function mountTab(
|
|||
const gateway = createGatewayHarness(createTestGatewayClient(request));
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ async function mountExpanded(request: ReturnType<typeof vi.fn>) {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
TWO_AGENTS,
|
||||
} from "../test-helpers/app-sidebar.ts";
|
||||
import { createTestGatewayClient } from "../test-helpers/gateway-client.ts";
|
||||
import { gatewayHelloForMethods } from "../test-helpers/gateway-methods.ts";
|
||||
import "./app-sidebar.ts";
|
||||
|
||||
const requireRecord = createRequireRecord("object", "expected-label");
|
||||
|
|
@ -36,6 +37,7 @@ describe("AppSidebar catalog scope replacement", () => {
|
|||
const gateway = createGatewayHarness(client);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -72,3 +74,86 @@ describe("AppSidebar catalog scope replacement", () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("AppSidebar catalog authority", () => {
|
||||
it.each(["reply", "error", "page"] as const)(
|
||||
"retires cached and in-flight %s data on same-client scope loss",
|
||||
async (pendingKind) => {
|
||||
vi.useFakeTimers();
|
||||
const pending = deferred<ReturnType<typeof catalogPage>>();
|
||||
const request = vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
catalogPage([{ threadId: "retained", name: "Retained catalog session" }], "page-2"),
|
||||
);
|
||||
const gateway = createGatewayHarness(createTestGatewayClient(request));
|
||||
const hello = gatewayHelloForMethods(["sessions.catalog.list"]);
|
||||
hello.features!.events = ["sessions.catalog.changed"];
|
||||
gateway.publish({ hello });
|
||||
const { sidebar, provider } = await mountSidebar(
|
||||
gateway.gateway,
|
||||
createSessions("main", ["agent:main:visitor-notes"]),
|
||||
);
|
||||
try {
|
||||
sidebar.connected = true;
|
||||
await sidebar.updateComplete;
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await sidebar.updateComplete;
|
||||
expect(sidebar.textContent).toContain("Retained catalog session");
|
||||
expect(
|
||||
sidebar.querySelector('[data-session-key="agent:main:visitor-notes"]'),
|
||||
).not.toBeNull();
|
||||
request.mockReturnValueOnce(pending.promise);
|
||||
const loading =
|
||||
pendingKind === "page"
|
||||
? sidebar.sessionData.loadMoreSessionCatalog("codex")
|
||||
: sidebar.sessionData.refreshSessionCatalogs();
|
||||
const progressId = request.mock.calls.at(-1)?.[1]?.progressId;
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
|
||||
gateway.publish({
|
||||
hello: { ...hello, auth: { role: "operator", scopes: ["operator.sessions.write"] } },
|
||||
});
|
||||
// No Lit render or promise turn separates authority loss from these events.
|
||||
gateway.publishEvent("sessions.catalog.host", {
|
||||
progressId,
|
||||
agentId: "main",
|
||||
catalog: catalogPage([{ threadId: "stale", name: "Late catalog session" }]).catalogs[0],
|
||||
});
|
||||
gateway.publishEvent("sessions.catalog.changed", { agentId: "main" });
|
||||
if (pendingKind === "error") {
|
||||
pending.reject(new Error("Stale catalog failure"));
|
||||
} else {
|
||||
pending.resolve(catalogPage([{ threadId: "stale", name: "Late catalog session" }]));
|
||||
}
|
||||
await loading;
|
||||
await sidebar.updateComplete;
|
||||
await vi.advanceTimersByTimeAsync(600_000);
|
||||
await sidebar.updateComplete;
|
||||
expect(sidebar.textContent).not.toContain("Retained catalog session");
|
||||
expect(sidebar.textContent).not.toContain("Late catalog session");
|
||||
expect(sidebar.querySelector(".sidebar-session-catalog-error")).toBeNull();
|
||||
expect(sidebar.sessionData.sessionCatalogs).toEqual([]);
|
||||
expect(sidebar.sessionData.sessionCatalogPageDepths.size).toBe(0);
|
||||
expect(sidebar.sessionData.sessionCatalogRevisions.size).toBe(0);
|
||||
expect(sidebar.sessionData.loadingMoreSessionCatalogIds.size).toBe(0);
|
||||
expect(sidebar.sessionData.sessionCatalogLive.timer).toBeNull();
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
sidebar.querySelector('[data-session-key="agent:main:visitor-notes"]'),
|
||||
).not.toBeNull();
|
||||
|
||||
gateway.publish({ hello });
|
||||
await sidebar.updateComplete;
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await sidebar.updateComplete;
|
||||
expect(request).toHaveBeenCalledTimes(3);
|
||||
expect(sidebar.textContent).toContain("Retained catalog session");
|
||||
} finally {
|
||||
pending.resolve(catalogPage([]));
|
||||
provider.remove();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import type {
|
|||
} from "../../../packages/gateway-protocol/src/index.ts";
|
||||
import type { GatewayBrowserClient } from "../api/gateway.ts";
|
||||
import type { ApplicationContext } from "../app/context.ts";
|
||||
import { isGatewayMethodAdvertised } from "../lib/gateway-methods.ts";
|
||||
import { canCallGatewayMethod } from "../lib/gateway-methods.ts";
|
||||
import { readSessionMethodScopeAccess } from "../lib/session-method-access.ts";
|
||||
import {
|
||||
buildCatalogSessionKey,
|
||||
|
|
@ -91,8 +91,11 @@ export function areSessionCatalogsSettled(
|
|||
const status = owner.sessionCatalogRefreshStatus;
|
||||
return (
|
||||
!status.error &&
|
||||
(isGatewayMethodAdvertised(owner.context?.gateway.snapshot ?? {}, "sessions.catalog.list") !==
|
||||
true ||
|
||||
(!canCallGatewayMethod(
|
||||
owner.context?.gateway.snapshot,
|
||||
"sessions.catalog.list",
|
||||
"operator.read",
|
||||
) ||
|
||||
(status.hasLoaded &&
|
||||
!status.awaitingGateway &&
|
||||
owner.sessionCatalogLive.requestGeneration === null &&
|
||||
|
|
@ -143,7 +146,7 @@ export function resolveSessionCatalogAgentId(
|
|||
// transient reconnect still preserves its rows until the replacement lands.
|
||||
if (
|
||||
gateway?.phase === "connected" &&
|
||||
isGatewayMethodAdvertised(gateway, "sessions.catalog.list") === false
|
||||
!canCallGatewayMethod(gateway, "sessions.catalog.list", "operator.read")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -181,7 +184,7 @@ export function resolveSessionCatalogAgentId(
|
|||
}
|
||||
|
||||
export function scheduleSessionCatalogRefresh(owner: SessionCatalogDataOwner): void {
|
||||
if (document.visibilityState === "hidden") {
|
||||
if (!visibleSessionCatalogClient(owner)) {
|
||||
owner.sessionCatalogLive.cancelScheduledRefreshes();
|
||||
return;
|
||||
}
|
||||
|
|
@ -385,6 +388,7 @@ export async function loadMoreSessionCatalog(
|
|||
hostIds?: readonly string[],
|
||||
discovering = false,
|
||||
): Promise<void> {
|
||||
owner.synchronizeSessionScope();
|
||||
if (owner.loadingMoreSessionCatalogIds.has(catalogId)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -399,7 +403,7 @@ export async function loadMoreSessionCatalog(
|
|||
if (!catalog || Object.keys(cursors).length === 0) {
|
||||
return;
|
||||
}
|
||||
const client = owner.context?.gateway.snapshot.client;
|
||||
const client = owner.sessionCatalogGatewayClient();
|
||||
const agentId = resolveSessionCatalogAgentId(owner);
|
||||
if (
|
||||
!client ||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { SIDEBAR_SESSION_ROSTER_LIMIT } from "../../../src/shared/session-list-l
|
|||
import type { ApplicationContext } from "../app/context.ts";
|
||||
import { readPresenceEntries, type PresencePayload } from "../app/user-profile.ts";
|
||||
import type { AgentCapability } from "../lib/agents/index.ts";
|
||||
import { canCallGatewayMethod } from "../lib/gateway-methods.ts";
|
||||
import { CATALOG_SESSION_CONTINUED_EVENT } from "../lib/sessions/catalog-key.ts";
|
||||
import type {
|
||||
SessionCapability,
|
||||
|
|
@ -227,11 +228,15 @@ export function subscribeSessionDataGatewayEvents(
|
|||
): () => void {
|
||||
return gateway.subscribeEvents((event) => {
|
||||
if (event.event === "sessions.catalog.host") {
|
||||
owner.handleSessionCatalogHostEvent(event.payload);
|
||||
if (canCallGatewayMethod(gateway.snapshot, "sessions.catalog.list", "operator.read")) {
|
||||
owner.handleSessionCatalogHostEvent(event.payload);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.event === "sessions.catalog.changed") {
|
||||
owner.handleSessionCatalogChanged(event.payload);
|
||||
if (canCallGatewayMethod(gateway.snapshot, "sessions.catalog.list", "operator.read")) {
|
||||
owner.handleSessionCatalogChanged(event.payload);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.event === "presence") {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@ import {
|
|||
hydrateSidebarChildSessions,
|
||||
retireStaleChildSessionRows,
|
||||
} from "./app-sidebar-child-session-data.ts";
|
||||
import { SessionCatalogLiveState } from "./app-sidebar-session-catalog-live.ts";
|
||||
import {
|
||||
SessionCatalogLiveState,
|
||||
sessionCatalogListClient,
|
||||
} from "./app-sidebar-session-catalog-live.ts";
|
||||
import type {
|
||||
SidebarSessionMutationScope,
|
||||
SidebarSessionsScrollState,
|
||||
|
|
@ -224,7 +227,7 @@ export class SessionDataController implements ReactiveController, SessionCatalog
|
|||
}
|
||||
|
||||
sessionCatalogGatewayClient(): GatewayBrowserClient | null {
|
||||
return this.gatewayClient;
|
||||
return sessionCatalogListClient(this.context?.gateway.snapshot, this.host.connected);
|
||||
}
|
||||
|
||||
private synchronizeOwnerSessionCounts(): void {
|
||||
|
|
@ -429,9 +432,10 @@ export class SessionDataController implements ReactiveController, SessionCatalog
|
|||
const available = isGatewayAvailable(gateway.snapshot);
|
||||
const becameAvailable = available && !this.gatewayAvailable;
|
||||
this.gatewayAvailable = available;
|
||||
// Presence and auth snapshots must not retire this client's in-flight
|
||||
// native or catalog pages unless its connection phase actually changes.
|
||||
// Presence updates preserve in-flight pages, but a new authority projection
|
||||
// can revoke catalog ownership without replacing the socket client.
|
||||
if (!sourceOrClientChanged && !connectionChanged) {
|
||||
this.synchronizeSessionScope();
|
||||
this.synchronizeOwnerSessionCounts();
|
||||
const { awaitingGateway, error } = this.sessionCatalogRefreshStatus;
|
||||
const requesting = this.sessionCatalogLive.requestGeneration !== null;
|
||||
|
|
|
|||
67
ui/src/e2e/sidebar-catalog-scope.e2e.test.ts
Normal file
67
ui/src/e2e/sidebar-catalog-scope.e2e.test.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import type { SessionDataController } from "../components/session-data-controller.ts";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
defaultControlUiFeatureMethods,
|
||||
installMockGateway,
|
||||
} from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiSessionRow } from "../test-helpers/control-ui-session-fixtures.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
const suite = createControlUiE2eSuite({ name: "Sidebar catalog scope" });
|
||||
|
||||
suite.define(() => {
|
||||
it("discovers and opens ordinary guest sessions without requesting the external catalog", async () => {
|
||||
const artifactDir = createControlUiE2eArtifactDir("sidebar-catalog-scope");
|
||||
const viewport = { width: 1440, height: 900 };
|
||||
const context = await suite.newBrowserContext({
|
||||
viewport,
|
||||
recordVideo: { dir: artifactDir, size: viewport },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
try {
|
||||
const key = "agent:main:visitor-notes";
|
||||
const gateway = await installMockGateway(page, {
|
||||
sessionKey: key,
|
||||
operatorScopes: ["operator.sessions.write"],
|
||||
featureMethods: [...defaultControlUiFeatureMethods, "sessions.catalog.list"],
|
||||
sessions: [createControlUiSessionRow(key, "Visitor notes", Date.now())],
|
||||
historyMessages: [
|
||||
{ role: "assistant", content: [{ type: "text", text: "Welcome back." }] },
|
||||
],
|
||||
methodResponses: {
|
||||
"sessions.catalog.list": {
|
||||
__mockError: { code: "INVALID_REQUEST", message: "missing scope: operator.read" },
|
||||
},
|
||||
},
|
||||
});
|
||||
await page.goto(`${suite.server.baseUrl}new`);
|
||||
const sidebar = page.locator("openclaw-app-sidebar");
|
||||
const session = sidebar.locator(`[data-session-key="${key}"]`).first();
|
||||
await session.waitFor({ state: "visible" });
|
||||
await page.waitForFunction(() => {
|
||||
const data = document.querySelector<HTMLElement & { sessionData: SessionDataController }>(
|
||||
"openclaw-app-sidebar",
|
||||
)?.sessionData;
|
||||
return (
|
||||
data &&
|
||||
(data.sessionCatalogAgentId === null ||
|
||||
data.sessionCatalogRefreshStatus.error !== null ||
|
||||
data.sessionCatalogRefreshStatus.hasLoaded)
|
||||
);
|
||||
});
|
||||
await page.screenshot({ path: path.join(artifactDir, "sidebar.png") });
|
||||
expect((await gateway.getRequests("sessions.list")).length).toBeGreaterThan(0);
|
||||
expect(await gateway.getRequests("sessions.catalog.list")).toEqual([]);
|
||||
expect(await sidebar.getByRole("alert").count()).toBe(0);
|
||||
await session.click();
|
||||
await page.getByText("Welcome back.", { exact: true }).waitFor();
|
||||
expect(await gateway.getRequests("sessions.catalog.list")).toEqual([]);
|
||||
expect(await sidebar.getByRole("alert").count()).toBe(0);
|
||||
await page.screenshot({ path: path.join(artifactDir, "opened-session.png") });
|
||||
} finally {
|
||||
await suite.closeBrowserContext(context);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -13,6 +13,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -81,6 +82,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -170,6 +172,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -245,6 +248,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -291,6 +295,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ describe("AppSidebar session catalog request errors", () => {
|
|||
gateway.publish({
|
||||
assistantAgentId: "roboclaw",
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -43,6 +44,7 @@ describe("AppSidebar session catalog request errors", () => {
|
|||
gateway.publish({
|
||||
assistantAgentId: "roboclaw",
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -69,6 +71,7 @@ describe("AppSidebar session catalog request errors", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -108,6 +111,7 @@ describe("AppSidebar session catalog request errors", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -146,6 +150,7 @@ describe("AppSidebar session catalog request errors", () => {
|
|||
gateway.publish({
|
||||
assistantAgentId: "roboclaw",
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -193,6 +198,7 @@ describe("AppSidebar session catalog request errors", () => {
|
|||
gateway.publish({
|
||||
assistantAgentId: null,
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -224,6 +230,7 @@ describe("AppSidebar session catalog request errors", () => {
|
|||
gateway.publish({
|
||||
suspensionPhase: "accepting",
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -284,6 +291,7 @@ describe("AppSidebar session catalog request errors", () => {
|
|||
gateway.publish({
|
||||
suspensionPhase: "accepting",
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -46,6 +47,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -101,6 +103,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -164,6 +167,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -207,6 +211,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -257,6 +262,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -276,6 +282,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
gateway.publish({
|
||||
phase: "connected",
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
} as unknown as GatewayBrowserClient);
|
||||
previousGateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -45,6 +46,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
} as unknown as GatewayBrowserClient);
|
||||
currentGateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -314,6 +316,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -374,6 +377,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -443,6 +447,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -508,6 +513,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -575,6 +581,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -616,6 +623,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -69,9 +69,14 @@ describe("AppSidebar session catalog ownership", () => {
|
|||
it.each([
|
||||
{ owner: "the selected agent", assistantAgentId: null },
|
||||
{ owner: "the advertised catalog capability", assistantAgentId: "main" },
|
||||
{
|
||||
owner: "catalog read authority",
|
||||
assistantAgentId: "main",
|
||||
scopes: ["operator.sessions.write"],
|
||||
},
|
||||
])(
|
||||
"retires catalog rows and creation after reconnect loses $owner",
|
||||
async ({ assistantAgentId }) => {
|
||||
async ({ assistantAgentId, scopes }) => {
|
||||
vi.useFakeTimers();
|
||||
let provider: HTMLElement | undefined;
|
||||
try {
|
||||
|
|
@ -128,7 +133,9 @@ describe("AppSidebar session catalog ownership", () => {
|
|||
gateway.publish({
|
||||
phase: "connected",
|
||||
assistantAgentId,
|
||||
hello: { ...catalogHello, features: { ...catalogHello.features, methods: [] } },
|
||||
hello: scopes
|
||||
? { ...catalogHello, auth: { role: "operator", scopes } }
|
||||
: { ...catalogHello, features: { ...catalogHello.features, methods: [] } },
|
||||
});
|
||||
await sidebar.updateComplete;
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ export function registerCatalogPageHostTests() {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
});
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -90,6 +91,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
});
|
||||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
const hello = {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"];
|
||||
gateway.publish({ hello });
|
||||
|
|
@ -171,6 +173,7 @@ describe("AppSidebar session catalog pagination", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ describe("AppSidebar catalog reconnect", () => {
|
|||
.mockResolvedValue(catalogPage([]));
|
||||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
const hello = {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"];
|
||||
gateway.publish({ hello });
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ async function mountWithCatalog(
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.admin"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -372,6 +372,7 @@ describe("AppSidebar catalog session rows", () => {
|
|||
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.admin"] },
|
||||
features: { methods: ["sessions.catalog.list"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
@ -576,10 +577,11 @@ describe("AppSidebar catalog session rows", () => {
|
|||
const items = menu.querySelectorAll<HTMLElement & { disabled: boolean }>("wa-dropdown-item");
|
||||
expect([...items].map((item) => item.getAttribute("value"))).toEqual([
|
||||
"viewer",
|
||||
"import",
|
||||
"terminal",
|
||||
"delete",
|
||||
]);
|
||||
expect(items[1]?.disabled).toBe(true);
|
||||
expect(items[2]?.disabled).toBe(true);
|
||||
const menuButton = row.querySelector<HTMLElement>("[data-catalog-session-menu]");
|
||||
expect(menuButton).not.toBeNull();
|
||||
|
||||
|
|
|
|||
|
|
@ -640,6 +640,7 @@ export async function mountSessionCatalogSidebar(client: GatewayBrowserClient) {
|
|||
const gateway = createGatewayHarness(client);
|
||||
gateway.publish({
|
||||
hello: {
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
features: { methods: ["sessions.catalog.list"], events: ["sessions.catalog.changed"] },
|
||||
} as ApplicationGatewaySnapshot["hello"],
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue