diff --git a/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-oci.test.ts b/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-oci.test.ts index 2e4c8d807d..0b57c9e320 100644 --- a/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-oci.test.ts +++ b/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-oci.test.ts @@ -248,6 +248,32 @@ describe('installOciPlugin — floating tags (RHDHBUGS-1077)', () => { expect(result.pluginPath).toBeNull(); expect(calls.getDigest).toEqual([]); }); + + it('re-installs rather than skipping when the configured pullPolicy is not a recognised value', async () => { + // `dynamic-plugins.yaml` is read with `parseYaml(...) as DynamicPluginsConfig` + // (installer.ts) — a type assertion, not a runtime check — so a typo like + // `pullPolicy: Never` reaches here as an unrecognised string. It must fall + // through to a re-install; silently skipping would strand the plugin at + // whatever version happened to be on disk. + const installed = await seedInstalled('digest-aaaa'); + const tarball = await makeLayerTarball(PLUGIN_PATH, '{"name":"my-plugin"}'); + const { cache, calls } = recordingImageCache({ + digest: 'digest-aaaa', + tarball, + }); + + const result = await installOciPlugin( + ociPlugin({ pullPolicy: 'Never' as PullPolicy }), + destination, + cache, + installed, + ); + + expect(result.pluginPath).toBe(PLUGIN_PATH); + expect(calls.getTarball).toHaveLength(1); + // Never consults the registry for a digest it would not know how to act on. + expect(calls.getDigest).toEqual(['oci://registry.io/org/plugin:latest']); + }); }); describe('installOciPlugin — package spec handling', () => { diff --git a/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-oci.ts b/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-oci.ts index 5ba49c2d4e..87ab418d0e 100644 --- a/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-oci.ts +++ b/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/installer-oci.ts @@ -42,6 +42,11 @@ function splitOciPackage( if (bang === -1) return null; const imagePart = pkg.slice(0, bang); const pluginPath = pkg.slice(bang + 1); + /* istanbul ignore next -- unreachable on the install path: OCI_REGEX rejects + a leading or trailing `!` (oci-key.test.ts `invalidCases`), and a package + with no `!` at all has one appended by the merger before it gets here + (merger.ts, `!plugin.package.includes('!')` and resolveInherit). Kept so + this function stays total if either changes. */ if (!imagePart || !pluginPath) return null; return { imagePart, pluginPath }; } @@ -137,6 +142,9 @@ async function isAlreadyInstalled( return true; } + // Not unreachable: `dynamic-plugins.yaml` is parsed with a type assertion, + // not a runtime check, so a typo'd `pullPolicy` arrives here as an + // unrecognised string and must fall through to a re-install. if (pullPolicy !== PullPolicy.ALWAYS) return false; const digestFile = path.join(destination, pathInstalled, IMAGE_HASH_FILE); @@ -144,6 +152,9 @@ async function isAlreadyInstalled( const localDigest = (await fs.readFile(digestFile, 'utf8')).trim(); const parts = splitOciPackage(pkg); + /* istanbul ignore next -- unreachable for the same reason as the guard in + splitOciPackage: every `pkg` that reaches here carries a `!`, either + from the user or appended by the merger. */ if (!parts) return false; const remoteDigest = await imageCache.getDigest(parts.imagePart); if (localDigest !== remoteDigest) return false;