fix(dashboard): verify assistant-health saves with read-back
This commit is contained in:
@@ -5683,6 +5683,17 @@
|
|||||||
"docs/plans/state.json"
|
"docs/plans/state.json"
|
||||||
],
|
],
|
||||||
"test_status": "pnpm typecheck passing"
|
"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": {
|
"overall_progress": {
|
||||||
|
|||||||
@@ -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') {
|
function setAssistantSaveState(message, tone = 'neutral') {
|
||||||
_assistantSaveState = {
|
_assistantSaveState = {
|
||||||
message,
|
message,
|
||||||
@@ -550,8 +574,28 @@ async function applyAssistantPatch(patches, statusEl) {
|
|||||||
message = `Runtime-only save (${applied.length} updated, file persistence unavailable)`;
|
message = `Runtime-only save (${applied.length} updated, file persistence unavailable)`;
|
||||||
tone = 'warning';
|
tone = 'warning';
|
||||||
} else {
|
} else {
|
||||||
message = `Saved to runtime + config file (${applied.length} updated)`;
|
// Verify read-after-write so UI cannot claim persistence when value did not stick.
|
||||||
tone = 'success';
|
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);
|
setAssistantSaveState(message, tone);
|
||||||
statusEl.textContent = message;
|
statusEl.textContent = message;
|
||||||
|
|||||||
Reference in New Issue
Block a user