test(memory): inventory Control UI migration routes

This commit is contained in:
Galin Iliev
2026-08-09 22:53:52 -07:00
parent a960a193d4
commit 5b7ecccfae
5 changed files with 104 additions and 15 deletions
@@ -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" }),
),
},
});
@@ -215,7 +215,7 @@ function hasExactlyTheSameUniqueSet<T>(
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(),
]);
}
+3 -1
View File
@@ -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"],
@@ -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",
@@ -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));