From 5b7ecccfae78d75f09525bc061a1d3d69481dea4 Mon Sep 17 00:00:00 2001 From: Galin Iliev Date: Sun, 9 Aug 2026 22:53:52 -0700 Subject: [PATCH] test(memory): inventory Control UI migration routes --- .../authorization-conformance-scenarios.ts | 22 ++--- .../src/host/authorization-conformance.ts | 4 +- src/agents/project-memory-bootstrap.test.ts | 4 +- ...thorization-path-inventory.test-support.ts | 4 + ...emory-authorization-path-inventory.test.ts | 85 ++++++++++++++++++- 5 files changed, 104 insertions(+), 15 deletions(-) diff --git a/packages/memory-host-sdk/src/host/authorization-conformance-scenarios.ts b/packages/memory-host-sdk/src/host/authorization-conformance-scenarios.ts index d8e00f6f8e75..e8962f64bfdd 100644 --- a/packages/memory-host-sdk/src/host/authorization-conformance-scenarios.ts +++ b/packages/memory-host-sdk/src/host/authorization-conformance-scenarios.ts @@ -570,7 +570,9 @@ export function createMemoryAuthorizationConformanceScenarios(): MemoryAuthoriza ...staleMount, plan: { ...staleMount.plan, - mounts: staleMount.plan.mounts.map((mount) => ({ ...mount, storeId: "store-b" })), + mounts: staleMount.plan.mounts.map((mount) => + Object.assign({}, mount, { storeId: "store-b" }), + ), }, }); @@ -579,10 +581,9 @@ export function createMemoryAuthorizationConformanceScenarios(): MemoryAuthoriza ...staleMountCapabilities, plan: { ...staleMountCapabilities.plan, - mounts: staleMountCapabilities.plan.mounts.map((mount) => ({ - ...mount, - capabilities: ["retrieve"], - })), + mounts: staleMountCapabilities.plan.mounts.map((mount) => + Object.assign({}, mount, { capabilities: ["retrieve"] }), + ), }, }); @@ -591,7 +592,9 @@ export function createMemoryAuthorizationConformanceScenarios(): MemoryAuthoriza ...staleMountAgent, plan: { ...staleMountAgent.plan, - mounts: staleMountAgent.plan.mounts.map((mount) => ({ ...mount, agentId: "agent-b" })), + mounts: staleMountAgent.plan.mounts.map((mount) => + Object.assign({}, mount, { agentId: "agent-b" }), + ), }, }); @@ -600,10 +603,9 @@ export function createMemoryAuthorizationConformanceScenarios(): MemoryAuthoriza ...staleMountAudience, plan: { ...staleMountAudience.plan, - mounts: staleMountAudience.plan.mounts.map((mount) => ({ - ...mount, - audienceRevision: "audience-revision-2", - })), + mounts: staleMountAudience.plan.mounts.map((mount) => + Object.assign({}, mount, { audienceRevision: "audience-revision-2" }), + ), }, }); diff --git a/packages/memory-host-sdk/src/host/authorization-conformance.ts b/packages/memory-host-sdk/src/host/authorization-conformance.ts index fb9975a303f7..32671f04100d 100644 --- a/packages/memory-host-sdk/src/host/authorization-conformance.ts +++ b/packages/memory-host-sdk/src/host/authorization-conformance.ts @@ -215,7 +215,7 @@ function hasExactlyTheSameUniqueSet( actualKeys.length === actualSet.size && expectedKeys.length === expectedSet.size && actualSet.size === expectedSet.size && - [...actualSet].every((key) => expectedSet.has(key)) + [...actualSet].every((candidateKey) => expectedSet.has(candidateKey)) ); } @@ -224,7 +224,7 @@ function mountKey(mount: MemoryAuthorizationConformanceMount): string { mount.storeId, mount.agentId, mount.audienceRevision, - [...mount.capabilities].sort(), + mount.capabilities.toSorted(), ]); } diff --git a/src/agents/project-memory-bootstrap.test.ts b/src/agents/project-memory-bootstrap.test.ts index 06e98b0b77c0..90e5a91e981c 100644 --- a/src/agents/project-memory-bootstrap.test.ts +++ b/src/agents/project-memory-bootstrap.test.ts @@ -127,7 +127,9 @@ describe("project memory bootstrap", () => { it("renders budgeted curated candidates through the selected active runtime", async () => { const rendered = await prepareEntries(entries); - expect(getSelectedMemoryRuntime()?.getMemorySearchManager).toBe(runtimeMocks.getManager); + expect(getSelectedMemoryRuntime()).toEqual( + expect.objectContaining({ getMemorySearchManager: runtimeMocks.getManager }), + ); expect(runtimeMocks.getManager).toHaveBeenCalledOnce(); expect(runtimeMocks.listCurated).toHaveBeenCalledWith({ activeProjectKeys: ["github.com/OpenClaw/OpenClaw"], diff --git a/src/plugins/memory-authorization-path-inventory.test-support.ts b/src/plugins/memory-authorization-path-inventory.test-support.ts index 905165c6448c..54ee289e5351 100644 --- a/src/plugins/memory-authorization-path-inventory.test-support.ts +++ b/src/plugins/memory-authorization-path-inventory.test-support.ts @@ -423,6 +423,7 @@ export const MEMORY_AUTHORIZATION_PATH_INVENTORY = Object.freeze([ "extensions/memory-core/src/session-backfill-gateway.runtime.ts", "extensions/memory-core/src/session-backfill.ts", "extensions/memory-core/src/session-ingestion.ts", + "ui/src/pages/memory-import/memory-import-page.ts", ), entry( "session-backfill-derived-memory-write", @@ -444,6 +445,7 @@ export const MEMORY_AUTHORIZATION_PATH_INVENTORY = Object.freeze([ "extensions/memory-core/src/short-term-promotion-record.ts", "extensions/memory-core/src/short-term-promotion-artifacts.ts", "extensions/memory-core/src/dreaming-narrative.ts", + "ui/src/pages/memory-import/memory-import-page.ts", ), entry( "dreaming-session-transcript-ingestion", @@ -500,6 +502,8 @@ export const MEMORY_AUTHORIZATION_PATH_INVENTORY = Object.freeze([ "src/wizard/setup.post-install-migration.ts", "src/wizard/setup.memory-import.ts", "src/plugin-sdk/migration-runtime.ts", + "ui/src/components/onboarding-memory-import.ts", + "ui/src/pages/memory-import/memory-import-page.ts", ), entry( "memory-export", diff --git a/src/plugins/memory-authorization-path-inventory.test.ts b/src/plugins/memory-authorization-path-inventory.test.ts index 1aaf730c4869..36b3f999fb85 100644 --- a/src/plugins/memory-authorization-path-inventory.test.ts +++ b/src/plugins/memory-authorization-path-inventory.test.ts @@ -141,6 +141,8 @@ const MEMORY_MIGRATION_IMPORT_ROUTE_SURFACES = [ "src/wizard/setup.post-install-migration.ts", "src/wizard/setup.memory-import.ts", "src/plugin-sdk/migration-runtime.ts", + "ui/src/components/onboarding-memory-import.ts", + "ui/src/pages/memory-import/memory-import-page.ts", ] as const; const MEMORY_MIGRATION_IMPORT_ROOTS = [ @@ -152,8 +154,22 @@ const MEMORY_MIGRATION_IMPORT_ROOTS = [ "src/gateway/server-methods", "src/wizard", "src/plugin-sdk", + "ui/src", ] as const; +const SESSION_BACKFILL_INGRESS_ROOTS = ["extensions/memory-core/src", "ui/src"] as const; + +const MEMORY_MIGRATION_IMPORT_REQUEST_METHODS = new Set([ + "migrations.memory.plan", + "migrations.memory.apply", +]); + +const SESSION_BACKFILL_REQUEST_METHODS = new Set([ + "memory.sessionBackfill.preview", + "memory.sessionBackfill.apply", + "memory.sessionBackfill.rollback", +]); + const MEMORY_CORE_DOCTOR_STATE_MIGRATION_REGISTRATION_SURFACES = [ "extensions/memory-core/doctor-contract-api.ts", "src/plugins/doctor-contract-registry.ts", @@ -279,6 +295,19 @@ function isMigrationProviderApplyCall(expression: ts.LeftHandSideExpression): bo ); } +function directClientRequestMethod(node: ts.CallExpression): string | null { + if ( + !ts.isPropertyAccessExpression(node.expression) || + !ts.isIdentifier(node.expression.expression) || + node.expression.expression.text !== "client" || + node.expression.name.text !== "request" + ) { + return null; + } + const method = node.arguments[0]; + return method && ts.isStringLiteral(method) ? method.text : null; +} + function containsMemoryStringLiteral(node: ts.Node): boolean { let found = false; const visit = (child: ts.Node) => { @@ -330,6 +359,10 @@ function listMemoryMigrationIngressMarkers(file: string, sourceText: string): st markers.push(node.getText(source)); } if (ts.isCallExpression(node)) { + const requestMethod = directClientRequestMethod(node); + if (requestMethod && MEMORY_MIGRATION_IMPORT_REQUEST_METHODS.has(requestMethod)) { + markers.push(requestMethod); + } if (isNamedCall(node.expression, copyMemoryBindings)) { markers.push(node.getText(source)); } @@ -357,6 +390,22 @@ function listMemoryMigrationIngressMarkers(file: string, sourceText: string): st return markers; } +function listSessionBackfillIngressMarkers(file: string, sourceText: string): string[] { + const source = ts.createSourceFile(file, sourceText, ts.ScriptTarget.Latest, true); + const markers: string[] = []; + const visit = (node: ts.Node) => { + if (ts.isCallExpression(node)) { + const requestMethod = directClientRequestMethod(node); + if (requestMethod && SESSION_BACKFILL_REQUEST_METHODS.has(requestMethod)) { + markers.push(requestMethod); + } + } + ts.forEachChild(node, visit); + }; + visit(source); + return markers; +} + function listSessionTranscriptIngestionCalls(file: string, sourceText: string): string[] { const source = ts.createSourceFile(file, sourceText, ts.ScriptTarget.Latest, true); const callBindings = new Map( @@ -658,6 +707,7 @@ describe("memory authorization path inventory", () => { "extensions/memory-core/src/session-backfill-gateway.ts", "extensions/memory-core/src/session-backfill-gateway.runtime.ts", "extensions/memory-core/src/session-backfill.ts", + "ui/src/pages/memory-import/memory-import-page.ts", ]), }); expect(entriesById.get("session-backfill-derived-memory-write")).toMatchObject({ @@ -678,6 +728,7 @@ describe("memory authorization path inventory", () => { "extensions/memory-core/src/session-ingestion.ts", "extensions/memory-core/src/short-term-promotion-record.ts", "extensions/memory-core/src/short-term-promotion-artifacts.ts", + "ui/src/pages/memory-import/memory-import-page.ts", ]), }); expect(entriesById.get("dreaming-session-transcript-ingestion")).toMatchObject({ @@ -797,6 +848,32 @@ describe("memory authorization path inventory", () => { ).toEqual(["provider-memory-import-apply"]); }); + it("finds direct Control UI migration and session-backfill requests", () => { + const source = ` + async function importMemory( + client: { request: Function }, + unrelated: { request: Function }, + ) { + await client.request("migrations.memory.plan", {}); + await client.request("migrations.memory.apply", {}); + await client.request("memory.sessionBackfill.preview", {}); + await client.request("memory.sessionBackfill.apply", {}); + await client.request("memory.sessionBackfill.rollback", {}); + await unrelated.request("migrations.memory.plan", {}); + } + `; + + expect(listMemoryMigrationIngressMarkers("fixture.ts", source)).toEqual([ + "migrations.memory.plan", + "migrations.memory.apply", + ]); + expect(listSessionBackfillIngressMarkers("fixture.ts", source)).toEqual([ + "memory.sessionBackfill.preview", + "memory.sessionBackfill.apply", + "memory.sessionBackfill.rollback", + ]); + }); + it("finds direct and aliased short-term recall recorders without matching properties", () => { expect( listImportedShortTermRecallRecordingCalls( @@ -883,9 +960,9 @@ describe("memory authorization path inventory", () => { expect(missing).toEqual([]); }); - it("does not allow an unrecorded production session transcript ingestion route", () => { + it("does not allow an unrecorded production session transcript or Control UI backfill route", () => { const scanned = listRepoFilesSync(REPO_ROOT, { - roots: ["extensions/memory-core/src"], + roots: SESSION_BACKFILL_INGRESS_ROOTS, includeFile: isProductionTypeScript, }); const inventoried = new Set(inventory.flatMap((item) => item.surfaces)); @@ -895,6 +972,10 @@ describe("memory authorization path inventory", () => { listSessionTranscriptIngestionCalls( file, fs.readFileSync(path.join(REPO_ROOT, file), "utf8"), + ).length > 0 || + listSessionBackfillIngressMarkers( + file, + fs.readFileSync(path.join(REPO_ROOT, file), "utf8"), ).length > 0, ) .filter((file) => !inventoried.has(file));