akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
447 lines (341 loc) • 19.4 kB
Markdown
# Testing Workflow
This project is a CLI with three risk-heavy areas:
- command behavior across real files, config, and cache directories
- managed-source install/update flows
- binary deployment and self-upgrade behavior
The safest way to test it is in layers: fast local checks first, then end-to-end
CLI coverage, then Docker-based deployment and upgrade validation. This document
owns automated test workflow and test-authoring guidance only. The authoritative
manual setup, command matrix, expected behavior, release gates, evidence, and
cleanup live in [AKM Manual Testing Runbook](./manual-testing-checklist.md).
## What To Validate
- core CLI flows: `setup`, `index`, `search`, `show`, `info`, `bundle list`, `config`
- asset lifecycle: add assets, re-index, search, show, and incremental refresh
- managed-source lifecycle: `akm bundle add`, `akm bundle list`, `akm bundle update`, `akm bundle remove`
- binary lifecycle: install, run, `akm upgrade --check`, `akm upgrade`
- cross-environment behavior on Ubuntu, Debian, Alpine, and Fedora containers
## Test Layers In This Repo
### 1. Fast local correctness
Run these before any release candidate or merge:
```sh
bun run test:unit
bunx biome check --write src/ tests/
bunx tsc --noEmit
```
Use this when you want the shortest full-project signal:
```sh
bun run check
```
Relevant coverage:
- `tests/integration/self-update.test.ts` - self-upgrade detection and checksum enforcement
- `tests/integration/registry-*.test.ts`, `tests/provider-registry.test.ts` - `list`, `remove`, `update`, cache cleanup, install resolution, tar safety, local/git/npm paths
- `tests/integration/setup-run.test.ts` - full setup wizard orchestration and failure handling
- `tests/integration/install-script.test.ts` - repeatable `install.sh` edge cases and permission paths
### Writing deterministic, isolated tests
Both `scripts/test-unit.sh` (`bun run test:unit`) and `scripts/test-integration.sh`
(`bun run test:integration`) shard their target across up to `min(nproc, 8)`
concurrent `bun test` processes, splitting the file list round-robin — neither
runner is a single shared process, and neither is "the unit suite" alone.
Within one shard, every file assigned to that process still runs in **one
shared process**: one `process.env`, one module-singleton namespace, and
(under fake timers) one global clock for every file in that shard. A test that
mutates shared state without restoring it, or that asserts on a wall-clock
measurement, can pass on one scheduling and
fail on another — the two release/0.8.0 flakes (scoring-pipeline Issue #14
reading the wrong index DB after a sibling mutated `XDG_DATA_HOME`; the
llm-client timeout test racing real timers) were both this class.
Rules (conventions, not statically enforced — the lint script that policed
them was deleted in 0.9.8; the runtime guard in `src/core/paths.ts` throws
`TEST_ISOLATION_MISSING` and the preload tripwire catches leaks):
1. **Never mutate `AKM_*` / `XDG_*` / `HOME` on `process.env` directly.** Use the
sanctioned helpers in `tests/_helpers/sandbox.ts`:
- `sandboxStashDir`, `sandboxXdgConfigHome`, `sandboxXdgDataHome`,
`sandboxXdgCacheHome`, `sandboxXdgStateHome`, `sandboxHome` — set the env var
to an isolated temp dir and return a `cleanup` that restores the prior value.
Chain them and call `cleanup()` in `afterEach`.
- `withEnv({ AKM_BUNDLE_DIR }, async () => …)` — scoped override that always
restores in a `finally`, even on throw. Use this for per-call overrides
around an in-process CLI invocation.
- `makeStashDir` / `makeSandboxDir` — temp dirs that are NOT wired into env
(pass them to `withEnv` or a subprocess env object yourself).
The DB path resolves from `XDG_DATA_HOME`; if you `akmIndex` then `akmSearch`,
both must see the **same** sandboxed `XDG_DATA_HOME` or the search reads a
different (empty/stale) DB. Sandbox it in `beforeEach`.
2. **Do not assert on a measured wall-clock delta.** `expect(Date.now() - start)
.toBeLessThan(N)` races the scheduler. Assert the *observable result* instead
(e.g. `result.reason === "timeout"`), or drive time deterministically with
`jest.useFakeTimers()` + `jest.advanceTimersByTime(...)` and a fetch/spawn stub.
Asserting on a `durationMs` field computed from injected fixture timestamps is
fine (it is deterministic).
3. **Sort/compare on the value the user sees.** When a test asserts ordering,
make the production sort key the same quantized value that is displayed
(`db-search.ts` sorts on the clamped+rounded score, then breaks ties by name),
so an invisible sub-display-precision epsilon can never reorder visible ties.
4. **Avoid stateful feedback within one test.** Re-running `akmSearch` without
`skipLogging: true` writes utility/recency rows that perturb the next search's
ranking. Pass `skipLogging: true` when you need repeatable ranking across
calls in a single test.
If a file legitimately needs a literal env value (e.g. pure path-resolution unit
tests) and restores via its own save/restore wrapper, add it to the linter's
`ENV_ASSIGN_ALLOWED` set with a one-line justification — the list may only shrink.
### 2. End-to-end CLI validation
Run the full integration suite when changing CLI behavior, indexing, search,
config, source management, or output shaping:
```sh
bun run test:integration
```
Collectively, these suites (`tests/integration/`) exercise real flows,
including:
- fallback search without an index
- `index -> search -> show`
- CLI subprocess execution through `src/cli.ts`
- config read/write behavior
- registry-source compatibility
- progressive indexing and re-indexing
- update and upgrade command error paths
- knowledge `#fragment` selection and mixed asset discovery
## Semantic Search States
Semantic search does not behave as a simple on/off feature at runtime. Testing
should distinguish between saved-config state and actual readiness.
### Config intent
Semantic search intent is saved independently as `semanticSearchMode`:
- `off` when the user opts out explicitly
- `auto` when the user wants semantic search enabled
Setup should not flip intent from `auto` back to `off` because preparation or
verification fails transiently.
### Runtime readiness
Actual semantic readiness is tracked separately from config intent. Runtime state
can be:
- `pending` when semantic search is enabled but not yet verified
- `ready-js` when every entry has a vector (the name is historical; there is no other vector path)
- `blocked` when semantic search cannot run with the current provider/setup
These cases are covered by `tests/integration/setup-run.test.ts` and the focused
semantic/config suites.
The real-model gate (`AKM_SEMANTIC_TESTS=1`) honors an explicit `HF_HOME` and
otherwise uses the ignored, repo-local `.ci-cache/huggingface` directory. That
path is outside the preload's disposable test `HOME`, so a second local run
reuses the model without exposing the developer's normal AKM cache. Gated CI
sets the same path and restores it from a model/source-identified Actions
cache. Candidate tags and manual runs are restore-only; only the scheduled
default-branch run may save a cache entry.
The same gate runs real semantic index/search round-trips against the declared
external dependency. Package acceptance separately verifies the ordinary npm
package surface; there is no installed-consumer compatibility architecture or
copied semantic runtime to audit.
### What to test explicitly
- config stays `off` only when the user disables semantic search intentionally
- config stays `auto` when preparation is skipped intentionally
- config stays `auto` when preparation fails but runtime status becomes `blocked`
- runtime status becomes `pending`, `ready-js`, or `blocked` as appropriate
- index and info output report readiness state instead of only config intent
### 3. Docker deployment validation
Run the Docker matrix when changing install, packaging, startup, runtime
dependencies, or platform behavior:
```sh
AKM_DOCKER_TESTS=1 bun test tests/integration/docker-install.test.ts
```
Or run the shell orchestrator directly:
```sh
./tests/docker/run-docker-tests.sh
```
This validates two deployment methods across four Linux families:
- bun-based install: Ubuntu, Debian, Alpine, Fedora
- compiled binary install: Ubuntu, Debian, Fedora
Binary validation currently excludes Alpine. The compiled Linux binary used in
this repo's Docker tests is not packaged for Alpine/musl compatibility, so the
binary deployment gate focuses on the glibc-based targets we currently support.
The Docker smoke test in `tests/docker/smoke-test.sh` verifies:
- `akm --help`
- `akm bundle create`
- bundle directory creation
- `akm index`
- `akm search`
- `akm show`
- `akm info`
- `akm bundle list`
- incremental re-index after adding a new asset
### 4. Benchmark (agent utility)
The LLM-provider-driven, multi-seed agent-utility benchmark (`akm-bench`) now
lives in the standalone repo **[itlackey/akm-bench](https://github.com/itlackey/akm-bench)**.
Run it from there after any change to `src/output/`, `src/commands/read/show.ts`,
APPLY directives, or other content that affects what agents see.
For curate/search ranking quality, run the retrieval eval of
**[itlackey/akm-eval](https://github.com/itlackey/akm-eval)**
(`evals/retrieval/run --corpus public`, with `AKM_BIN` naming the build under
test). It scores `akm search` and `akm curate` against graded judgments and
calls no model service. The in-repo curate benchmark that used to do this,
`akm-eval-curate-bench`, left with the rest of `scripts/akm-eval/`; see
`docs/maintainers/eval.md`.
## Recommended Workflow
### Normal change
Use this for most code changes:
```sh
bun run test:unit
bun run test:integration
bunx biome check --write src/ tests/
bunx tsc --noEmit
```
### Install, packaging, or release-related change
Use this when touching `src/cli.ts`, `src/commands/sources/self-update.ts`, install flows,
source management, or Docker assets:
```sh
bun run test:unit
bun test tests/integration/self-update.test.ts tests/integration/setup-run.test.ts tests/integration/install-script.test.ts
./tests/docker/run-docker-tests.sh
bunx biome check --write src/ tests/
bunx tsc --noEmit
```
### Release gate
Use this before publishing a release:
```sh
bun run release:check
```
If Docker is available, prefer `./tests/docker/run-docker-tests.sh` over a
single-variant container run so both bun and binary installs are covered.
For a local release gate without Docker, use:
```sh
./tests/release-check.sh --skip-docker
```
That script now runs a dedicated install/setup regression suite before the full
test run so first-run, installer, and wizard failures surface early.
The local script is only one half of release validation. Follow the
[maintainer release checklist](../../maintainers/release-checklist.md) to
run **Gated CI** from a `gated-ci/candidate-*` tag targeting the exact candidate
commit, or manually dispatch it with the full candidate
SHA after the workflow reaches the default branch. Its real-embedding, Docker,
and Linux/macOS/Windows scheduler jobs must all succeed, and the release PR
must link the resulting Actions run. The weekly scheduled run detects
default-branch drift; it cannot attest a different release-candidate commit.
## Manual QA Authority
Use [AKM Manual Testing Runbook](./manual-testing-checklist.md) for every manual
flow. Its full sandbox is mandatory; the smaller historical XDG-only setup is
unsafe because `AKM_CONFIG_DIR` can bypass it. The runbook also owns expected
exits/channels, fixtures, release evidence, platform gates, and cleanup.
## Docker Deployment Validation
### Automated matrix
The repo already contains Dockerfiles for:
- `tests/docker/Dockerfile.ubuntu-bun`
- `tests/docker/Dockerfile.debian-bun`
- `tests/docker/Dockerfile.alpine-bun`
- `tests/docker/Dockerfile.fedora-bun`
- `tests/docker/Dockerfile.ubuntu-binary`
- `tests/docker/Dockerfile.debian-binary`
- `tests/docker/Dockerfile.fedora-binary`
`Dockerfile.alpine-binary` does not exist and is deliberately not part of this
matrix: binary validation excludes Alpine (see above) because the compiled
binary targets glibc and Alpine is musl-based.
Run one variant if you need a focused repro:
```sh
./tests/docker/run-docker-tests.sh ubuntu-binary
./tests/docker/run-docker-tests.sh --bun-only
./tests/docker/run-docker-tests.sh --binary-only
```
### What the Docker matrix proves
- the CLI starts in minimal Linux images
- runtime dependencies are sufficient for `bundle create`, `index`, `search`, and `show`
- bun-linked installs work after building from source
- compiled Linux binaries run correctly when copied into the image
- the CLI can create a fresh bundle, build an index, and discover new assets
Treat this matrix as a release gate for shell-level regressions too. The Docker
smoke path exercises real entrypoint scripts and container command execution,
which can catch failures that unit and subprocess tests miss.
### What it does not prove by itself
- that the published release artifact matches the local compiled binary
- that `install.sh` works against a real GitHub release
- that self-upgrade can replace the running binary in-place
Those should be validated in disposable containers as described next.
## Published Artifact Acceptance
Published binary, installer, checksum, native-platform, and self-upgrade
procedures are release-facing manual gates. Run sections 21-22 of the
[manual runbook](./manual-testing-checklist.md); do not validate release bytes
with an installer fetched from raw `main`.
## Upgrade Regression Coverage
Managed-source updates and CLI self-upgrade are separate implementations.
Provider/registry suites cover source add/update/remove/cache behavior;
`tests/integration/self-update.test.ts` covers install-method detection,
orchestration, checksum, and failure paths. Real managed-source and self-upgrade
acceptance is manual and lives in section 21 of the
[manual runbook](./manual-testing-checklist.md).
### Upgrade rehearsal gate
`tests/integration/upgrade-rehearsal/` installs the PREVIOUS published stable
`akm-cli` release as a real global npm package (real `npm pack` / `npm
install --global` into a throwaway prefix — see `scripts/package-install.ts`)
under the label `live`, drives it to build a realistic home — a filesystem
bundle, a git bundle (with an in-bundle symlink at its root), a website
bundle, an npm bundle, three scheduled tasks (two granted, one left
ungranted) plus a manual task, and a synced native (fake) crontab — then
installs the CANDIDATE build OVER `live`, in the SAME prefix: an in-place
swap, the same thing a real `npm i -g akm-cli@…`/`bun add -g` upgrade does,
not a side-by-side install. It then runs the CANDIDATE against that home
(`migrate status`/`apply`, `bundle list` with every bundle confirmed enabled,
`search`/`show`, `task sync` dry-run and real — plain, no `--rebind`, as an
upgrading user actually runs it — executing the generated cron command and
confirming it ran the candidate, `task run`, `health`, `improve --plan`), and
finally installs a separate untouched copy of the PREVIOUS release and runs
it back against the candidate-written home to prove read-back still works.
It exists because no other suite drives a real prior release against a real
candidate build: the symlink-abort regression fixed in 0.9.17-alpha.3
(`d76af0a7b`) went undetected by the full unit/integration suite,
`tests/release-check.sh`, and the Docker matrix, because no fixture combined
a git bundle with an in-bundle symlink, a website bundle, an npm bundle, an
ungranted task, and a real crontab row in one home. Plain `task sync` proves
clean specifically because the candidate lands inside the SAME prefix the
previous release's scheduler rows already point at — no launcher path moves,
so nothing needs rebinding; a side-by-side install (a separate prefix) would
need `--rebind` instead, which is not what an in-place package-manager
upgrade does. `tests/integration/previous-release-corpus.test.ts` covers
persisted-data shapes read in isolation; this gate covers the whole CLI
surface driven end to end across two real installed releases.
Run it locally with:
```sh
bun run build
AKM_UPGRADE_REHEARSAL=1 TMPDIR=/tmp bun test --timeout=900000 tests/integration/upgrade-rehearsal/
```
Unset (the default), the suite logs one line and skips — it never fails
"inconclusively" (#795): every missing capability (no `npm` on `PATH`, no
network reachable to resolve/fetch the previous release, no candidate build)
throws naming the fix instead. Env overrides:
- `AKM_UPGRADE_FROM=<version>` — pin the previous release instead of
resolving the highest published stable version below the candidate's
`package.json` version.
- `AKM_CANDIDATE_TARBALL=<path>` — use an already-packed candidate tarball
(`tests/release-check.sh` passes `$PACKAGE_CANDIDATE`) instead of running
`npm pack` on the working tree (which requires `dist/cli.js` — run
`bun run build` first).
Fetched previous-release tarballs are cached under
`${TMPDIR:-/tmp}/akm-upgrade-rehearsal/previous-release/<version>/` so repeat
runs do not re-hit the network. It is wired into CI as the `upgrade-rehearsal`
job (`.github/workflows/ci.yml`) and into `tests/release-check.sh` right
after packing the release candidate.
## Coverage Gap Guide
This repo now has broad coverage across the major CLI, indexing, registry, and
semantic-search paths. Use this file as a current gap guide, not a greenfield
test plan.
### Areas With Strong Existing Coverage
- database and scoring (`tests/integration/db.test.ts`, `tests/integration/db-scoring.test.ts`, `tests/integration/fts-field-weighting.test.ts`)
- search/show CLI surfaces (`tests/integration/commands/search-cli-envelope.test.ts`, `tests/integration/commands/show.test.ts`, and the other `search-*`/`show-*` suites under `tests/integration/`)
- registry install/search/update/list flows
- workflow, vault, and wiki behavior
- semantic status, vector search, and embedding config behavior
- CLI error handling and output shaping
- Docker install validation
### Highest-Value Remaining Gaps
1. corrupt or version-mismatched DB fallback behavior during search
2. local model download and ONNX startup failures
3. partial embedding-generation failures during indexing
4. concurrent search/embedder behavior under load
5. semantic readiness reporting parity between `setup`, `search`, and `info`
6. broader platform CI for Alpine/musl, ARM, and Windows edge cases
### When Adding Tests
- prefer focused `bun:test` files under `tests/`
- use isolated temp config/cache/stash dirs
- cover the user-visible CLI behavior when the risk is output shaping or command routing
- cover internal units directly when the risk is scoring, metadata extraction, or persistence
### Useful Existing Suites To Extend
- `tests/integration/commands/search-cli-envelope.test.ts`
- `tests/integration/commands/show.test.ts`
- `tests/integration/vector-search.test.ts`
- `tests/integration/semantic-status.test.ts`
- `tests/setup-wizard.test.ts`, `tests/setup-scheduled-tasks.test.ts`
- `tests/integration/info-command.test.ts`
- `tests/integration/docker-install.test.ts`