Skip to content

test(install-dynamic-plugins): cover getTarball's manifest guards and download dedup (RHIDP-16760) - #4554

Open
gustavolira wants to merge 2 commits into
redhat-developer:mainfrom
gustavolira:test/rhidp-16760-image-cache-tests
Open

test(install-dynamic-plugins): cover getTarball's manifest guards and download dedup (RHIDP-16760)#4554
gustavolira wants to merge 2 commits into
redhat-developer:mainfrom
gustavolira:test/rhidp-16760-image-cache-tests

Conversation

@gustavolira

@gustavolira gustavolira commented Sep 2, 2026

Copy link
Copy Markdown
Member

What

Adds seven tests for OciImageCache.getTarball and its private helper downloadAndLocateTarball, which had no coverage at all. Test-only; no production file is touched.

Closes RHIDP-16760. Follow-up to #4526 (RHIDP-16222), where this gap was found and deliberately left out.

Why this was not in #4526

#4526 added image-cache.test.ts for getPluginPaths and getDigest only. Three independent review passes on that PR flagged the same remaining hole, and it was left out on purpose: it is pre-existing code that PR neither touched nor broke, and folding it in would have turned a regression-test PR into a coverage sweep.

Measured on main before this PR, image-cache.ts sits at 85.24% statements / 66.66% branches with lines 109-123 uncovered — the whole of downloadAndLocateTarball.

What is covered

The two readable-failure guards. These matter for the same reason RHDHBUGS-2439 did: they are what an operator reads out of the init container log. Drop either one and the failure becomes path.join(localDir, undefined) instead of a message naming the image.

  • a manifest with "layers": [] and one with no layers key at all, as one it.each table — measured, the two shapes cover no distinct branch, so they are equivalent inputs to one assertion rather than two tests
  • a layer digest with no algorithm separator (nocolon), asserting both the InstallException type and the message naming the offending digest

The promise cache, which is the class docblock's stated reason to exist — several plugins in one overlay image sharing a single skopeo copy — and which had no test.

  • three concurrent getTarball calls for one image fork skopeo once, and all three resolve to the same path
  • two calls for different images fork twice

Both directions, because a dedup test on its own would pass against a cache that collapsed every image onto one download.

The failure eviction (pending.catch(() => this.tarballs.delete(resolved))), which is what stops one transient registry error from being replayed to every later caller for the life of the process. The fake fails its first invocation and succeeds afterwards; the test asserts the second call retries and succeeds.

The success path — the returned path is the layer blob inside the cache directory, and the skopeo argv is asserted verbatim.

Seam

No network, no registry, no jest.mock — there is not one in this directory. The fake skopeo is a shell binary that materialises manifest.json plus the layer blob at the dir: destination, exactly as extra-catalog-index.test.ts does, with the invocation log from skopeo.test.ts so the dedup tests can count forks.

Mutation results

Each test was made to fail before being kept: break the behaviour it covers, run the full suite, revert.

Mutation Failures Which tests
no-layers guard returns a path instead of throwing 2 both rows of the no-layers table
malformed-digest guard returns a path instead of throwing 1 names the offending digest ...
layer path returned unjoined (return localDir) 2 the success test and the retry test
promise cache removed from getTarball 1 shares one skopeo copy between concurrent callers
pending.catch eviction removed 1 evicts a failed download ...

No test survived its own mutation.

Checks

  • yarn tsc — clean
  • yarn prettier:check — clean
  • yarn lint:all — clean
  • CI=true yarn test — 19 suites, 265 tests, all passing

No changeset: test-only, no published behaviour change.

Still not covered

image-cache.ts line 94, if (entry && typeof entry === 'object') in getPluginPaths — the falsy-entry path, which a malformed annotation like [null, "x", {"plugin":{}}] would exercise. It leaves the file at 94.44% branches. Left alone deliberately: it is pre-existing code in a different method from this PR's subject, and the same blast-radius rule that kept getTarball out of #4526 keeps this out of here.

run.ts's child.on('error') path — the variant where the binary exists but cannot be executed. Separate gap, not filed; Skopeo's constructor already throws for a skopeo that is missing from PATH, so the common field case is handled elsewhere.


🤖 Generated with Claude Code

… dedup (RHIDP-16760)

The deep review of RHIDP-16222 left image-cache.ts covered for getPluginPaths and
getDigest but not for getTarball: lines 109-123, the whole of
downloadAndLocateTarball, were uncovered, and nothing asserted the promise cache
the class docblock is written around.

The two InstallException guards matter for the same reason RHDHBUGS-2439 did.
They are readable-failure paths an operator reads out of the init container log,
and dropping either turns the failure into path.join(localDir, undefined) rather
than a message naming the image.

The cache is the class's stated reason to exist — several plugins in one overlay
image share a single skopeo copy — and it had no test at all. Nor did the
failure eviction, which is what stops one transient registry error from being
replayed to every later caller for the life of the process.

Faked at the seam the siblings already use: a fake skopeo shell binary that
materialises manifest.json plus the layer blob at the dir: destination
(extra-catalog-index.test.ts), with the invocation log that lets the dedup tests
count forks (skopeo.test.ts). No network, no registry, no jest.mock.

