mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-03 22:25:30 +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:
@@ -37,6 +37,7 @@ vi.mock('@/context/NodeContext', () => ({
|
||||
import { apiFetch, fetchForNode } from '@/lib/api';
|
||||
import { requestServiceUpdate } from '@/lib/serviceUpdate';
|
||||
import AutoUpdateReadinessView, { MobileReadinessCard, CadenceStrip, type StackCard } from '../AutoUpdateReadinessView';
|
||||
import { isAuthoritativeNegativePreview } from '@/types/imageUpdates';
|
||||
|
||||
function card(over: Partial<StackCard> = {}): StackCard {
|
||||
return {
|
||||
@@ -49,16 +50,20 @@ function card(over: Partial<StackCard> = {}): StackCard {
|
||||
scheduledTask: null,
|
||||
preview: {
|
||||
stack_name: 'nextcloud',
|
||||
images: [],
|
||||
images: [{
|
||||
service: 'app', image: 'nextcloud:27.1.4', current_tag: '27.1.4', next_tag: '27.1.4',
|
||||
has_update: true, digest_update: true, tag_update: false, semver_bump: 'patch', check_status: 'ok',
|
||||
}],
|
||||
summary: {
|
||||
has_update: true,
|
||||
primary_image: 'nextcloud',
|
||||
current_tag: '27.1.4',
|
||||
next_tag: '27.1.5',
|
||||
next_tag: '27.1.4',
|
||||
semver_bump: 'patch',
|
||||
update_kind: 'tag',
|
||||
update_kind: 'digest',
|
||||
blocked: false,
|
||||
blocked_reason: null,
|
||||
check_status: 'ok',
|
||||
},
|
||||
rollback_target: null,
|
||||
changelog: 'Fixes. Security patch.',
|
||||
@@ -74,6 +79,39 @@ it('enables Apply for a safe, non-blocked update', () => {
|
||||
expect(apply()).toBeEnabled();
|
||||
});
|
||||
|
||||
it('disables Apply for tag-only advisory updates', () => {
|
||||
render(
|
||||
<MobileReadinessCard
|
||||
card={card({
|
||||
preview: {
|
||||
stack_name: 'nextcloud',
|
||||
images: [{
|
||||
service: 'app', image: 'nextcloud:27.1.4', current_tag: '27.1.4', next_tag: '27.1.5',
|
||||
has_update: true, digest_update: false, tag_update: true, semver_bump: 'patch', check_status: 'ok',
|
||||
}],
|
||||
summary: {
|
||||
has_update: true,
|
||||
primary_image: 'nextcloud',
|
||||
current_tag: '27.1.4',
|
||||
next_tag: '27.1.5',
|
||||
semver_bump: 'patch',
|
||||
update_kind: 'tag',
|
||||
blocked: false,
|
||||
blocked_reason: null,
|
||||
check_status: 'ok',
|
||||
},
|
||||
rollback_target: null,
|
||||
changelog: 'Fixes.',
|
||||
},
|
||||
})}
|
||||
onApply={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText(/Newer tag/i)).toBeInTheDocument();
|
||||
expect(apply()).toBeDisabled();
|
||||
});
|
||||
|
||||
|
||||
it('disables Apply when the update is blocked (major bump)', () => {
|
||||
render(
|
||||
<MobileReadinessCard
|
||||
@@ -117,9 +155,12 @@ it('offers per-service Apply when build-only companions make the stack multi-ser
|
||||
service: 'app',
|
||||
image: 'nextcloud:27',
|
||||
current_tag: '27.1.4',
|
||||
next_tag: '27.1.5',
|
||||
next_tag: '27.1.4',
|
||||
has_update: true,
|
||||
digest_update: true,
|
||||
tag_update: false,
|
||||
semver_bump: 'patch',
|
||||
check_status: 'ok',
|
||||
}],
|
||||
build_services: ['cron'],
|
||||
summary: {
|
||||
@@ -128,10 +169,11 @@ it('offers per-service Apply when build-only companions make the stack multi-ser
|
||||
current_tag: '27.1.4',
|
||||
next_tag: '27.1.5',
|
||||
semver_bump: 'patch',
|
||||
update_kind: 'tag',
|
||||
update_kind: 'digest',
|
||||
blocked: false,
|
||||
blocked_reason: null,
|
||||
has_build_services: true,
|
||||
check_status: 'ok',
|
||||
},
|
||||
rollback_target: null,
|
||||
changelog: 'Fixes.',
|
||||
@@ -200,9 +242,12 @@ describe('AutoUpdateReadinessView desktop Apply now', () => {
|
||||
service: 'app',
|
||||
image: 'nextcloud:27',
|
||||
current_tag: '27.1.4',
|
||||
next_tag: '27.1.5',
|
||||
next_tag: '27.1.4',
|
||||
has_update: true,
|
||||
digest_update: true,
|
||||
tag_update: false,
|
||||
semver_bump: 'patch' as const,
|
||||
check_status: 'ok' as const,
|
||||
},
|
||||
{
|
||||
service: 'redis',
|
||||
@@ -210,18 +255,22 @@ describe('AutoUpdateReadinessView desktop Apply now', () => {
|
||||
current_tag: '7.2',
|
||||
next_tag: '7.2',
|
||||
has_update: false,
|
||||
digest_update: false,
|
||||
tag_update: false,
|
||||
semver_bump: 'none' as const,
|
||||
check_status: 'ok' as const,
|
||||
},
|
||||
],
|
||||
summary: {
|
||||
has_update: true,
|
||||
primary_image: 'nextcloud',
|
||||
current_tag: '27.1.4',
|
||||
next_tag: '27.1.5',
|
||||
next_tag: '27.1.4',
|
||||
semver_bump: 'patch' as const,
|
||||
update_kind: 'tag' as const,
|
||||
update_kind: 'digest' as const,
|
||||
blocked: false,
|
||||
blocked_reason: null,
|
||||
check_status: 'ok' as const,
|
||||
},
|
||||
rollback_target: null,
|
||||
changelog: 'Fixes.',
|
||||
@@ -229,7 +278,7 @@ describe('AutoUpdateReadinessView desktop Apply now', () => {
|
||||
const refreshedPreview = {
|
||||
...multiPreview,
|
||||
images: multiPreview.images.map((img) => (
|
||||
img.service === 'app' ? { ...img, has_update: false, current_tag: '27.1.5', next_tag: '27.1.5' } : img
|
||||
img.service === 'app' ? { ...img, has_update: false, digest_update: false, current_tag: '27.1.4', next_tag: '27.1.4' } : img
|
||||
)),
|
||||
summary: { ...multiPreview.summary, has_update: false, current_tag: '27.1.5' },
|
||||
};
|
||||
@@ -455,3 +504,40 @@ describe('AutoUpdateReadinessView cadence fetch race', () => {
|
||||
expect(screen.getByText(/Recheck available in/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAuthoritativeNegativePreview (Fleet card drop parity)', () => {
|
||||
it('drops when a checkable image has ok + no update', () => {
|
||||
expect(isAuthoritativeNegativePreview({
|
||||
images: [{ check_status: 'ok' }],
|
||||
summary: { has_update: false, check_status: 'ok' },
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it('retains not_checkable-only negative previews', () => {
|
||||
expect(isAuthoritativeNegativePreview({
|
||||
images: [{ check_status: 'not_checkable' }],
|
||||
summary: { has_update: false, check_status: 'ok' },
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
it('clears when every image is ok even if summary check_status is omitted', () => {
|
||||
expect(isAuthoritativeNegativePreview({
|
||||
images: [{ check_status: 'ok' }],
|
||||
summary: { has_update: false },
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it('retains when image check_status is missing', () => {
|
||||
expect(isAuthoritativeNegativePreview({
|
||||
images: [{}],
|
||||
summary: { has_update: false, check_status: 'ok' },
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
it('retains empty image lists even with ok summary', () => {
|
||||
expect(isAuthoritativeNegativePreview({
|
||||
images: [],
|
||||
summary: { has_update: false, check_status: 'ok' },
|
||||
})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user