diff --git a/.github/label-actions.yml b/.github/label-actions.yml
index 42c351f76b200c429f7a01cbb437ac035488d11f..6f5e6646899c86029047eb218d1a8821adbb7d11 100644
--- a/.github/label-actions.yml
+++ b/.github/label-actions.yml
@@ -260,7 +260,7 @@
 
 
     ```ts
-    // v8 ignore next: typescript strict null check
+    /* v8 ignore next: typescript strict null check */
     if (!url) {
       return null;
     }
diff --git a/docs/development/best-practices.md b/docs/development/best-practices.md
index ca4482cfa356cc401b0e48459932ed152e31b339..73efcabbdab3c8b91ee8d430352401b36a5f399e 100644
--- a/docs/development/best-practices.md
+++ b/docs/development/best-practices.md
@@ -36,7 +36,7 @@ Read the [GitHub Docs, renaming a branch](https://docs.github.com/en/repositorie
 - Avoid `Boolean` instead use `is` functions from `@sindresorhus/is` package, for example: `is.string`
 
 ```ts
-// v8 ignore next: can never happen
+/* v8 ignore next: can never happen */
 ```
 
 ### Functions
diff --git a/docs/development/issue-labeling.md b/docs/development/issue-labeling.md
index d5277ee170b17adf437595359d6521cbc71b51be..3bae833dde9c5f1c5af8bcf193d124faf5f62832 100644
--- a/docs/development/issue-labeling.md
+++ b/docs/development/issue-labeling.md
@@ -203,7 +203,7 @@ Add a label `auto:logs` to indicate that there's a problem with the logs, and th
 
 Add a label `auto:needs-details` to discussions which need more details to move forward.
 
-Add a label `auto:no-coverage-ignore` if PR authors avoid needed unit tests by v8 ignoring code with the `// v8 ignore` comment.
+Add a label `auto:no-coverage-ignore` if PR authors avoid needed unit tests by v8 ignoring code with the `/* v8 ignore ... */` comment.
 
 Add a label `auto:no-done-comments` if PR authors unnecessary "Done" comments, or type comments to ask for a review instead of requesting a new review through GitHub's UI.
 
diff --git a/docs/development/local-development.md b/docs/development/local-development.md
index 0b15c230ef8b99431d0e5df38fa36a97f8cdf74b..59eaa8f731b59457ac9cfc73481ea715c15467e1 100644
--- a/docs/development/local-development.md
+++ b/docs/development/local-development.md
@@ -158,7 +158,7 @@ e.g. `pnpm vitest composer -u` would update the saved snapshots for _all_ tests
 ### Coverage
 
 The Renovate project maintains 100% test coverage, so any Pull Request will fail if it does not have full coverage for code.
-Using `// v8 ignore` is not ideal, but can be a pragmatic solution if adding more tests wouldn't really prove anything.
+Using `/* v8 ignore ... */` is not ideal, but can be a pragmatic solution if adding more tests wouldn't really prove anything.
 
 To view the current test coverage locally, open up `coverage/index.html` in your browser.
 
diff --git a/lib/config-validator.ts b/lib/config-validator.ts
index 210b78d81f7f12ee4a47665ea989c9be71df0ab5..bb8bfb21761a8fb784d3702fd5b7205bf8d4c631 100644
--- a/lib/config-validator.ts
+++ b/lib/config-validator.ts
@@ -1,5 +1,4 @@
 #!/usr/bin/env node
-// istanbul ignore file
 import 'source-map-support/register';
 import './punycode.cjs';
 import { dequal } from 'dequal';
diff --git a/lib/config/decrypt.ts b/lib/config/decrypt.ts
index 6cb34bb1fa68d9a35bf75b9db7018217014ebc0b..63622820d1be107324594865dd540abc3147a273 100644
--- a/lib/config/decrypt.ts
+++ b/lib/config/decrypt.ts
@@ -38,13 +38,14 @@ export async function tryDecrypt(
       );
     } else {
       decryptedStr = tryDecryptPublicKeyPKCS1(privateKey, encryptedStr);
-      // istanbul ignore if
+      /* v8 ignore start: not testable */
       if (is.string(decryptedStr)) {
         logger.warn(
           { keyName },
           'Encrypted value is using deprecated PKCS1 padding, please change to using PGP encryption.',
         );
       }
+      /* v8 ignore stop */
     }
   }
   return decryptedStr;
@@ -56,7 +57,6 @@ function validateDecryptedValue(
 ): string | null {
   try {
     const decryptedObj = DecryptedObject.safeParse(decryptedObjStr);
-    // istanbul ignore if
     if (!decryptedObj.success) {
       const error = new Error('config-validation');
       error.validationError = `Could not parse decrypted config.`;
diff --git a/lib/config/decrypt/legacy.ts b/lib/config/decrypt/legacy.ts
index b88f515aef47ba9c499421be9efaaa1b830e382b..c220bb3ca926ea04d3c9b00c00fdb17f82fc8be1 100644
--- a/lib/config/decrypt/legacy.ts
+++ b/lib/config/decrypt/legacy.ts
@@ -1,4 +1,3 @@
-/** istanbul ignore file */
 import crypto from 'node:crypto';
 import { logger } from '../../logger';
 
diff --git a/lib/config/migrate-validate.ts b/lib/config/migrate-validate.ts
index 2ba19032334fe4a4d5cd0e5923cebc8912d0e1e7..16818bb055b32703cc590e2233a2395854b109aa 100644
--- a/lib/config/migrate-validate.ts
+++ b/lib/config/migrate-validate.ts
@@ -33,7 +33,7 @@ export async function migrateAndValidate(
       warnings: ValidationMessage[];
       errors: ValidationMessage[];
     } = await configValidation.validateConfig('repo', massagedConfig);
-    // istanbul ignore if
+    /* v8 ignore start: hard to test */
     if (is.nonEmptyArray(warnings)) {
       logger.warn({ warnings }, 'Found renovate config warnings');
     }
@@ -45,7 +45,8 @@ export async function migrateAndValidate(
       massagedConfig.warnings = (config.warnings ?? []).concat(warnings);
     }
     return massagedConfig;
-  } catch (err) /* istanbul ignore next */ {
+    /* v8 ignore next 3: TODO: test me */
+  } catch (err) {
     logger.debug({ config: input }, 'migrateAndValidate error');
     throw err;
   }
diff --git a/lib/config/migration.ts b/lib/config/migration.ts
index 1ac07046d69f5e50e16605c3231841d2d3a805fc..86cf232a98dc0dfa15f506bfd839cd2d0b8f19ad 100644
--- a/lib/config/migration.ts
+++ b/lib/config/migration.ts
@@ -196,7 +196,8 @@ export function migrateConfig(
       };
     }
     return { isMigrated, migratedConfig };
-  } catch (err) /* istanbul ignore next */ {
+    /* v8 ignore next 4: TODO: test me */
+  } catch (err) {
     logger.debug({ config, err }, 'migrateConfig() error');
     throw err;
   }
diff --git a/lib/config/parse.spec.ts b/lib/config/parse.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ce5ba6498e838a87c4d53cf54fc1e550f1d4782b
--- /dev/null
+++ b/lib/config/parse.spec.ts
@@ -0,0 +1,59 @@
+import jsonValidator from 'json-dup-key-validator';
+import { parseFileConfig } from './parse';
+
+vi.mock('json-dup-key-validator', { spy: true });
+
+describe('config/parse', () => {
+  describe('json', () => {
+    it('parses', () => {
+      expect(parseFileConfig('config.json', '{}')).toEqual({
+        success: true,
+        parsedContents: {},
+      });
+    });
+
+    it('returns error', () => {
+      // syntax validation
+      expect(parseFileConfig('config.json', '{')).toEqual({
+        success: false,
+        validationError: 'Invalid JSON (parsing failed)',
+        validationMessage: 'Syntax error: unclosed statement near {',
+      });
+
+      // duplicate keys
+      vi.mocked(jsonValidator).validate.mockReturnValueOnce(undefined);
+      expect(parseFileConfig('config.json', '{')).toEqual({
+        success: false,
+        validationError: 'Duplicate keys in JSON',
+        validationMessage: '"Syntax error: unclosed statement near {"',
+      });
+
+      // JSON.parse
+      vi.mocked(jsonValidator).validate.mockReturnValue(undefined);
+      expect(parseFileConfig('config.json', '{')).toEqual({
+        success: false,
+        validationError: 'Invalid JSON (parsing failed)',
+        validationMessage:
+          'JSON.parse error:  `JSON5: invalid end of input at 1:2`',
+      });
+    });
+  });
+
+  describe('json5', () => {
+    it('parses', () => {
+      expect(parseFileConfig('config.json5', '{}')).toEqual({
+        success: true,
+        parsedContents: {},
+      });
+    });
+
+    it('returns error', () => {
+      expect(parseFileConfig('config.json5', '{')).toEqual({
+        success: false,
+        validationError: 'Invalid JSON5 (parsing failed)',
+        validationMessage:
+          'JSON5.parse error: `JSON5: invalid end of input at 1:2`',
+      });
+    });
+  });
+});
diff --git a/lib/config/parse.ts b/lib/config/parse.ts
index 45bbb8e651d434947c3346764bb9c39a519f47c1..991a1eb2144434ba2b953ac451097d7f972c8787 100644
--- a/lib/config/parse.ts
+++ b/lib/config/parse.ts
@@ -15,7 +15,7 @@ export function parseFileConfig(
   if (fileType === '.json5') {
     try {
       return { success: true, parsedContents: JSON5.parse(fileContents) };
-    } catch (err) /* istanbul ignore next */ {
+    } catch (err) {
       logger.debug({ fileName, fileContents }, 'Error parsing JSON5 file');
       const validationError = 'Invalid JSON5 (parsing failed)';
       const validationMessage = `JSON5.parse error: \`${err.message.replaceAll(
@@ -62,7 +62,7 @@ export function parseFileConfig(
         success: true,
         parsedContents: parseJson(fileContents, fileName),
       };
-    } catch (err) /* istanbul ignore next */ {
+    } catch (err) {
       logger.debug({ fileContents }, 'Error parsing renovate config');
       const validationError = 'Invalid JSON (parsing failed)';
       const validationMessage = `JSON.parse error:  \`${err.message.replaceAll(
diff --git a/lib/config/presets/gitea/index.spec.ts b/lib/config/presets/gitea/index.spec.ts
index 888afb7b11194d03474c1cf59c6005fb22ae7626..27b717a02b01c152f4248e107697bffcd852bcf1 100644
--- a/lib/config/presets/gitea/index.spec.ts
+++ b/lib/config/presets/gitea/index.spec.ts
@@ -1,22 +1,17 @@
-import { mockDeep } from 'jest-mock-extended';
 import * as httpMock from '../../../../test/http-mock';
-import { mocked } from '../../../../test/util';
-import * as _hostRules from '../../../util/host-rules';
+import { ExternalHostError } from '../../../types/errors/external-host-error';
 import { setBaseUrl } from '../../../util/http/gitea';
 import { toBase64 } from '../../../util/string';
 import { PRESET_INVALID_JSON, PRESET_NOT_FOUND } from '../util';
 import * as gitea from '.';
-
-vi.mock('../../../util/host-rules', () => mockDeep());
-
-const hostRules = mocked(_hostRules);
+import { hostRules } from '~test/host-rules';
 
 const giteaApiHost = gitea.Endpoint;
 const basePath = '/api/v1/repos/some/repo/contents';
 
 describe('config/presets/gitea/index', () => {
   beforeEach(() => {
-    hostRules.find.mockReturnValue({ token: 'abc' });
+    hostRules.add({ token: 'abc' });
     setBaseUrl(giteaApiHost);
   });
 
@@ -54,6 +49,24 @@ describe('config/presets/gitea/index', () => {
       );
       expect(res).toEqual({ from: 'api' });
     });
+
+    it('throws external host error', async () => {
+      httpMock
+        .scope(giteaApiHost)
+        .get(`${basePath}/some-filename.json`)
+        .reply(404, {});
+
+      hostRules.add({ abortOnError: true });
+
+      await expect(
+        gitea.fetchJSONFile(
+          'some/repo',
+          'some-filename.json',
+          giteaApiHost,
+          null,
+        ),
+      ).rejects.toThrow(ExternalHostError);
+    });
   });
 
   describe('getPreset()', () => {
diff --git a/lib/config/presets/gitea/index.ts b/lib/config/presets/gitea/index.ts
index bbe1dae14f95db6ea1ca1b5ccedd8cbcbe27c5cf..203de5bb1602d32a7558a397f9e14fd22736ba30 100644
--- a/lib/config/presets/gitea/index.ts
+++ b/lib/config/presets/gitea/index.ts
@@ -20,7 +20,6 @@ export async function fetchJSONFile(
       baseUrl: endpoint,
     });
   } catch (err) {
-    // istanbul ignore if: not testable with nock
     if (err instanceof ExternalHostError) {
       throw err;
     }
diff --git a/lib/config/presets/github/index.spec.ts b/lib/config/presets/github/index.spec.ts
index 623841f3c2accfc189478f381623a79ada2ff714..1829799e34e4ae28895fd8042e61714a1033a7a3 100644
--- a/lib/config/presets/github/index.spec.ts
+++ b/lib/config/presets/github/index.spec.ts
@@ -1,21 +1,16 @@
-import { mockDeep } from 'jest-mock-extended';
 import * as httpMock from '../../../../test/http-mock';
-import { mocked } from '../../../../test/util';
-import * as _hostRules from '../../../util/host-rules';
+import { ExternalHostError } from '../../../types/errors/external-host-error';
 import { toBase64 } from '../../../util/string';
 import { PRESET_INVALID_JSON, PRESET_NOT_FOUND } from '../util';
 import * as github from '.';
-
-vi.mock('../../../util/host-rules', () => mockDeep());
-
-const hostRules = mocked(_hostRules);
+import { hostRules } from '~test/host-rules';
 
 const githubApiHost = github.Endpoint;
 const basePath = '/repos/some/repo/contents';
 
 describe('config/presets/github/index', () => {
   beforeEach(() => {
-    hostRules.find.mockReturnValue({ token: 'abc' });
+    hostRules.add({ token: 'abc' });
   });
 
   describe('fetchJSONFile()', () => {
@@ -35,6 +30,24 @@ describe('config/presets/github/index', () => {
       );
       expect(res).toEqual({ from: 'api' });
     });
+
+    it('throws external host error', async () => {
+      httpMock
+        .scope(githubApiHost)
+        .get(`${basePath}/some-filename.json`)
+        .reply(404, {});
+
+      hostRules.add({ abortOnError: true });
+
+      await expect(
+        github.fetchJSONFile(
+          'some/repo',
+          'some-filename.json',
+          githubApiHost,
+          undefined,
+        ),
+      ).rejects.toThrow(ExternalHostError);
+    });
   });
 
   describe('getPreset()', () => {
diff --git a/lib/config/presets/github/index.ts b/lib/config/presets/github/index.ts
index a9063233107ec3d137171b01cb6fdabed043bdeb..643cfbc976b625d440440fe7ea8ce3b79b7e47e9 100644
--- a/lib/config/presets/github/index.ts
+++ b/lib/config/presets/github/index.ts
@@ -26,7 +26,6 @@ export async function fetchJSONFile(
   try {
     res = await http.getJsonUnchecked(url);
   } catch (err) {
-    // istanbul ignore if: not testable with nock
     if (err instanceof ExternalHostError) {
       throw err;
     }
diff --git a/lib/config/presets/http/index.spec.ts b/lib/config/presets/http/index.spec.ts
index 315619b8742be5ea269aceebe5b704e85a2fa62a..3fafe4799c0b9e16b128d38e75b826498339272f 100644
--- a/lib/config/presets/http/index.spec.ts
+++ b/lib/config/presets/http/index.spec.ts
@@ -1,6 +1,8 @@
 import * as httpMock from '../../../../test/http-mock';
+import { ExternalHostError } from '../../../types/errors/external-host-error';
 import { PRESET_DEP_NOT_FOUND, PRESET_INVALID_JSON } from '../util';
 import * as http from '.';
+import { hostRules } from '~test/host-rules';
 
 const host = 'https://my.server/';
 const filePath = '/test-preset.json';
@@ -46,5 +48,12 @@ describe('config/presets/http/index', () => {
         PRESET_DEP_NOT_FOUND,
       );
     });
+    it('throws external host error', async () => {
+      httpMock.scope(host).get(filePath).reply(404, {});
+
+      hostRules.add({ abortOnError: true });
+
+      await expect(http.getPreset({ repo })).rejects.toThrow(ExternalHostError);
+    });
   });
 });
diff --git a/lib/config/presets/http/index.ts b/lib/config/presets/http/index.ts
index 646b39a462e9deb50d284a7c3e8deb0e32799d09..90bd220f14e7384bc16b8543edb2b876fe18d511 100644
--- a/lib/config/presets/http/index.ts
+++ b/lib/config/presets/http/index.ts
@@ -22,7 +22,6 @@ export async function getPreset({
   try {
     response = await http.get(url);
   } catch (err) {
-    // istanbul ignore if: not testable with nock
     if (err instanceof ExternalHostError) {
       throw err;
     }
diff --git a/lib/config/presets/index.spec.ts b/lib/config/presets/index.spec.ts
index fc9e14a60ba9842061bb16a59b2b7aa285cf9677..708a93cb92d639b9adbefbc63a13c08c1015a164 100644
--- a/lib/config/presets/index.spec.ts
+++ b/lib/config/presets/index.spec.ts
@@ -1,6 +1,7 @@
 import { mockDeep } from 'jest-mock-extended';
 import { Fixtures } from '../../../test/fixtures';
-import { mocked } from '../../../test/util';
+import { PLATFORM_RATE_LIMIT_EXCEEDED } from '../../constants/error-messages';
+import { ExternalHostError } from '../../types/errors/external-host-error';
 import * as memCache from '../../util/cache/memory';
 import * as _packageCache from '../../util/cache/package';
 import { GlobalConfig } from '../global';
@@ -15,6 +16,7 @@ import {
   PRESET_RENOVATE_CONFIG_NOT_FOUND,
 } from './util';
 import * as presets from '.';
+import { logger, mocked } from '~test/util';
 
 vi.mock('./npm');
 vi.mock('./github');
@@ -83,9 +85,25 @@ describe('config/presets/index', () => {
       expect(res).toEqual({ foo: 1 });
     });
 
+    it('skips duplicate resolves', async () => {
+      config.extends = ['local>some/repo:a', 'local>some/repo:b'];
+      local.getPreset.mockResolvedValueOnce({ extends: ['local>some/repo:c'] });
+      local.getPreset.mockResolvedValueOnce({ extends: ['local>some/repo:c'] });
+      local.getPreset.mockResolvedValueOnce({ foo: 1 });
+      expect(await presets.resolveConfigPresets(config)).toEqual({
+        foo: 1,
+      });
+      expect(local.getPreset).toHaveBeenCalledTimes(3);
+      expect(logger.logger.debug).toHaveBeenCalledWith(
+        'Already seen preset local>some/repo:c in [local>some/repo:a, local>some/repo:c]',
+      );
+    });
+
     it('throws if invalid preset file', async () => {
       config.foo = 1;
-      config.extends = ['notfound'];
+      config.extends = ['local>some/repo'];
+
+      local.getPreset.mockResolvedValueOnce({ extends: ['notfound'] });
       let e: Error | undefined;
       try {
         await presets.resolveConfigPresets(config);
@@ -95,7 +113,8 @@ describe('config/presets/index', () => {
       expect(e).toBeDefined();
       expect(e!.validationSource).toBeUndefined();
       expect(e!.validationError).toBe(
-        "Cannot find preset's package (notfound)",
+        "Cannot find preset's package (notfound)." +
+          ' Note: this is a *nested* preset so please contact the preset author if you are unable to fix it yourself.',
       );
       expect(e!.validationMessage).toBeUndefined();
     });
@@ -464,6 +483,24 @@ describe('config/presets/index', () => {
 
       expect(packageCache.set.mock.calls[0][3]).toBe(60);
     });
+
+    it('throws', async () => {
+      config.extends = ['local>username/preset-repo'];
+
+      local.getPreset.mockRejectedValueOnce(
+        new ExternalHostError(new Error('whoops')),
+      );
+      await expect(presets.resolveConfigPresets(config)).rejects.toThrow(
+        ExternalHostError,
+      );
+
+      local.getPreset.mockRejectedValueOnce(
+        new Error(PLATFORM_RATE_LIMIT_EXCEEDED),
+      );
+      await expect(presets.resolveConfigPresets(config)).rejects.toThrow(
+        PLATFORM_RATE_LIMIT_EXCEEDED,
+      );
+    });
   });
 
   describe('replaceArgs', () => {
diff --git a/lib/config/presets/index.ts b/lib/config/presets/index.ts
index b482521ff41d591b252037eb2ee18f35dcbcffa9..68b756241ee5d87300016271d20f4d44e9f56fc1 100644
--- a/lib/config/presets/index.ts
+++ b/lib/config/presets/index.ts
@@ -159,7 +159,6 @@ export async function getPreset(
   }
   logger.trace({ presetConfig }, `Applied params to preset ${preset}`);
   const presetKeys = Object.keys(presetConfig);
-  // istanbul ignore if
   if (
     presetKeys.length === 2 &&
     presetKeys.includes('description') &&
@@ -212,7 +211,6 @@ export async function resolveConfigPresets(
           ignorePresets,
           existingPresets.concat([preset]),
         );
-        // istanbul ignore if
         if (inputConfig?.ignoreDeps?.length === 0) {
           delete presetConfig.description;
         }
@@ -271,11 +269,9 @@ async function fetchPreset(
     return await getPreset(preset, baseConfig ?? inputConfig);
   } catch (err) {
     logger.debug({ preset, err }, 'Preset fetch error');
-    // istanbul ignore if
     if (err instanceof ExternalHostError) {
       throw err;
     }
-    // istanbul ignore if
     if (err.message === PLATFORM_RATE_LIMIT_EXCEEDED) {
       throw err;
     }
@@ -295,7 +291,6 @@ async function fetchPreset(
     } else {
       error.validationError = `Preset caused unexpected error (${preset})`;
     }
-    // istanbul ignore if
     if (existingPresets.length) {
       error.validationError +=
         '. Note: this is a *nested* preset so please contact the preset author if you are unable to fix it yourself.';
@@ -313,7 +308,6 @@ function shouldResolvePreset(
   existingPresets: string[],
   ignorePresets: string[],
 ): boolean {
-  // istanbul ignore if
   if (existingPresets.includes(preset)) {
     logger.debug(
       `Already seen preset ${preset} in [${existingPresets.join(', ')}]`,
@@ -321,7 +315,6 @@ function shouldResolvePreset(
     return false;
   }
   if (ignorePresets.includes(preset)) {
-    // istanbul ignore next
     logger.debug(
       `Ignoring preset ${preset} in [${existingPresets.join(', ')}]`,
     );
diff --git a/lib/config/presets/internal/index.spec.ts b/lib/config/presets/internal/index.spec.ts
index e3dfbd4154dc3ba5fea8d80eadc1ce11b8dd0495..a85b009b5141e30e76b02330b2f01d275a630591 100644
--- a/lib/config/presets/internal/index.spec.ts
+++ b/lib/config/presets/internal/index.spec.ts
@@ -55,4 +55,8 @@ describe('config/presets/internal/index', () => {
       .flat()
       .forEach((preset) => expect(preset).not.toMatch(/{{.*}}/));
   });
+
+  it('returns undefined for unknown preset', () => {
+    expect(internal.getPreset({ repo: 'some/repo' })).toBeUndefined();
+  });
 });
diff --git a/lib/config/presets/internal/index.ts b/lib/config/presets/internal/index.ts
index d9ec8c4bdff8ba54619780a61146a91eb10b70e0..b550eca4a89ee179ec07e5d76e381443158bf929 100644
--- a/lib/config/presets/internal/index.ts
+++ b/lib/config/presets/internal/index.ts
@@ -41,7 +41,5 @@ export function getPreset({
   repo,
   presetName,
 }: PresetConfig): Preset | undefined {
-  return groups[repo] && presetName
-    ? groups[repo][presetName]
-    : /* istanbul ignore next */ undefined;
+  return groups[repo] && presetName ? groups[repo][presetName] : undefined;
 }
diff --git a/lib/config/validation.ts b/lib/config/validation.ts
index c89f07f4ee8e59bbd166290300a03e31d3658c1e..27962e25b3e83938cf1b2fe9d18f03ada2b4f17c 100644
--- a/lib/config/validation.ts
+++ b/lib/config/validation.ts
@@ -166,7 +166,7 @@ export async function validateConfig(
 
   for (const [key, val] of Object.entries(config)) {
     const currentPath = parentPath ? `${parentPath}.${key}` : key;
-    // istanbul ignore if
+    /* v8 ignore next 7: TODO: test me */
     if (key === '__proto__') {
       errors.push({
         topic: 'Config security error',
@@ -818,11 +818,9 @@ export async function validateConfig(
   }
 
   function sortAll(a: ValidationMessage, b: ValidationMessage): number {
-    // istanbul ignore else: currently never happen
     if (a.topic === b.topic) {
       return a.message > b.message ? 1 : -1;
     }
-    // istanbul ignore next: currently never happen
     return a.topic > b.topic ? 1 : -1;
   }
 
@@ -843,7 +841,7 @@ async function validateGlobalConfig(
   currentPath: string | undefined,
   config: RenovateConfig,
 ): Promise<void> {
-  // istanbul ignore if
+  /* v8 ignore next 5: not testable yet */
   if (getDeprecationMessage(key)) {
     warnings.push({
       topic: 'Deprecation Warning',
diff --git a/lib/constants/category.ts b/lib/constants/category.ts
index 94c22015c9a5cd3d0cec107ba1b89d2d31b183e4..e0a11c9a75ac0f72c3b27c5d0f43a7803dcda62b 100644
--- a/lib/constants/category.ts
+++ b/lib/constants/category.ts
@@ -1,4 +1,3 @@
-// istanbul ignore next
 export const Categories = [
   'ansible',
   'batect',
diff --git a/lib/instrumentation/index.ts b/lib/instrumentation/index.ts
index 71e66aab5ea038311a054a340eb241f584c3dfe6..23d7e630b931289b3aa05d07a893df09a0e73bf1 100644
--- a/lib/instrumentation/index.ts
+++ b/lib/instrumentation/index.ts
@@ -74,11 +74,8 @@ export function init(): void {
 
   instrumentations = [
     new HttpInstrumentation({
-      applyCustomAttributesOnSpan: /* istanbul ignore next */ (
-        span,
-        request,
-        response,
-      ) => {
+      /* v8 ignore start: not easily testable */
+      applyCustomAttributesOnSpan: (span, request, response) => {
         // ignore 404 errors when the branch protection of Github could not be found. This is expected if no rules are configured
         if (
           request instanceof ClientRequest &&
@@ -89,6 +86,7 @@ export function init(): void {
           span.setStatus({ code: SpanStatusCode.OK });
         }
       },
+      /* v8 ignore stop */
     }),
     new BunyanInstrumentation(),
   ];
@@ -97,8 +95,7 @@ export function init(): void {
   });
 }
 
-/* istanbul ignore next */
-
+/* v8 ignore start: not easily testable */
 // https://github.com/open-telemetry/opentelemetry-js-api/issues/34
 export async function shutdown(): Promise<void> {
   const traceProvider = getTracerProvider();
@@ -111,8 +108,8 @@ export async function shutdown(): Promise<void> {
     }
   }
 }
+/* v8 ignore stop */
 
-/* istanbul ignore next */
 export function disableInstrumentations(): void {
   for (const instrumentation of instrumentations) {
     instrumentation.disable();
diff --git a/lib/logger/__snapshots__/config-serializer.spec.ts.snap b/lib/logger/__snapshots__/config-serializer.spec.ts.snap
deleted file mode 100644
index bd6053b5b24a97ea557410e20f77737187e103c5..0000000000000000000000000000000000000000
--- a/lib/logger/__snapshots__/config-serializer.spec.ts.snap
+++ /dev/null
@@ -1,14 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`logger/config-serializer > squashes templates 1`] = `
-{
-  "nottoken": "b",
-  "prBody": "[Template]",
-}
-`;
-
-exports[`logger/config-serializer > suppresses content 1`] = `
-{
-  "content": "[content]",
-}
-`;
diff --git a/lib/logger/cmd-serializer.spec.ts b/lib/logger/cmd-serializer.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ab877f9c5fcacedcf5f1e970eb2da09fe8304e1c
--- /dev/null
+++ b/lib/logger/cmd-serializer.spec.ts
@@ -0,0 +1,13 @@
+import cmdSerializer from './cmd-serializer';
+
+describe('logger/cmd-serializer', () => {
+  it('returns array', () => {
+    expect(cmdSerializer([''])).toEqual(['']);
+  });
+
+  it('redacts', () => {
+    expect(cmdSerializer(' https://token@domain.com')).toEqual(
+      ' https://**redacted**@domain.com',
+    );
+  });
+});
diff --git a/lib/logger/cmd-serializer.ts b/lib/logger/cmd-serializer.ts
index 6b3492d3ba0e44296a4b568b3d025cf0ae7e9732..7dd7c78f3746268e534da263e18a7eda5b36a0a7 100644
--- a/lib/logger/cmd-serializer.ts
+++ b/lib/logger/cmd-serializer.ts
@@ -1,4 +1,3 @@
-// istanbul ignore next
 export default function cmdSerializer(
   cmd: string | string[],
 ): string | string[] {
diff --git a/lib/logger/config-serializer.spec.ts b/lib/logger/config-serializer.spec.ts
index 8a2dfd95ef7537203667431be40a1b5406745ca0..64847e096a407d897a777719d2f1c181d38ed9af 100644
--- a/lib/logger/config-serializer.spec.ts
+++ b/lib/logger/config-serializer.spec.ts
@@ -6,7 +6,8 @@ describe('logger/config-serializer', () => {
       nottoken: 'b',
       prBody: 'foo',
     };
-    expect(configSerializer(config)).toMatchSnapshot({
+    expect(configSerializer(config)).toEqual({
+      nottoken: 'b',
       prBody: '[Template]',
     });
   });
@@ -15,8 +16,17 @@ describe('logger/config-serializer', () => {
     const config = {
       content: {},
     };
-    expect(configSerializer(config)).toMatchSnapshot({
+    expect(configSerializer(config)).toEqual({
       content: '[content]',
     });
   });
+
+  it('suppresses packageFiles', () => {
+    const config = {
+      packageFiles: [],
+    };
+    expect(configSerializer(config)).toEqual({
+      packageFiles: '[Array]',
+    });
+  });
 });
diff --git a/lib/logger/config-serializer.ts b/lib/logger/config-serializer.ts
index 4e484b92ab6869a93666f8af1a02f5a0de7e1d71..b8a4c9e1bdfa2ae1ab43c1c025af6ff7f3f23150 100644
--- a/lib/logger/config-serializer.ts
+++ b/lib/logger/config-serializer.ts
@@ -22,7 +22,6 @@ export default function configSerializer(
       if (contentFields.includes(key)) {
         this.update('[content]');
       }
-      // istanbul ignore if
       if (arrayFields.includes(key)) {
         this.update('[Array]');
       }
diff --git a/lib/logger/index.spec.ts b/lib/logger/index.spec.ts
index fce2edf9be5a3329f33578bb14de81ba95d95441..92a61baeff1cfdcafb3ae41df136f5d1d22926ab 100644
--- a/lib/logger/index.spec.ts
+++ b/lib/logger/index.spec.ts
@@ -5,10 +5,12 @@ import { partial } from '../../test/util';
 import { add } from '../util/host-rules';
 import { addSecretForSanitizing as addSecret } from '../util/sanitize';
 import type { RenovateLogger } from './renovate-logger';
+import { ProblemStream } from './utils';
 import {
   addMeta,
   addStream,
   clearProblems,
+  createDefaultStreams,
   getContext,
   getProblems,
   levels,
@@ -161,6 +163,18 @@ describe('logger/index', () => {
     });
   });
 
+  describe('createDefaultStreams', () => {
+    it('creates log file stream', () => {
+      expect(
+        createDefaultStreams('info', new ProblemStream(), 'file.log'),
+      ).toMatchObject([
+        { name: 'stdout', type: 'raw' },
+        { name: 'problems', type: 'raw' },
+        { name: 'logfile' },
+      ]);
+    });
+  });
+
   it('sets level', () => {
     expect(logLevel()).toBeDefined(); // depends on passed env
     expect(() => levels('stdout', 'debug')).not.toThrow();
diff --git a/lib/logger/index.ts b/lib/logger/index.ts
index 301fdb323da990e64bece8772a71ac25edb63897..0f670affe2a2ee81b836c41168d2ce66ae4569cc 100644
--- a/lib/logger/index.ts
+++ b/lib/logger/index.ts
@@ -34,7 +34,6 @@ export function createDefaultStreams(
     stream: process.stdout,
   };
 
-  // istanbul ignore if: not testable
   if (getEnv('LOG_FORMAT') !== 'json') {
     // TODO: typings (#9615)
     const prettyStdOut = new RenovateStream() as any;
@@ -50,7 +49,6 @@ export function createDefaultStreams(
     type: 'raw',
   };
 
-  // istanbul ignore next: not easily testable
   const logFileStream: bunyan.Stream | undefined = is.string(logFile)
     ? createLogFileStream(logFile)
     : undefined;
@@ -60,7 +58,6 @@ export function createDefaultStreams(
   ) as bunyan.Stream[];
 }
 
-// istanbul ignore next: not easily testable
 function createLogFileStream(logFile: string): bunyan.Stream {
   // Ensure log file directory exists
   const directoryName = upath.dirname(logFile);
@@ -135,9 +132,7 @@ export function withMeta<T>(obj: Record<string, unknown>, cb: () => T): T {
   }
 }
 
-export /* istanbul ignore next */ function addStream(
-  stream: bunyan.Stream,
-): void {
+export function addStream(stream: bunyan.Stream): void {
   loggerInternal.addStream(stream);
 }
 
diff --git a/lib/logger/once.ts b/lib/logger/once.ts
index 41aa9b55d17d3f5d045606912aa0b0ec85d4c038..17e8328811a3d23b97efb6c861ed8dfd22f03377 100644
--- a/lib/logger/once.ts
+++ b/lib/logger/once.ts
@@ -1,5 +1,7 @@
 type OmitFn = (...args: any[]) => any;
 
+// TODO: use `callsite` package instead?
+
 /**
  * Get the single frame of this function's callers stack.
  *
@@ -24,7 +26,8 @@ function getCallSite(omitFn: OmitFn): string | null {
     if (callsite) {
       result = callsite.toString();
     }
-  } catch /* istanbul ignore next */ {
+    /* v8 ignore next 2: should not happen */
+  } catch {
     // no-op
   } finally {
     Error.stackTraceLimit = stackTraceLimitOrig;
@@ -39,7 +42,7 @@ const keys = new Set<string>();
 export function once(callback: () => void, omitFn: OmitFn = once): void {
   const key = getCallSite(omitFn);
 
-  // istanbul ignore if
+  /* v8 ignore next 3: should not happen */
   if (!key) {
     return;
   }
diff --git a/lib/logger/pretty-stdout.ts b/lib/logger/pretty-stdout.ts
index f358730a3ddae9652340c663eaef9a589e8716fb..dea632c4bbaf5924eb785b244feb1867bc5e70fc 100644
--- a/lib/logger/pretty-stdout.ts
+++ b/lib/logger/pretty-stdout.ts
@@ -100,7 +100,6 @@ export class RenovateStream extends Stream {
     this.writable = true;
   }
 
-  // istanbul ignore next
   write(data: BunyanRecord): boolean {
     this.emit('data', formatRecord(data));
     return true;
diff --git a/lib/logger/remap.spec.ts b/lib/logger/remap.spec.ts
index 3e7608971e27c748d1441f81458486d60499a057..fe0fce5d369ff85a0d6488dce527590ed8679e68 100644
--- a/lib/logger/remap.spec.ts
+++ b/lib/logger/remap.spec.ts
@@ -23,7 +23,7 @@ describe('logger/remap', () => {
 
   it('performs global remaps', () => {
     setGlobalLogLevelRemaps([{ matchMessage: '*foo*', newLogLevel: 'error' }]);
-    setRepositoryLogLevelRemaps(undefined);
+    setRepositoryLogLevelRemaps([]);
 
     const res = getRemappedLevel('foo');
 
diff --git a/lib/logger/renovate-logger.ts b/lib/logger/renovate-logger.ts
index 096b763ec42d4d29e9c3c99cf7b8a7d122bd8085..0d964a332fe6efe72bc14de7f006ce7473e4d58e 100644
--- a/lib/logger/renovate-logger.ts
+++ b/lib/logger/renovate-logger.ts
@@ -115,7 +115,7 @@ export class RenovateLogger implements Logger {
 
       if (is.string(msg)) {
         const remappedLevel = getRemappedLevel(msg);
-        // istanbul ignore if: not easily testable
+        /* v8 ignore next 4: not easily testable */
         if (remappedLevel) {
           meta.oldLevel = level;
           level = remappedLevel;
diff --git a/lib/logger/utils.spec.ts b/lib/logger/utils.spec.ts
index df0fb726fed246a2aa632869dc41cbc3ede0ee67..c9c94cb007b86d5884dc8caab0369d2e3bf98ec2 100644
--- a/lib/logger/utils.spec.ts
+++ b/lib/logger/utils.spec.ts
@@ -1,3 +1,4 @@
+import { TimeoutError } from 'got';
 import { z } from 'zod';
 import prepareError, {
   prepareZodIssues,
@@ -106,6 +107,9 @@ describe('logger/utils', () => {
     }
 
     it('prepareZodIssues', () => {
+      expect(prepareZodIssues(null)).toBe(null);
+      expect(prepareZodIssues({ _errors: ['a', 'b'] })).toEqual(['a', 'b']);
+
       expect(prepareIssues(z.string(), 42)).toBe(
         'Expected string, received number',
       );
@@ -214,5 +218,21 @@ describe('logger/utils', () => {
         stack: expect.stringMatching(/^ZodError: Schema error/),
       });
     });
+
+    it('handles HTTP timout error', () => {
+      const err = new TimeoutError(
+        // @ts-expect-error some types are private
+        new Error('timeout'),
+        {},
+        { context: { hostType: 'foo' } },
+      );
+      Object.assign(err, {
+        response: {},
+      });
+      expect(prepareError(err)).toMatchObject({
+        message: 'timeout',
+        name: 'TimeoutError',
+      });
+    });
   });
 });
diff --git a/lib/logger/utils.ts b/lib/logger/utils.ts
index d734057792e1f9f63ef41c568a4beb0959a30183..723f8e4e00ee18f51d0fcba02810650318839d31 100644
--- a/lib/logger/utils.ts
+++ b/lib/logger/utils.ts
@@ -57,14 +57,12 @@ type ZodShortenedIssue =
     };
 
 export function prepareZodIssues(input: unknown): ZodShortenedIssue {
-  // istanbul ignore if
   if (!is.plainObject(input)) {
     return null;
   }
 
   let err: null | string | string[] = null;
   if (is.array(input._errors, is.string)) {
-    // istanbul ignore else
     if (input._errors.length === 1) {
       err = input._errors[0];
     } else if (input._errors.length > 1) {
@@ -96,11 +94,11 @@ export function prepareZodIssues(input: unknown): ZodShortenedIssue {
 }
 
 export function prepareZodError(err: ZodError): Record<string, unknown> {
-  // istanbul ignore next
   Object.defineProperty(err, 'message', {
     get: () => 'Schema error',
+    /* v8 ignore next 3: TODO: drop set? */
     set: () => {
-      // intentionally empty
+      /* intentionally empty */
     },
   });
 
@@ -144,13 +142,11 @@ export default function prepareError(err: Error): Record<string, unknown> {
     options.method = err.options.method;
     options.http2 = err.options.http2;
 
-    // istanbul ignore else
     if (err.response) {
       response.response = {
-        statusCode: err.response?.statusCode,
-        statusMessage: err.response?.statusMessage,
+        statusCode: err.response.statusCode,
+        statusMessage: err.response.statusMessage,
         body:
-          // istanbul ignore next: not easily testable
           err.name === 'TimeoutError'
             ? undefined
             : structuredClone(err.response.body),
diff --git a/lib/proxy.ts b/lib/proxy.ts
index 6cc1550ce08444983eb208173981252acfc19a5d..25c2b82c584847627ee3a07709065a3eb19d8798 100644
--- a/lib/proxy.ts
+++ b/lib/proxy.ts
@@ -8,13 +8,14 @@ let agent = false;
 
 export function bootstrap(): void {
   envVars.forEach((envVar) => {
-    /* istanbul ignore if: env is case-insensitive on windows */
+    /* v8 ignore start: env is case-insensitive on windows */
     if (
       typeof process.env[envVar] === 'undefined' &&
       typeof process.env[envVar.toLowerCase()] !== 'undefined'
     ) {
       process.env[envVar] = process.env[envVar.toLowerCase()];
     }
+    /* v8 ignore stop */
 
     if (process.env[envVar]) {
       logger.debug(`Detected ${envVar} value in env`);
diff --git a/lib/renovate.ts b/lib/renovate.ts
index d60ae5b2d7dec13a62f97dbf667983d046ded14a..a9c1ef2c7f81e37c2cf28fdebe70c6619864bcb1 100644
--- a/lib/renovate.ts
+++ b/lib/renovate.ts
@@ -7,7 +7,7 @@ import { logger } from './logger';
 import { bootstrap } from './proxy';
 import { start } from './workers/global';
 
-// istanbul ignore next
+/* v8 ignore next 3: not easily testable */
 process.on('unhandledRejection', (err) => {
   logger.error({ err }, 'unhandledRejection');
 });
@@ -19,7 +19,7 @@ bootstrap();
   process.exitCode = await instrument('run', () => start());
   await telemetryShutdown(); //gracefully shutdown OpenTelemetry
 
-  // istanbul ignore if
+  /* v8 ignore next 3: no test required */
   if (process.env.RENOVATE_X_HARD_EXIT) {
     process.exit(process.exitCode);
   }
diff --git a/lib/util/cache/package/redis.ts b/lib/util/cache/package/redis.ts
index 24c36a8d4ab6c4c21184179c2eb615754d5b9e54..5e3a640c9e84076c69cd9d97b39937a32ec6bf63 100644
--- a/lib/util/cache/package/redis.ts
+++ b/lib/util/cache/package/redis.ts
@@ -1,4 +1,3 @@
-/* istanbul ignore file */
 import { DateTime } from 'luxon';
 import { createClient, createCluster } from 'redis';
 import { logger } from '../../../logger';
diff --git a/lib/util/http/legacy.ts b/lib/util/http/legacy.ts
index 3582c251d167da208360f66291b71df552b11beb..318a17106cbed483212e2e5d842d8179003966d9 100644
--- a/lib/util/http/legacy.ts
+++ b/lib/util/http/legacy.ts
@@ -1,4 +1,3 @@
-// istanbul ignore file
 import { RequestError as HttpError } from 'got';
 import { parseUrl } from '../url';
 
diff --git a/lib/workers/repository/cache.ts b/lib/workers/repository/cache.ts
index 55dc4beac8ea68949bbe2befc56d723bcc54b14f..30d92fd33faaf34e581493349feee3cb464964ba 100644
--- a/lib/workers/repository/cache.ts
+++ b/lib/workers/repository/cache.ts
@@ -1,5 +1,3 @@
-/* istanbul ignore file */
-
 import { REPOSITORY_CHANGED } from '../../constants/error-messages';
 import { logger } from '../../logger';
 import { platform } from '../../modules/platform';
diff --git a/test/host-rules.ts b/test/host-rules.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ba3c997cadde3a25d5e7073aec4fb32d5b028ba2
--- /dev/null
+++ b/test/host-rules.ts
@@ -0,0 +1,7 @@
+import { hostRules } from './util';
+
+export * as hostRules from '../lib/util/host-rules';
+
+beforeEach(() => {
+  hostRules.clear();
+});
diff --git a/vitest.config.ts b/vitest.config.ts
index d42fd4df7d613589a2f85ef00051be0ff0f1eddb..ae7716726cba7e5d961a6ac480e033cb241aefb9 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -105,6 +105,7 @@ export default defineConfig(() =>
             '__mocks__/**',
             // fully ignored files
             'lib/config-validator.ts',
+            'lib/constants/category.ts',
             'lib/modules/datasource/hex/v2/package.ts',
             'lib/modules/datasource/hex/v2/signed.ts',
             'lib/util/cache/package/redis.ts',