mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-24 08:57:25 +00:00
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:
@@ -32,22 +32,26 @@ function previewBody(hasUpdate: boolean, buildServices: string[] = []) {
|
||||
service: 'web',
|
||||
image: 'nginx:1.25',
|
||||
current_tag: '1.25',
|
||||
next_tag: hasUpdate ? '1.26' : null,
|
||||
next_tag: '1.25',
|
||||
has_update: hasUpdate,
|
||||
semver_bump: hasUpdate ? 'minor' : 'none',
|
||||
digest_update: hasUpdate,
|
||||
tag_update: false,
|
||||
semver_bump: hasUpdate ? 'patch' : 'none',
|
||||
check_status: 'ok',
|
||||
},
|
||||
],
|
||||
summary: {
|
||||
has_update: hasUpdate,
|
||||
primary_image: 'nginx',
|
||||
current_tag: '1.25',
|
||||
next_tag: '1.26',
|
||||
semver_bump: 'minor',
|
||||
update_kind: hasUpdate ? 'tag' : 'none',
|
||||
next_tag: '1.25',
|
||||
semver_bump: hasUpdate ? 'patch' : 'none',
|
||||
update_kind: hasUpdate ? 'digest' : 'none',
|
||||
blocked: false,
|
||||
blocked_reason: null,
|
||||
has_build_services: hasBuild,
|
||||
rebuild_available: hasBuild,
|
||||
check_status: 'ok',
|
||||
},
|
||||
changelog: null,
|
||||
};
|
||||
@@ -118,6 +122,33 @@ describe('StackAnatomyPanel edit affordance', () => {
|
||||
});
|
||||
|
||||
describe('StackAnatomyPanel update banner', () => {
|
||||
it('hides apply when only a newer tag is available', async () => {
|
||||
vi.mocked(apiFetch).mockImplementation(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url.includes('/update-preview')) {
|
||||
return jsonRes({
|
||||
build_services: [],
|
||||
images: [{
|
||||
service: 'web', image: 'nginx:1.25', current_tag: '1.25', next_tag: '1.26',
|
||||
has_update: true, digest_update: false, tag_update: true, semver_bump: 'minor', check_status: 'ok',
|
||||
}],
|
||||
summary: {
|
||||
has_update: true, primary_image: 'nginx', current_tag: '1.25', next_tag: '1.26',
|
||||
semver_bump: 'minor', update_kind: 'tag', blocked: false, blocked_reason: null,
|
||||
has_build_services: false, rebuild_available: false, check_status: 'ok',
|
||||
},
|
||||
changelog: null,
|
||||
});
|
||||
}
|
||||
if (url.includes('/scan-status')) return jsonRes({ status: 'ok' });
|
||||
return jsonRes(null, false);
|
||||
});
|
||||
render(panel(false));
|
||||
await waitFor(() => expect(screen.getByTestId('update-available-banner')).toBeInTheDocument());
|
||||
expect(screen.getByText((t) => typeof t === 'string' && t.includes('newer tag') && t.includes('edit Compose pin'))).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'apply' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the apply button and fires onApplyUpdate when clicked', async () => {
|
||||
const onApply = vi.fn();
|
||||
render(panel(false, onApply));
|
||||
@@ -135,9 +166,9 @@ describe('StackAnatomyPanel update banner', () => {
|
||||
return jsonRes({
|
||||
build_services: [],
|
||||
images: [
|
||||
{ service: 'web', image: 'nginx:1.25', current_tag: '1.25', next_tag: '1.26', has_update: true, semver_bump: 'minor' },
|
||||
{ service: 'cache', image: 'redis:7.2', current_tag: '7.2', next_tag: '7.4', has_update: true, semver_bump: 'minor' },
|
||||
{ service: 'db', image: 'postgres:16', current_tag: '16', next_tag: null, has_update: false, semver_bump: 'none' },
|
||||
{ service: 'web', image: 'nginx:1.25', current_tag: '1.25', next_tag: '1.25', has_update: true, digest_update: true, tag_update: false, semver_bump: 'patch', check_status: 'ok' },
|
||||
{ service: 'cache', image: 'redis:7.2', current_tag: '7.2', next_tag: '7.2', has_update: true, digest_update: true, tag_update: false, semver_bump: 'patch', check_status: 'ok' },
|
||||
{ service: 'db', image: 'postgres:16', current_tag: '16', next_tag: null, has_update: false, digest_update: false, tag_update: false, semver_bump: 'none', check_status: 'ok' },
|
||||
],
|
||||
summary: {
|
||||
has_update: true,
|
||||
|
||||
Reference in New Issue
Block a user