diff --git a/src/gateway/session-group-mutation-targets.test.ts b/src/gateway/session-group-mutation-targets.test.ts index 49a576aeaf54..08c644dea27c 100644 --- a/src/gateway/session-group-mutation-targets.test.ts +++ b/src/gateway/session-group-mutation-targets.test.ts @@ -1,7 +1,8 @@ import fs from "node:fs"; import { performance } from "node:perf_hooks"; +import { DatabaseSync } from "node:sqlite"; import { expect, test, vi } from "vitest"; -import { upsertSessionEntryCore } from "../config/sessions/session-accessor.js"; +import { loadSessionEntry, upsertSessionEntryCore } from "../config/sessions/session-accessor.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import * as sqliteIntegrity from "../infra/sqlite-integrity.js"; import * as sqliteWal from "../infra/sqlite-wal.js"; @@ -9,6 +10,7 @@ import * as agentDatabaseLeases from "../state/openclaw-agent-db-lease.js"; import { closeOpenClawAgentDatabasesForTest, listOpenClawAgentDatabasesForTest, + openOpenClawAgentDatabase, } from "../state/openclaw-agent-db.js"; import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js"; import { setStateDirEnv, withStateDirEnv } from "../test-helpers/state-dir-env.js"; @@ -16,6 +18,89 @@ import { resolveSessionGroupMutationTargetsByName } from "./session-groups.js"; const EXPECTED_OPEN_HANDLE_CAP = 64; +test.each([false, true])( + "discovers current group members without decoding saved prompts (cold=%s)", + async (cold) => { + await withStateDirEnv("openclaw-session-group-metadata-", async ({ stateDir }) => { + setStateDirEnv(fs.realpathSync(stateDir)); + const scopes = [ + { agentId: "main", sessionKey: "agent:main:group-member" }, + { agentId: "research", sessionKey: "agent:research:matrix:group:!Room:example.org" }, + ] as const; + const config = { + agents: { list: [{ id: "main", default: true }, { id: "research" }] }, + } satisfies OpenClawConfig; + const entry = { + sessionId: "group-member", + updatedAt: 1, + category: " Shared work ", + skillsSnapshot: { prompt: "unneeded-group-prompt".repeat(4096), skills: [] }, + systemPromptReport: { + source: "run" as const, + generatedAt: 1, + systemPrompt: { chars: 1, projectContextChars: 0, nonProjectContextChars: 1 }, + injectedWorkspaceFiles: [], + skills: { promptChars: 0, entries: [] }, + tools: { listChars: 0, schemaChars: 0, entries: [] }, + }, + }; + const parse = vi.spyOn(JSON, "parse"); + const readTargets = () => { + parse.mockClear(); + const targets = resolveSessionGroupMutationTargetsByName(config); + expect( + parse.mock.calls.filter( + ([json]) => json.includes('"skillsSnapshot"') || json.includes('"systemPromptReport"'), + ), + ).toEqual([]); + return targets; + }; + try { + for (const scope of scopes) { + await upsertSessionEntryCore(scope, entry); + } + if (cold) { + closeOpenClawAgentDatabasesForTest(); + } + expect(readTargets()).toEqual(new Map([["Shared work", scopes]])); + await upsertSessionEntryCore(scopes[0], { ...entry, category: "Renamed" }); + expect(readTargets()).toEqual( + new Map([ + ["Renamed", [scopes[0]]], + ["Shared work", [scopes[1]]], + ]), + ); + const database = openOpenClawAgentDatabase(scopes[0]); + const external = new DatabaseSync(database.path); + try { + external + .prepare( + "UPDATE session_nodes SET entry_json = json_set(entry_json, '$.category', ?) WHERE session_key = ?", + ) + .run("External", scopes[0].sessionKey); + } finally { + external.close(); + } + expect(readTargets()).toEqual( + new Map([ + ["External", [scopes[0]]], + ["Shared work", [scopes[1]]], + ]), + ); + parse.mockRestore(); + expect(loadSessionEntry(scopes[0])).toMatchObject({ + skillsSnapshot: entry.skillsSnapshot, + systemPromptReport: entry.systemPromptReport, + }); + } finally { + parse.mockRestore(); + closeOpenClawAgentDatabasesForTest(); + closeOpenClawStateDatabaseForTest(); + } + }); + }, +); + test("discovers groups across more than the handle cap without writable database maintenance", async () => { await withStateDirEnv("openclaw-session-group-readonly-", async ({ stateDir }) => { setStateDirEnv(fs.realpathSync(stateDir)); diff --git a/src/gateway/session-groups.ts b/src/gateway/session-groups.ts index 1a617bf4709b..85d1c427f125 100644 --- a/src/gateway/session-groups.ts +++ b/src/gateway/session-groups.ts @@ -450,6 +450,9 @@ export function resolveSessionGroupMutationTargetsByName( for (const { sessionKey, entry } of listSessionEntriesReadOnly({ agentId: storeTarget.agentId, storePath: storeTarget.storePath, + // Membership only borrows category metadata; full reads decode every saved prompt. + projection: "list", + clone: false, })) { const groupName = normalizeOptionalString(entry.category); if (!groupName) {