diff --git a/docs/plans/state.json b/docs/plans/state.json index 91c9b10..1c72264 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -5683,6 +5683,17 @@ "docs/plans/state.json" ], "test_status": "pnpm typecheck passing" + }, + "dashboard-assistant-health-readback-verification": { + "status": "completed", + "date": "2026-02-19", + "updated": "2026-02-19", + "summary": "Added read-after-write verification to Assistant Health saves. After `config.patch`, dashboard now performs `config.get` and confirms each patched key/value actually stuck; mismatches are surfaced as explicit errors instead of false green success.", + "files_modified": [ + "src/gateway/ui/pages/dashboard.js", + "docs/plans/state.json" + ], + "test_status": "pnpm typecheck passing" } }, "overall_progress": { diff --git a/src/gateway/ui/pages/dashboard.js b/src/gateway/ui/pages/dashboard.js index 905510d..ac73cbf 100644 --- a/src/gateway/ui/pages/dashboard.js +++ b/src/gateway/ui/pages/dashboard.js @@ -123,6 +123,30 @@ function buildRollbackPatchesFromSnapshot(snapshot) { }; } +function getByPath(obj, dottedPath) { + if (!obj || typeof obj !== 'object') {return undefined;} + const parts = dottedPath.split('.'); + let cursor = obj; + for (const part of parts) { + if (!cursor || typeof cursor !== 'object' || !(part in cursor)) { + return undefined; + } + cursor = cursor[part]; + } + return cursor; +} + +function valuesMatch(expected, actual) { + if (Array.isArray(expected) && Array.isArray(actual)) { + if (expected.length !== actual.length) {return false;} + for (let i = 0; i < expected.length; i++) { + if (expected[i] !== actual[i]) {return false;} + } + return true; + } + return expected === actual; +} + function setAssistantSaveState(message, tone = 'neutral') { _assistantSaveState = { message, @@ -550,8 +574,28 @@ async function applyAssistantPatch(patches, statusEl) { message = `Runtime-only save (${applied.length} updated, file persistence unavailable)`; tone = 'warning'; } else { - message = `Saved to runtime + config file (${applied.length} updated)`; - tone = 'success'; + // Verify read-after-write so UI cannot claim persistence when value did not stick. + try { + const fresh = await _dashboardClient.call('config.get'); + const mismatches = []; + for (const [key, value] of Object.entries(patches)) { + const actual = getByPath(fresh, key); + if (!valuesMatch(value, actual)) { + mismatches.push(`${key} expected=${JSON.stringify(value)} actual=${JSON.stringify(actual)}`); + } + } + + if (mismatches.length > 0) { + message = `Saved response received but read-back mismatch: ${mismatches.join('; ')}`; + tone = 'error'; + } else { + message = `Saved to runtime + config file (${applied.length} updated)`; + tone = 'success'; + } + } catch (verifyError) { + message = `Saved response received, but verification failed: ${verifyError instanceof Error ? verifyError.message : String(verifyError)}`; + tone = 'warning'; + } } setAssistantSaveState(message, tone); statusEl.textContent = message;