fix: reconcile sticky update indicators with Anatomy preview (#1698)

* fix: reconcile sticky update indicators with Anatomy preview

Sidebar, Updates filter, and Fleet treated retained partial/failed
scanner has_update as confirmed. Keep raw state for retention/notifications,
project confirmed-only to APIs, show distinct incomplete indicators, and
clear sticky rows only after an authoritative-negative preview.

Closes #1685

* test: align sidebar truncate E2E with failed-over-retained precedence

Purple update indicators are confirmed-only; hasUpdate with a failed
check correctly shows the failed trailing icon.

* fix: clear confirmed update rows on authoritative-negative preview

Address audit SF-1/SF-2/SF-3: observation-watermark clears for older
ok+has_update rows (DB + memory gens), Fleet checkability parity with
backend not_checkable, and Updates chip confirmed-only regressions.

* fix: tombstone equal-generation writers on preview clear

Advance the per-stack write generation when clearing at the observation
watermark so a scanner reserved before preview cannot recreate the row
after an authoritative-negative reconcile.

* fix: clear sticky updates with digest and tag preview parity

Share detection across scanner and preview, keep GET read-only with POST reconcile, gate Apply to digest and rebuild updates, and invalidate the hub fleet cache on clear.

* test: set digestUpdate on auto-update checkImage mocks

Scheduler and execute routes now gate Compose on digest drift; fixtures that expect an apply need digestUpdate so they exercise the update path.

* fix: clear unused lint errors on sticky update branch

Drop unused partial helper and fleet invalidate import; keep the CacheService inflight self-ref as let with an eslint exception so tsc stays green.

* fix: use inflight holder for CacheService prefer-const

Keep generation-aware ownership without a let self-reference that fights ESLint and tsc.
This commit is contained in:
Anso
2026-07-25 15:42:19 -04:00
committed by GitHub
parent 8b5407fcff
commit 0daddfde00
43 changed files with 2529 additions and 390 deletions
@@ -46,10 +46,40 @@ describe('stack_update_status tri-state accessors', () => {
expect(db().getStackUpdateDetail(NODE).web.checkStatus).toBe('ok');
});
it('keeps getStackUpdateStatus a boolean map for the fleet contract', () => {
it('keeps getStackUpdateStatus a raw boolean map ignoring check_status', () => {
db().upsertStackUpdateStatus(NODE, 'web', true, 1000, 'ok', null);
db().upsertStackUpdateStatus(NODE, 'api', false, 1000, 'failed', 'boom');
expect(db().getStackUpdateStatus(NODE)).toEqual({ web: true, api: false });
db().upsertStackUpdateStatus(NODE, 'sticky', true, 1000, 'partial', 'half');
expect(db().getStackUpdateStatus(NODE)).toEqual({ web: true, api: false, sticky: true });
});
it('projects confirmed updates only via getConfirmedStackUpdateStatus', () => {
db().upsertStackUpdateStatus(NODE, 'web', true, 1000, 'ok', null);
db().upsertStackUpdateStatus(NODE, 'sticky', true, 1000, 'partial', 'half');
db().upsertStackUpdateStatus(NODE, 'failed', true, 1000, 'failed', 'boom');
db().upsertStackUpdateStatus(NODE, 'clean', false, 1000, 'ok', null);
expect(db().getConfirmedStackUpdateStatus(NODE)).toEqual({
web: true,
sticky: false,
failed: false,
clean: false,
});
});
it('clearStackUpdateStatus returns deleted row count and removes services_json', () => {
db().upsertStackUpdateStatus(NODE, 'web', true, 1000, 'partial', 'half', [
{ service: 'web', image: 'web:1', hasUpdate: true, checkStatus: 'ok', lastError: null },
]);
expect(db().clearStackUpdateStatus(NODE, 'web')).toBe(1);
expect(db().getStackUpdateDetail(NODE).web).toBeUndefined();
expect(db().clearStackUpdateStatus(NODE, 'web')).toBe(0);
});
it('getNodeUpdateSummary counts only confirmed updates', () => {
db().upsertStackUpdateStatus(NODE, 'web', true, 1000, 'ok', null);
db().upsertStackUpdateStatus(NODE, 'sticky', true, 1000, 'partial', 'half');
const summary = db().getNodeUpdateSummary().find((r) => r.node_id === NODE);
expect(summary?.stacks_with_updates).toBe(1);
});
it('recordStackCheckFailure preserves a prior has_update while marking failed', () => {