chore(ui): guard dreaming toggle for strict plugin schemas

This commit is contained in:
Vignesh Natarajan
2026-04-07 03:01:18 -07:00
parent ec708f44df
commit 3fbb229d04
2 changed files with 96 additions and 0 deletions

View File

@@ -193,6 +193,48 @@ describe("dreaming controller", () => {
});
});
it("blocks dreaming patch when selected plugin config rejects unknown keys", async () => {
const { state, request } = createState();
state.configSnapshot = {
hash: "hash-1",
config: {
plugins: {
slots: {
memory: "memory-lancedb",
},
},
},
};
request.mockImplementation(async (method: string) => {
if (method === "config.schema.lookup") {
return {
path: "plugins.entries.memory-lancedb.config",
schema: {
type: "object",
additionalProperties: false,
},
children: [
{ key: "retentionDays", path: "plugins.entries.memory-lancedb.config.retentionDays" },
],
};
}
if (method === "config.patch") {
return { ok: true };
}
return {};
});
const ok = await updateDreamingEnabled(state, true);
expect(ok).toBe(false);
expect(request).toHaveBeenCalledWith("config.schema.lookup", {
path: "plugins.entries.memory-lancedb.config",
});
expect(request).not.toHaveBeenCalledWith("config.patch", expect.anything());
expect(state.dreamingStatusError).toContain("memory-lancedb");
expect(state.dreamingStatusError).toContain("does not support dreaming settings");
});
it("reads dreaming enabled state from the selected memory slot plugin", () => {
expect(
resolveConfiguredDreaming({

View File

@@ -309,11 +309,65 @@ async function writeDreamingPatch(
}
}
function lookupIncludesDreamingProperty(value: unknown): boolean {
const lookup = asRecord(value);
const children = Array.isArray(lookup?.children) ? lookup.children : [];
for (const child of children) {
const childRecord = asRecord(child);
if (normalizeTrimmedString(childRecord?.key) === "dreaming") {
return true;
}
}
return false;
}
function lookupDisallowsUnknownProperties(value: unknown): boolean {
const lookup = asRecord(value);
const schema = asRecord(lookup?.schema);
return schema?.additionalProperties === false;
}
async function ensureDreamingPathSupported(
state: DreamingState,
pluginId: string,
): Promise<boolean> {
if (!state.client || !state.connected) {
return true;
}
try {
const lookup = await state.client.request("config.schema.lookup", {
path: `plugins.entries.${pluginId}.config`,
});
if (lookupIncludesDreamingProperty(lookup)) {
return true;
}
if (lookupDisallowsUnknownProperties(lookup)) {
const message = `Selected memory plugin "${pluginId}" does not support dreaming settings.`;
state.dreamingStatusError = message;
state.lastError = message;
return false;
}
} catch {
return true;
}
return true;
}
export async function updateDreamingEnabled(
state: DreamingState,
enabled: boolean,
): Promise<boolean> {
if (state.dreamingModeSaving) {
return false;
}
if (!state.configSnapshot?.hash) {
state.dreamingStatusError = "Config hash missing; refresh and retry.";
return false;
}
const { pluginId } = resolveConfiguredDreaming(asRecord(state.configSnapshot?.config) ?? null);
if (!(await ensureDreamingPathSupported(state, pluginId))) {
return false;
}
const ok = await writeDreamingPatch(state, {
plugins: {
entries: {