mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
8b66fc103d
* feat(ui): durable session board face and dashboards index Board face lived only in client-side boardSessionViews, capped at 50 entries, so the preference never followed the user to another device, evicted as sessions accumulated, and could not be seen as a set. Persist it as SessionEntry.boardFace, which rides the existing entry_json blob and so needs no SQLite schema change or version bump. Expose it on the session list row and add it to the sessions.patch write-scope allowlist alongside label, pinned, and archived: setting your own view preference is user-level chat organization, not policy. Unknown patch fields still fail closed to operator.admin. Generic navigation now reads the stored face, so the sidebar and session list open a thread on the face you left it on. boardSessionViews keeps only activeTabId and reopenDockByTab, which are genuinely per-device. Add /dashboards listing threads whose preferred face is dashboard. Filtering runs server-side in filterSessionEntries before pagination, because the client holds only a capped page and a client-side filter would silently omit dashboards. * test(protocol): assert the pre-rename face param is rejected The gateway-protocol validator test still passed the pre-rename 'face' key, which the closed schema rejects. Use boardFace, and pin the old name as a negative case so it cannot silently return. * chore(protocol): regenerate Swift bindings and docs map for boardFace Adding boardFace to the sessions schema changes two committed generated artifacts: the Swift gateway models (pnpm protocol:gen:swift) and the docs map (pnpm docs:map:gen), which now lists the dashboards index section.
85 lines
2.6 KiB
TypeScript
85 lines
2.6 KiB
TypeScript
/** Gateway durable session-face behavior. */
|
|
import { expect, test } from "vitest";
|
|
import { rpcReq, writeSessionStore } from "./test-helpers.js";
|
|
import {
|
|
directSessionReq,
|
|
setupGatewaySessionsTestHarness,
|
|
} from "./test/server-sessions.test-helpers.js";
|
|
|
|
const { createSessionStoreDir, openClient } = setupGatewaySessionsTestHarness();
|
|
|
|
test("a write-scoped face patch is visible to another client", async () => {
|
|
await createSessionStoreDir();
|
|
await writeSessionStore({
|
|
entries: {
|
|
main: { sessionId: "sess-main", updatedAt: Date.now() },
|
|
},
|
|
});
|
|
|
|
const firstClient = await openClient({ scopes: ["operator.read", "operator.write"] });
|
|
try {
|
|
const patched = await rpcReq<{ ok: true; entry: { boardFace?: string } }>(
|
|
firstClient.ws,
|
|
"sessions.patch",
|
|
{ key: "agent:main:main", boardFace: "dashboard" },
|
|
);
|
|
expect(patched.ok).toBe(true);
|
|
expect(patched.payload?.entry.boardFace).toBe("dashboard");
|
|
|
|
const unknownField = await rpcReq(firstClient.ws, "sessions.patch", {
|
|
key: "agent:main:main",
|
|
futureFace: "dashboard",
|
|
});
|
|
expect(unknownField.ok).toBe(false);
|
|
expect(unknownField.error?.message).toContain("missing scope: operator.admin");
|
|
} finally {
|
|
firstClient.ws.close();
|
|
}
|
|
|
|
const secondClient = await openClient({ scopes: ["operator.read"] });
|
|
try {
|
|
const listed = await rpcReq<{ sessions: Array<{ key: string; boardFace?: string }> }>(
|
|
secondClient.ws,
|
|
"sessions.list",
|
|
{ boardFace: "dashboard" },
|
|
);
|
|
expect(listed.ok).toBe(true);
|
|
expect(listed.payload?.sessions).toMatchObject([
|
|
{ key: "agent:main:main", boardFace: "dashboard" },
|
|
]);
|
|
} finally {
|
|
secondClient.ws.close();
|
|
}
|
|
});
|
|
|
|
test("sessions.list applies face filtering before pagination", async () => {
|
|
await createSessionStoreDir();
|
|
const now = Date.now();
|
|
await writeSessionStore({
|
|
entries: {
|
|
...Object.fromEntries(
|
|
Array.from({ length: 51 }, (_, index) => [
|
|
`chat-${index}`,
|
|
{ sessionId: `sess-chat-${index}`, updatedAt: now - index },
|
|
]),
|
|
),
|
|
dashboard: {
|
|
sessionId: "sess-dashboard",
|
|
updatedAt: now - 10_000,
|
|
boardFace: "dashboard",
|
|
},
|
|
},
|
|
});
|
|
|
|
const listed = await directSessionReq<{
|
|
sessions: Array<{ key: string; boardFace?: string }>;
|
|
totalCount: number;
|
|
}>("sessions.list", { boardFace: "dashboard", limit: 50 });
|
|
|
|
expect(listed.ok).toBe(true);
|
|
expect(listed.payload?.totalCount).toBe(1);
|
|
expect(listed.payload?.sessions).toEqual([
|
|
expect.objectContaining({ key: "agent:main:dashboard", boardFace: "dashboard" }),
|
|
]);
|
|
});
|