mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-30 21:45:13 +02:00
fix(control-pane): replace stale live-region guidance when a poll fails
The live region announced the advisory and steering state on every change, but a failed or invalid poll only set the visible status to offline. The live region kept the last known message, so during an outage a screen-reader user could still hear "No steering is required" from a poll that had already failed. That is worse than silence, because stale guidance reads as current. Route announcements through a single helper and have the failure path replace the guidance with a message saying the data is unavailable and the counts are unknown. The helper still suppresses a repeat of the same message, so a continuing outage stays quiet and recovery speaks again. Cover the success then failure and failure then recovery sequences at runtime. Reverting just the failure-path announcement fails the new assertion, and the unmutated view passes all of them. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
@@ -83,8 +83,8 @@ function renderControlPlaneViewHtml() {
|
||||
var canvas = document.getElementById('c');
|
||||
var ctx = canvas.getContext('2d');
|
||||
var view = { tasks: [], lanes: [], pairs: [], events: [], projection: { agents: [] }, thresholds: { ta: 0.35, ra: 0.7 } };
|
||||
// Last advisory/steering pair handed to the live region, so a poll that
|
||||
// changes nothing stays silent.
|
||||
// Last message handed to the live region, so a poll that changes nothing
|
||||
// stays silent.
|
||||
var lastSpoken = null;
|
||||
|
||||
function resize() {
|
||||
@@ -240,6 +240,15 @@ function renderControlPlaneViewHtml() {
|
||||
});
|
||||
}
|
||||
|
||||
// Polling runs every few seconds, so only speak when the advisory and
|
||||
// steering counts actually move. Repeating an unchanged summary would talk
|
||||
// over the operator without telling them anything new.
|
||||
function announce(message) {
|
||||
if (message === lastSpoken) return;
|
||||
lastSpoken = message;
|
||||
document.getElementById('announce').textContent = message;
|
||||
}
|
||||
|
||||
function apply(data) {
|
||||
if (!data || data.schemaVersion !== 'ecc.control-plane.view.v1' ||
|
||||
!['tasks', 'lanes', 'pairs', 'events'].every(function (key) { return Array.isArray(data[key]); }) ||
|
||||
@@ -259,17 +268,9 @@ function renderControlPlaneViewHtml() {
|
||||
(c.advisories || 0) + ' advisories (' + (c.resolutions || 0) + ' steering)' +
|
||||
(view.inventory && view.inventory.status !== 'ok' ? ' | inventory ' + view.inventory.status : '');
|
||||
|
||||
// Polling runs every few seconds, so only speak when the advisory and
|
||||
// steering counts actually move. Repeating an unchanged summary would talk
|
||||
// over the operator without telling them anything new.
|
||||
var speaker = document.getElementById('announce');
|
||||
var spoken = (c.advisories || 0) + '/' + (c.resolutions || 0);
|
||||
if (spoken !== lastSpoken) {
|
||||
lastSpoken = spoken;
|
||||
speaker.textContent = (c.advisories || 0) + ' advisories, ' +
|
||||
(c.resolutions || 0) + ' steering. ' +
|
||||
((c.resolutions || 0) > 0 ? 'Steering is required.' : 'No steering is required.');
|
||||
}
|
||||
announce((c.advisories || 0) + ' advisories, ' +
|
||||
(c.resolutions || 0) + ' steering. ' +
|
||||
((c.resolutions || 0) > 0 ? 'Steering is required.' : 'No steering is required.'));
|
||||
}
|
||||
|
||||
function poll() {
|
||||
@@ -278,6 +279,9 @@ function renderControlPlaneViewHtml() {
|
||||
return r.json();
|
||||
}).then(apply).catch(function () {
|
||||
document.getElementById('status').textContent = 'offline';
|
||||
// The last guidance is now stale, so replace it rather than leaving the
|
||||
// live region claiming the airspace is clear.
|
||||
announce('Control-plane data is unavailable. Advisories and steering are unknown.');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -206,6 +206,43 @@ let passed = 0;
|
||||
`a changed poll must announce the new counts, got ${changed.elements.get('announce').textContent}`);
|
||||
passed += 1;
|
||||
|
||||
// An outage must not leave the live region holding the last known guidance,
|
||||
// which would read as a current "airspace is clear" after data stopped.
|
||||
const outage = await render([
|
||||
{ ok: true, data: populatedView() },
|
||||
{ ok: false, data: { ok: false, error: 'snapshot unavailable' } }
|
||||
]);
|
||||
const beforeOutage = outage.elements.get('announce').textContent;
|
||||
await outage.pollAgain();
|
||||
assert.strictEqual(outage.elements.get('status').textContent, 'offline');
|
||||
assert.ok(outage.elements.get('announce').textContent !== beforeOutage,
|
||||
'a failed poll must not leave the previous guidance in the live region');
|
||||
assert.ok(/unavailable/i.test(outage.elements.get('announce').textContent)
|
||||
&& /unknown/i.test(outage.elements.get('announce').textContent),
|
||||
`an outage must say the counts are unknown, got ${outage.elements.get('announce').textContent}`);
|
||||
passed += 1;
|
||||
|
||||
// A repeated failure stays silent, but recovering online must speak again.
|
||||
const writesAfterOutage = outage.writesTo('announce');
|
||||
await outage.pollAgain();
|
||||
assert.strictEqual(outage.writesTo('announce'), writesAfterOutage,
|
||||
'a repeated failure must not re-announce the same outage');
|
||||
passed += 1;
|
||||
|
||||
const recovered = await render([
|
||||
{ ok: true, data: populatedView() },
|
||||
{ ok: false, data: { ok: false, error: 'snapshot unavailable' } },
|
||||
{ ok: true, data: populatedView({ counts: { tasks: 3, lanes: 1, agents: 2, advisories: 1, resolutions: 1 } }) }
|
||||
]);
|
||||
await recovered.pollAgain();
|
||||
await recovered.pollAgain();
|
||||
assert.ok(recovered.elements.get('status').textContent.includes('3 tasks'),
|
||||
'a recovered poll must restore the live status');
|
||||
assert.ok(recovered.elements.get('announce').textContent.includes('1 advisories')
|
||||
&& recovered.elements.get('announce').textContent.includes('Steering is required.'),
|
||||
`a recovered poll must announce the restored counts, got ${recovered.elements.get('announce').textContent}`);
|
||||
passed += 1;
|
||||
|
||||
console.log(`Results: Passed: ${passed}, Failed: 0`);
|
||||
})().catch(error => {
|
||||
console.error(error.message);
|
||||
|
||||
Reference in New Issue
Block a user