Files
sencho/backend/src/__tests__/image-update-detect-cross-surface.test.ts
T
Anso 719180f156 fix(fleet): verify update status before removing readiness cards (#1697)
* fix(fleet): verify update status before removing readiness cards

Full-stack Apply now rechecks persisted status after the health gate starts, reloads the live preview before dropping a card, and invalidates the hub fleet aggregation so cleared updates cannot resurrect from a stale cache.

Closes #1686

* fix(fleet): align persisted update status with preview semver detection

Share digest-plus-tag detection so post-Apply sidebar status matches Fleet and Anatomy.

* fix(fleet): keep tag-only updates advisory for Compose automation

Expose digestUpdate vs tagUpdate from checkImage so scheduled and API auto-update only apply same-tag digest drift Compose can pull.

* docs: clarify scheduled auto-update applies digest drift only

Document that higher pinned tags stay advisory until Compose is changed, matching schedule and Run Now behavior.

* docs: require Compose pin edits for higher-tag advisories

Stop recommending Apply now or Update as remedies that cannot rewrite a pinned image tag.

* docs: clarify Apply now pulls pinned tags only

Align the detection-cadence bullet with digest-rebuild vs higher-tag guidance.

* fix(fleet): keep tag advisories after apply and scheduled updates

Tag-only previews were treated as cleared on Fleet reload, and scheduled/
Run Now paths wiped status without rechecking. Align post-update verification
with the manual Apply path (health gate first, recheck, no blind clear) and
block digest apply when sibling image checks failed.

* fix(fleet): clear eslint unused-arg and containers assignment
2026-07-26 03:09:21 -04:00

98 lines
3.7 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { detectImageUpdate } from '../services/imageUpdateDetect';
import { computeImagePreview } from '../services/UpdatePreviewService';
import type { DigestComparisonResult } from '../services/registry-api';
const PLATFORM = { os: 'linux', architecture: 'amd64' };
const LOCAL_DIGEST = `sha256:${'a'.repeat(64)}`;
const CREDENTIALS = { username: 'u', password: 'p' };
const IMAGE = 'nginx:1.2.3';
/**
* COR-1 regression: persisted sidebar status (via detectImageUpdate /
* checkImage) and Fleet/Anatomy preview (computeImagePreview) must agree when
* the declared-tag digest matches but a higher semantic tag exists.
*/
describe('cross-surface update detection (digest match + higher semver)', () => {
async function runDetectionAndPreview(
comparison: DigestComparisonResult,
tags: string[],
localDigests: string[] = [LOCAL_DIGEST],
) {
const compareDigest = vi.fn().mockResolvedValue(comparison);
const listRegistryTagsResult = vi.fn().mockResolvedValue({ ok: true, tags });
const detection = await detectImageUpdate({
localDigests,
platform: PLATFORM,
registry: 'registry-1.docker.io',
repo: 'library/nginx',
tag: '1.2.3',
credentials: CREDENTIALS,
deps: { compareDigest, listRegistryTagsResult },
});
const preview = await computeImagePreview('app', IMAGE, {
getCredentials: vi.fn().mockResolvedValue(CREDENTIALS),
getLocalDigest: vi.fn().mockResolvedValue({ digests: localDigests, platform: PLATFORM, emptyReason: null }),
compareDigest,
listRegistryTagsResult,
});
return { detection, preview, compareDigest };
}
it('shared detector and preview both report hasUpdate for app:1.2.3 when 1.2.4 exists', async () => {
const { detection, preview } = await runDetectionAndPreview(
{ kind: 'match' },
['1.2.3', '1.2.4'],
);
expect(detection.hasUpdate).toBe(true);
expect(detection.nextTag).toBe('1.2.4');
expect(detection.digestUpdate).toBe(false);
expect(preview.has_update).toBe(true);
expect(preview.next_tag).toBe('1.2.4');
expect(preview.has_update).toBe(detection.hasUpdate);
});
it('shared detector and preview both clear when digest matches and no higher tag exists', async () => {
const { detection, preview } = await runDetectionAndPreview({ kind: 'match' }, ['1.2.3']);
expect(detection.hasUpdate).toBe(false);
expect(preview.has_update).toBe(false);
expect(preview.has_update).toBe(detection.hasUpdate);
});
it('shared detector and preview both report hasUpdate when digest errors but 1.2.4 exists', async () => {
const { detection, preview } = await runDetectionAndPreview(
{ kind: 'error', reason: 'Registry unreachable' },
['1.2.3', '1.2.4'],
);
expect(detection.hasUpdate).toBe(true);
expect(detection.nextTag).toBe('1.2.4');
expect(detection.digestUpdate).toBe(false);
expect(detection.digestError).toBe('Registry unreachable');
expect(preview.has_update).toBe(true);
expect(preview.next_tag).toBe('1.2.4');
expect(preview.has_update).toBe(detection.hasUpdate);
});
it('shared detector and preview both report hasUpdate with no local digests when 1.2.4 exists', async () => {
const { detection, preview, compareDigest } = await runDetectionAndPreview(
{ kind: 'update' },
['1.2.3', '1.2.4'],
[],
);
expect(compareDigest).not.toHaveBeenCalled();
expect(detection.hasUpdate).toBe(true);
expect(detection.digestUpdate).toBe(false);
expect(detection.nextTag).toBe('1.2.4');
expect(preview.has_update).toBe(true);
expect(preview.next_tag).toBe('1.2.4');
expect(preview.has_update).toBe(detection.hasUpdate);
});
});