Files
dockhand-upstream/tests/index-child-digest.test.ts

65 lines
3.0 KiB
TypeScript

import { describe, test, expect } from 'bun:test';
import { indexChildDigests, localDigestIsIndexChild } from '../src/lib/server/scheduler/tasks/update-utils';
// Real shape from ghcr.io/mastodon/mastodon:v4.6.5 (the #1367 repro): an OCI index
// whose per-arch child digests differ from the index digest a HEAD returns.
const MASTODON_INDEX = {
mediaType: 'application/vnd.oci.image.index.v1+json',
manifests: [
{ platform: { architecture: 'arm64', os: 'linux' }, digest: 'sha256:2cd8bcf2e903dab42a759956008e17da16a1268e842e738c2c640f6f7b54bae8' },
{ platform: { architecture: 'amd64', os: 'linux' }, digest: 'sha256:696439e1ada71d0cf3d51d4d6a4744d6e40b57aafa64980b18f4d3b78230d0cf' }
]
};
const INDEX_DIGEST = 'sha256:77f11d1a6c674664217372d94ccdb9203524c60447827fe74ab6e11466825815';
const AMD64_CHILD = 'sha256:696439e1ada71d0cf3d51d4d6a4744d6e40b57aafa64980b18f4d3b78230d0cf';
describe('indexChildDigests', () => {
test('extracts every per-arch child digest from an image index', () => {
expect(indexChildDigests(MASTODON_INDEX)).toEqual([
'sha256:2cd8bcf2e903dab42a759956008e17da16a1268e842e738c2c640f6f7b54bae8',
AMD64_CHILD
]);
});
test('returns [] for a single-arch manifest (no manifests array)', () => {
expect(indexChildDigests({ mediaType: 'application/vnd.oci.image.manifest.v1+json', layers: [] })).toEqual([]);
});
test('returns [] for null / garbage / error bodies without throwing', () => {
expect(indexChildDigests(null)).toEqual([]);
expect(indexChildDigests(undefined)).toEqual([]);
expect(indexChildDigests('not json')).toEqual([]);
expect(indexChildDigests({ errors: [{ code: 'MANIFEST_UNKNOWN' }] })).toEqual([]);
});
test('skips malformed manifest entries (missing/empty/non-string digest)', () => {
const body = { manifests: [{ digest: AMD64_CHILD }, { digest: '' }, { digest: 42 }, {}, null] };
expect(indexChildDigests(body)).toEqual([AMD64_CHILD]);
});
});
describe('localDigestIsIndexChild', () => {
test('true when local per-arch digest is a child of the index (the #1367 fix)', () => {
// local image recorded its amd64 per-arch digest, HEAD returned the index digest
expect(localDigestIsIndexChild([AMD64_CHILD], MASTODON_INDEX)).toBe(true);
});
test('false when the local digest is the index digest itself (already handled upstream)', () => {
// upstream matches this against the HEAD digest first; here it is NOT a child
expect(localDigestIsIndexChild([INDEX_DIGEST], MASTODON_INDEX)).toBe(false);
});
test('false when local digest is genuinely unrelated (a real update)', () => {
expect(localDigestIsIndexChild(['sha256:deadbeef'], MASTODON_INDEX)).toBe(false);
});
test('false for a non-index body (single-arch / error) - degrades to "keep update"', () => {
expect(localDigestIsIndexChild([AMD64_CHILD], { layers: [] })).toBe(false);
expect(localDigestIsIndexChild([AMD64_CHILD], null)).toBe(false);
});
test('matches if ANY of several local digests is a child', () => {
expect(localDigestIsIndexChild(['sha256:unrelated', AMD64_CHILD], MASTODON_INDEX)).toBe(true);
});
});