feat: acknowledge Compose Doctor preflight findings per stack (#1560)

* feat: acknowledge Compose Doctor preflight findings per stack

Add node-scoped preflight acknowledgements with read-time filtering.

Supports four expiry modes and activeStatus for banner, tab dot, and readiness.

* fix: align preflight acknowledge UI with design system

Use Combobox, modal chrome, mono fields, and non-destructive clear confirm.

* fix: update test mocks to match new preflight field names

The preflight-acknowledgements feature renamed status-\>activeStatus and
highestSeverity-\>activeHighestSeverity in the preflight report shape. The
corresponding test mocks in three files still used the old field names,
causing 6 test failures across backend and frontend.

- backend: update-guard-service mock now passes activeStatus
- frontend PreflightPanel: Report interface and report() helper now include
  activeStatus, activeHighestSeverity, activeCount, acknowledgedCount
- frontend StackAnatomyPanel doctor: mock API response now includes
  activeHighestSeverity and activeStatus
This commit is contained in:
Anso
2026-07-05 04:12:03 -04:00
committed by GitHub
parent 122c1b8073
commit 4077546492
16 changed files with 1002 additions and 54 deletions
@@ -39,17 +39,17 @@ const summary = (over: Partial<UpdatePreviewSummary> = {}): UpdatePreviewSummary
describe('preflightSignal', () => {
const cases = [
{ status: 'never-run', expected: 'unknown', affects: false },
{ status: 'blocker', expected: 'blocked', affects: true },
{ status: 'unrenderable', expected: 'attention', affects: true },
{ status: 'high', expected: 'attention', affects: true },
{ status: 'warning', expected: 'warning', affects: true },
{ status: 'pass', expected: 'ok', affects: true },
{ status: 'info', expected: 'ok', affects: true },
{ activeStatus: 'never-run', expected: 'unknown', affects: false },
{ activeStatus: 'blocker', expected: 'blocked', affects: true },
{ activeStatus: 'unrenderable', expected: 'attention', affects: true },
{ activeStatus: 'high', expected: 'attention', affects: true },
{ activeStatus: 'warning', expected: 'warning', affects: true },
{ activeStatus: 'pass', expected: 'ok', affects: true },
{ activeStatus: 'info', expected: 'ok', affects: true },
] as const;
it.each(cases)('maps preflight status $status to $expected', ({ status, expected, affects }) => {
const signal = preflightSignal({ status });
it.each(cases)('maps preflight activeStatus $activeStatus to $expected', ({ activeStatus, expected, affects }) => {
const signal = preflightSignal({ activeStatus });
expect(signal.status).toBe(expected);
expect(signal.affectsVerdict).toBe(affects);
});
@@ -182,10 +182,10 @@ describe('aggregateVerdict', () => {
});
it('reaches every verdict from realistic signal sets', () => {
expect(aggregateVerdict([preflightSignal({ status: 'blocker' }), driftSignal(0)])).toBe('blocked');
expect(aggregateVerdict([preflightSignal({ status: 'high' }), driftSignal(0)])).toBe('review_required');
expect(aggregateVerdict([preflightSignal({ activeStatus: 'blocker' }), driftSignal(0)])).toBe('blocked');
expect(aggregateVerdict([preflightSignal({ activeStatus: 'high' }), driftSignal(0)])).toBe('review_required');
expect(aggregateVerdict([containersSignal('error'), driftSignal(0)])).toBe('unknown');
expect(aggregateVerdict([driftSignal(1), preflightSignal({ status: 'pass' })])).toBe('ready_with_warnings');
expect(aggregateVerdict([driftSignal(0), preflightSignal({ status: 'pass' }), healthchecksSignal([probe()])])).toBe('ready');
expect(aggregateVerdict([driftSignal(1), preflightSignal({ activeStatus: 'pass' })])).toBe('ready_with_warnings');
expect(aggregateVerdict([driftSignal(0), preflightSignal({ activeStatus: 'pass' }), healthchecksSignal([probe()])])).toBe('ready');
});
});