Both directions of the cache are covered, because a dedup test on its own would
pass against a cache that collapsed every image onto one download: three
concurrent calls for one image fork skopeo once, two calls for different images
fork twice.

Mutation-checked, each mutant reverted after its run:

  no-layers guard returns a path instead of throwing
    -> 2 failures, both no-layers tests
  malformed-digest guard returns a path instead of throwing
    -> 1 failure
  layer path returned unjoined
    -> 2 failures
  promise cache removed from getTarball
    -> 1 failure, the concurrent-callers test
  pending.catch eviction removed
    -> 1 failure, the retry test

19 suites / 265 tests. tsc, prettier and lint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gustavolira
gustavolira requested review from a team as code owners September 2, 2026 22:18
@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 62.31%. Comparing base (a6fe7ec) to head (0fecf56).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4554   +/-   ##
=======================================
  Coverage   62.31%   62.31%           
=======================================
  Files        2606     2606           
  Lines      104644   104644           
  Branches    29391    29391           
=======================================
+ Hits        65204    65213    +9     
+ Misses      37623    37614    -9     
  Partials     1817     1817           
Flag Coverage Δ *Carryforward flag
adoption-insights 84.30% <ø> (ø) Carriedforward from 36b1cb6
ai-integrations 76.15% <ø> (ø) Carriedforward from 36b1cb6
app-defaults 56.22% <ø> (ø) Carriedforward from 36b1cb6
augment 46.67% <ø> (ø) Carriedforward from 36b1cb6
boost 80.51% <ø> (ø) Carriedforward from 36b1cb6
bulk-import 73.12% <ø> (ø) Carriedforward from 36b1cb6
cost-management 13.55% <ø> (ø) Carriedforward from 36b1cb6
dcm 73.47% <ø> (ø) Carriedforward from 36b1cb6
e2e-adoption-insights 60.00% <ø> (ø) Carriedforward from 36b1cb6
e2e-extensions 62.32% <ø> (ø) Carriedforward from 36b1cb6
e2e-global-header 50.35% <ø> (ø) Carriedforward from 36b1cb6
e2e-homepage 61.11% <ø> (ø) Carriedforward from 36b1cb6
e2e-intelligent-assistant 47.04% <ø> (ø) Carriedforward from 36b1cb6
e2e-orchestrator 49.52% <ø> (ø) Carriedforward from 36b1cb6
e2e-orchestrator-plugin 49.51% <ø> (ø) Carriedforward from 36b1cb6
e2e-quickstart 55.21% <ø> (ø) Carriedforward from 36b1cb6
e2e-scorecard 50.21% <ø> (ø) Carriedforward from 36b1cb6
e2e-theme 16.36% <ø> (ø) Carriedforward from 36b1cb6
extensions 56.66% <ø> (ø) Carriedforward from 36b1cb6
global-floating-action-button 71.18% <ø> (ø) Carriedforward from 36b1cb6
global-header 68.09% <ø> (ø) Carriedforward from 36b1cb6
homepage 48.48% <ø> (ø) Carriedforward from 36b1cb6
install-dynamic-plugins 71.98% <ø> (+0.67%) ⬆️
intelligent-assistant 76.40% <ø> (ø) Carriedforward from 36b1cb6
konflux 91.98% <ø> (ø) Carriedforward from 36b1cb6
lightspeed 69.02% <ø> (ø) Carriedforward from 36b1cb6
mcp-integrations 84.14% <ø> (ø) Carriedforward from 36b1cb6
orchestrator 71.13% <ø> (ø) Carriedforward from 36b1cb6
quickstart 63.74% <ø> (ø) Carriedforward from 36b1cb6
sandbox 79.56% <ø> (ø) Carriedforward from 36b1cb6
scorecard 87.90% <ø> (ø) Carriedforward from 36b1cb6
theme 87.91% <ø> (ø) Carriedforward from 36b1cb6
translations 5.12% <ø> (ø) Carriedforward from 36b1cb6
x2a 77.10% <ø> (ø) Carriedforward from 36b1cb6

*This pull request uses carry forward flags. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a6fe7ec...0fecf56. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…table (RHIDP-16760)

Review of the previous commit. It shipped two separate tests for the same
error — a manifest with `"layers": []` and one with no `layers` key — and the
mutation run should have been the tell: both died together, neither
independently.

Measured with --no-cache: dropping the second changes no covered statement and
no covered branch path. Both shapes reach the same guard through the same
`manifest.layers?.[0]?.digest` optional chain, so as two `it` blocks the second
was documentation dressed as coverage. That is the exact criticism levelled at
the W10= case during the review of redhat-developer#4526, and it applies here too.

Folded into one `it.each` table, which is what they are: equivalent inputs to
one assertion. Same form oci-key.test.ts uses for its invalidCases list. The
comment records that the equivalence was measured rather than assumed, so nobody
re-splits them later on the theory that they cover different branches.

No coverage change, by construction. 19 suites / 265 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant