akm docs

DI seams plan — replace mock.module with swap-and-restore module seams

Branch: refactor/di-seams

Implementation note (added later): this plan has landed — the seam pattern (_set…ForTests delegators, tests/_helpers/seams.ts, overrideSeam/withSeam) is in production use (e.g. _setChatCompletionForTests in src/llm/client.ts, consumed by multiple test files), mock.module( no longer appears as live code anywhere under tests/ (only two historical comments reference the retired pattern), and src/indexer/db/db.ts — flagged below as needing its stale mock deleted — no longer exists at all; its functionality (openIndexDatabase) now lives in src/storage/repositories/index-connection.ts. Several of the specific test file paths named per-module below were renamed or consolidated by a later, unrelated test reorganization; those are annotated in place rather than silently repointed, since this document's per-module breakdown is itself the historical record of how the migration was planned.

Guardrails (binding, non-negotiable)

This workstream was previously done wrong (~900 dependency-injection parameter threads, reverted). The rules for THIS attempt:

The seam pattern (canonical shape)

For a module exporting foo(a, b):

// ── Test seam ────────────────────────────────────────────────────────────────
// Swap-and-restore override. Inert in production; only tests call the setter.
let fooOverride: typeof fooReal | undefined;

/** TEST-ONLY. Swap the implementation of `foo`; pass undefined to restore. */
export function _setFooForTests(fake?: typeof fooReal): void {
  fooOverride = fake;
}

export function foo(a: A, b: B): R {
  if (fooOverride) return fooOverride(a, b);
  return fooReal(a, b);
}

function fooReal(a: A, b: B): R {
  // …existing body, renamed, NOT exported…
}

Notes on the shape:

Shared test helper — tests/_helpers/seams.ts (new file, ~40 lines)

// tests/_helpers/seams.ts
type SeamSetter<T> = (fake: T | undefined) => void;

/** Setters that currently hold a fake; drained by resetAllSeams(). */
const active = new Set<SeamSetter<unknown>>();

/**
 * Install a fake for the current test. Restoration is automatic: the
 * tests/_preload.ts afterEach calls resetAllSeams(). Use this for
 * file-scoped or beforeEach-scoped fakes (the common case, mirroring
 * today's top-of-file mock.module blocks).
 */
export function overrideSeam<T>(set: SeamSetter<T>, fake: T): void {
  set(fake);
  active.add(set as SeamSetter<unknown>);
}

/** Scoped swap → run → finally-restore, for fakes needed in one test only. */
export async function withSeam<T, R>(
  set: SeamSetter<T>,
  fake: T,
  run: () => R | Promise<R>,
): Promise<R> {
  set(fake);
  active.add(set as SeamSetter<unknown>);
  try {
    return await run();
  } finally {
    set(undefined);
    active.delete(set as SeamSetter<unknown>);
  }
}

/** Safety net: restore every active seam. Called by tests/_preload.ts. */
export function resetAllSeams(): void {
  for (const set of active) set(undefined);
  active.clear();
}

Wiring into tests/_preload.ts

Two one-line additions to the existing harness (no new lifecycle machinery):

  1. resetSingletons() (tests/_preload.ts:294) gains resetAllSeams(); — a leaked seam from a previous file is cleared before every test, exactly like resetConfigCache() / resetLocalEmbedder() today.
  2. The existing afterEach (tests/_preload.ts:329) gains resetAllSeams(); so a fake never survives past the test that installed it.

Because tests always install fakes via overrideSeam/withSeam, the preload needs no knowledge of individual _set…ForTests functions — the registry is the reset list. A test that calls a _set…ForTests setter directly (bypassing the helper) is a review-reject; grep-able (_set.*ForTests outside tests/_helpers/seams.ts usage must go through the helper).

Typical file migration shape (replaces a top-of-file mock.module block):

import { overrideSeam } from "../_helpers/seams";
import { _setChatCompletionForTests } from "../../src/llm/client";

let chatResponder: (userContent: string) => string | Promise<string> = () => "";

beforeEach(() => {
  overrideSeam(_setChatCompletionForTests, async (_conn, messages) => {
    const user = messages.find((m) => m.role === "user");
    return chatResponder(user?.content ?? "");
  });
});

Static imports of the module under test become safe again (no more "mock.module must run before the module under test is imported" ordering comments, no more dynamic-import contortions).


Per-module seam designs

Ordered for sequential implementation, most-used-by-tests first. Implement one module per commit: add seam → migrate its test files → delete the mock.module blocks → gate green → commit.

0. Helper first: tests/_helpers/seams.ts + tests/_preload.ts wiring

As specified above. Land with module 1 (the helper is exercised immediately).

1. src/llm/client — 5 test files (easiest, highest fan-out)

2. src/core/warn — 3 test files

The module already has real hooks for all its state (setQuiet/resetQuiet/setVerbose/resetVerbose/setLogFile/clearLogFile). Tests mock it only to capture output. So the seam is a single sink intercept, not 13 overridable functions:

3. src/llm/embedder — 3 test files

Facade module; several mocked names are re-exports of pure constants/math that need no seam (DEFAULT_LOCAL_MODEL constant, cosineSimilarity math — the real ones satisfy every test).

4. @huggingface/transformers — 2 test files (seam lives in src, NOT the package)

Third-party, but the ONLY consumer is the dynamic await import("@huggingface/transformers") inside LocalEmbedder.getPipeline (src/llm/embedders/local.ts:220). That import is a perfect internal binding:

5. src/setup/registry-stash-loader — 1 test file

6. src/tasks/backends — 1 test file

selectBackend(options) already accepts injected backends (src/tasks/backends/index.ts:59-69) but production callers invoke it with no args — threading a fake backend through akmTasksAdd would be a call-site change (forbidden). Module seam instead:

7-14. The tests/integration/setup-run.test.ts cluster (one file, 74 mock.module calls across 9 near-identical blocks)

This file repeats the same ~10 mocks in every test block. Migrate it LAST, in one pass, after all seams below exist. Each block's mock stanza collapses to a single installSetupSeams(overrides) local helper (test-file-local function calling overrideSeam per seam) — deleting ~600 lines of repeated mock boilerplate. Per module:

7. src/core/config/config — NO SEAM (subtract the mock)

resetConfigCache() exists (config.ts:124) and already runs in the preload. The module is XDG-env-driven and the sandbox helpers (withIsolatedAkmStorage / sandboxXdgConfigHome in tests/_helpers/sandbox.ts) already isolate it. Migration: delete the mock; let the wizard read/write the REAL config in the sandboxed XDG home. Assertions on "what was saved" read the config file (or loadUserConfig()) from the sandbox instead of spying on saveConfig. Fallback ONLY if an assertion truly needs call interception: a minimal _setSaveConfigForTests — decide during implementation, default is no seam.

8. src/core/paths — NO SEAM (subtract the mock)

Pure functions of process.env; getConfigDir/getDataDir already take (env, platform). With sandboxed XDG env vars the mock is dead weight. Migration: delete the mock block; assert against the sandbox paths.

9. src/setup/detect — seam for the two network/host probes

10. src/commands/sources/init — seam for akmInit

Single export, no state. Real body → akmInitReal; delegator + export function _setAkmInitForTests(fake?: typeof akmInit): void;.

11. src/indexer/indexer — seam for akmIndex

Today's full-module replacement leaves every OTHER export (e.g. buildFileBasenameMap) undefined for the module under test — a latent bug the seam fixes for free. Real body → akmIndexReal; delegator + export function _setAkmIndexForTests(fake?: typeof akmIndex): void;. All other exports stay real.

12. src/indexer/db/db — DELETE THE STALE MOCK (no seam, decide fallback in-flight)

The current mock is broken: it exports openDatabase, but the real module and setup.ts:41 use openIndexDatabase — under the mock, openIndexDatabase is undefined and the vec-probe try/catch (setup.ts:582-600) silently swallows the TypeError. The test has never exercised this path. Migration: delete the mock and let the vec probe open a REAL index DB inside the sandboxed tmp dirs (this is an integration test; that is the honest behavior and un-swallows the probe). Fallback only if a real open proves too slow or vec-extension-dependent in CI: minimal _setIndexDbForTests({ openIndexDatabase?, isVecAvailable? }) delegators. Default is deletion. (Landed as deletion, confirmed: tests/integration/setup-run.test.ts has zero mock.module calls today. src/indexer/db/db.ts no longer exists — openIndexDatabase now lives in src/storage/repositories/index-connection.ts — though setup-run.test.ts still has a stale comment naming the old path.)

13. src/integrations/agent — seam in the DEFINING module

The test spreads the real barrel and overrides detectAgentCliProfiles + pickDefaultAgentProfile. Both are defined in src/integrations/agent/detect.ts (:81, :106) and re-exported by the barrel (index.ts:33). The seam must live in detect.ts (a barrel re-export of a delegator carries the seam automatically; seaming the barrel itself would not affect direct ./detect importers).

14. src/commands/tasks/default-tasks — seam for the three mocked exports

registerDefaultTasks(deps) and isCiEnvironment(env = process.env) already have DI params, but setup.ts:19 imports and calls them bare — using the DI params would be a call-site change (forbidden). Module seam:


Deferred — RESOLVED (deferral is over)

@clack/prompts — src seam SHIPPED (src/cli/clack.ts); mock.module is at ZERO

Originally deferred because seaming a third-party package needs a src-side wrapper module and the guardrails forbid new wrappers. The deferral was overturned by an adversarial review that BLOCKED landing the branch on proof:

Resolution (same pattern as the @huggingface/transformers loader seam in src/llm/embedders/local.ts — a third-party module gets its seam in src):

Standing rule: mock.module( must never reappear in tests/ (grep-enforced at review; the preload comment documents why). New third-party dependencies that tests need to fake get a src-side seam module like this one.


Implementation order (one commit per numbered step, gate green each time)

  1. tests/_helpers/seams.ts + tests/_preload.ts wiring + src/llm/client seam, migrating its 5 test files (proves the whole pattern end-to-end on the easiest group).
  2. src/core/warn sink seam — 3 files (includes the agent-builders rewrite).
  3. src/llm/embedder facade seam — indexer.test.ts + dedup-cache-wiring migrations (setup-run's embedder block waits for step 7).
  4. @huggingface/transformers loader seam in embedders/local.ts — 2 files.
  5. src/setup/registry-stash-loader — setup-wizard partial migration (clack mock stays).
  6. src/tasks/backends — tasks-write-target.
  7. setup-run cluster, one seam-module at a time inside the single file: add seams for detect / init / indexer / agent-detect / default-tasks; delete the config + paths + indexer-db mocks (sandbox/real replacements); dedupe the clack mock; collapse the 9 stanzas into installSetupSeams().

Per-commit gate: bun run check (types + lint + unit/integration per repo release-gate rule) — 0 errors, 0 warnings, 0 failures. Expected net effect: ~15-20 added lines per seamed src module vs hundreds of deleted mock lines in tests (setup-run alone should shrink by roughly half).