Skip to main content

write-tests

How to write clice integration tests (TypeScript/vitest) — fixture forms, Workspace/CliceClient API, snapshot workflow, hard rules and known pitfalls. Read BEFORE writing or modifying anything under tests/ or unittests/.

来源信息

仓库
clice-io/clice
最近来源活动
2026年10月11日 03:50
检测到的 SKILL.md 语言
英语
星标
1,344
分支
85

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
write-tests
description
How to write clice integration tests (TypeScript/vitest) — fixture forms, Workspace/CliceClient API, snapshot workflow, hard rules and known pitfalls. Read BEFORE writing or modifying anything under tests/ or unittests/.
# Writing clice integration tests The suite is TypeScript on vitest. Harness = the `@clice/tools` workspace package (`tools/`, session machinery in `tools/client/session.ts`); each suite binds it in its own fixture file (`tests/integration/fixtures.ts`, `tests/snap/fixtures.ts`). Tests live in `tests/integration/<area>/*.test.ts`; tests of the tooling itself in `tests/tools/`. Run: `cd tests && CLICE_EXECUTABLE=../build/RelWithDebInfo/bin/bin/clice npx vitest run --config integration/vitest.config.ts <file>`; gates: `npm run check` at the repo root (tsc strict + ESLint, zero tolerance). ## Choosing a fixture form 1. **All tests target one data workspace** (`tests/data/<name>`): the bound form — zero boilerplate, teardown fully automatic. ```ts import { cliceTest, expect } from "../fixtures.ts"; const test = cliceTest("document_links"); test("links with pch", async ({ client, workspace }) => { const [uri] = await client.openAndWait("main.cpp"); // workspace-relative ... }); ``` 2. **Anything else** (several servers, temp workspaces, custom argv, per-test options): the `session` factory — the test's resource manager. Everything it vends is reclaimed in teardown (shutdown gate, anomaly gate, directory removal); never write try/finally cleanup. ```ts import { expect, test } from "../fixtures.ts"; test("rebuild after restart", async ({ session }) => { const ws = session.tmpdir(); // auto-removed Workspace ws.write("main.cpp", "int main() {}\n"); // relative path, auto-mkdir ws.writeCDB(["main.cpp"]); const first = await session.spawn(ws).initialize(ws); await first.openAndWait("main.cpp"); await first.shutdown(); // explicit mid-test shutdown is fine const second = await session.spawn(ws).initialize(ws); ... // teardown owns `second` }); ``` Variants: `session("name", opts)` (data workspace, locked + initialized), `session.tmp()` (tmpdir + un-initialized server). Options: `initializationOptions`, `allowAnomaly` (ONLY for tests that deliberately crash workers — assert on the anomaly explicitly), `drainStderr: false` (backpressure tests), `args`, `socketPort`. 3. **Snap tests** (feature output): don't write assertions at all — add a fixture to the corpus `tests/snap/<feature>/` and the snap suite (`tests/snap/snap.test.ts`, domain logic in `tools/snap/`) pins the reply from the paths its `verify:` mode asks for: inspect (`clice inspect`, no server) and server (a real server on a materialized throwaway workspace). Position-dependent fixtures carry `§(name)` annotations (see `@clice/tools/snap/annotation`). A fixture is a single `.cpp` or a subdirectory entered through its `main.cpp` — one multi-file unit whose sibling sources (module interfaces, headers) belong to it; files carrying markers participate, the rest are support. A fixture that documents a capability lives in a section directory as `<section>/NN_name.cpp` (or `<section>/NN_unit/main.cpp`): the directory is the doc page's generated-region key, the two-digit number orders the item within the section. Its header has this exact shape: ```cpp /// # Qualified name /// /// - status: supported /// - issues: clangd#710 /// - verify: both /// - snap: separate /// - config: {"show_aka": false} /// - diagnostics: expected /// - indexing: true /// - flags: ["-std=c++23"] /// /// The hover card shows the enclosing namespace and class scope /// /// Optional further paragraphs describe reader-visible behavior. // snap: Maintainer notes about the harness follow the header directly. // snap: Every line in the note repeats the prefix. ``` The name is a noun phrase of at most five words, with no dash details or terminal punctuation. The first prose paragraph is the card summary: one sentence, capitalized (unless it begins with code), with no terminal punctuation. Further prose is reader-facing only: behavior the user can observe, never internal class names, source paths, or mechanisms. Put harness details — why a fixture is server-only, why inspect and server differ, or what a snapshot pins — in the adjacent `// snap:` block instead. `issues:` cites only issues that map to the capability and are still current design-level items; a stale, version-specific bug report ("Clangd 11 RC1 …") is padding, not evidence. The header is the first content in the file. Edge-case fixtures without a doc header stay at the corpus root, un-numbered; any explanatory prologue there uses ordinary `//` comments, not `///`. Accept intentional changes with `UPDATE_SNAPSHOTS=1 npm run snap` and review the diff like code; a shared-snapshot mismatch on the server side is a real divergence, not something to update over. Fixture meta (strict — unknown keys are errors, validated by `tools/snap/corpus.ts` and `tools/docs/feature.ts`), declared as `- key: value` lines in the leading `///` header. The only keys, in fixed order, are `status`, `issues`, `verify`, `snap`, `config`, `diagnostics`, `indexing`, `flags`; omit unused optional keys without reordering the rest. `status` is required and is `supported`, `partial` or `unsupported`. `status` and `issues` render into the docs; the rest drive the suites: - `verify: both` (default) runs inspect and server; `inspect`/`server` runs only that path, which then owns the plain `<name>.snap.yml`. - `snap:` relates the two paths of a `verify: both` fixture. `shared` (default): byte-identical, one `<name>.snap.yml`. `separate` (with a `// snap:` comment explaining why): a genuine known difference, pinned as `<name>.inspect.snap.yml` / `<name>.server.snap.yml`. `skip`: a known-wrong divergence — the fixture runs nowhere and keeps no snapshot until fixed. `skip` documents a divergence that predates your change — it is never a way to get your own regression past the suite. - `config: {...}`: feature-options overlay; the snapshot pins BOTH halves (`default:` / `configured:` blocks) on both paths. - `diagnostics: expected`: the fixture deliberately does not compile cleanly — unexpected diagnostics fail, and so does a clean compile under the declaration. - `indexing: true`: enables background indexing on the server path (off by default for speed). - `flags: [...]`: extra compile flags, appended to the corpus-wide flags in `tests/snap/<feature>/corpus.json`. `UPDATE_SNAPSHOTS=1` updates everything in one run: inspect tests run first and own shared bodies; the server side can only update its own variants. Fixture doc headers feed the generated feature pages, but do not regenerate or translate them per edit — that happens once at the end of the branch, delegated (the docs skill's "Syncing docs at the end of a branch"). ## API cheat sheet `Workspace` (`@clice/tools/workspace`): `path(rel)` `uri(rel)` `write` `read` `exists` `mkdir` `rm` `writeCDB(files, {extraArgs, std})` `writeEntries` `generateCDB()` `pinCacheDir()` and cache inspection (`pchFiles()` `pcmFiles()` `tmpFiles()` `readCacheJson()`). Raw string path: `ws.root`. Exotic fs ops: `node:fs` + `ws.path(...)`. `CliceClient` (`@clice/tools/client`): after `initialize(ws)` all paths may be workspace-relative. Requests: `hoverAt` `definitionAt` `referencesAt` `completionAt` `documentLinks` `foldingRanges` `semanticTokensFull` `inlayHints` `formatDocument` ... Documents: `open` `openAndWait` `change` `save` `close`. Waiting: `armDiagnostics` (arm BEFORE the trigger) / `waitDiagnostics` / `waitForRecompile` / `waitForIndex` / `waitForReference`. Asserts: `assertNoErrors` `assertHasErrors` `assertCleanCompile` `assertNoAnomaly` `errors`. Lifecycle: `shutdown()` `killServer()` `assertExitedCleanly()`. Custom protocol (typed): `queryContext` `currentContext` `switchContext` `poll` `stats` `logFlood`; raw wire: `sendRequest(TypeOrMethod, params, token?)`, `onNotification`. Custom protocol types live in `@clice/tools/protocol` — NEVER redeclare them locally (the VSCode extension shares them). Timing: use `sleep`, `MTIME_GRANULARITY`, `SETTLE_TIME`, `IDLE_TIMEOUT` from `@clice/tools/client` — never bare magic-number sleeps, and prefer deterministic waits (`poll("cdb")`, `armDiagnostics`) over sleeping. ## Hard rules - **Never** `.skip` / `.fails` / `.todo`, never weaken an assertion to get green, never add retries around flakiness — fix the root cause. - URIs in server replies are validated strictly (see `@clice/tools/snap/snapshot` normalizeFileUri). Do not "normalize away" a malformed URI; a raw path or unencoded space is a server bug. - `allowAnomaly` requires the test to assert the expected anomaly itself. - Comments: `///` for doc comments, `//` inline; explain constraints the code can't show, nothing else. Keep tests concise: descriptive test names, no large comment blocks explaining layout or expected behavior. - Same-workspace exclusivity across files comes from the session lock — never touch `tests/data/*` outside a session, and never run two suites concurrently. Probes and experiments copy a workspace to a temp dir first; a server under test whose `.clice` gets deleted underneath it fails every PCH build. - Tear down servers by the PIDs you recorded (and their subtree) — never `pkill` by name. A pattern sweep kills a sibling run's servers, and those deaths look exactly like the bug being hunted. ## Known pitfalls (each cost a real debugging session) - JS numbers mangle 64-bit values: cache.json dep hashes need `JSON.rawJSON`/BigInt-reviver round trips (see persistent_cache.test.ts); `clice query` answers carry symbol ids as `#<hex>` strings for this reason. - Python-style truthiness does not port: `expect([]).toBeFalsy()` fails — assert `length` explicitly. - LSP positions are UTF-16 code units; ASCII fixtures keep them equal to string indices — non-ASCII fixtures need real conversion. - `Diagnostic.message` is `string | MarkupContent` — narrow before `.includes`. - Child stdout/stderr backpressure is real: an undrained pipe blocks the server; `spawnSync` has a 1MB default `maxBuffer` that silently kills children. - Every snap corpus directory carries a `.clang-format` with `DisableFormat: true`. Without it `pixi run format` wraps long `///` header lines (meta keys vanish silently) and reflows the code (every snapshot drifts). - `§` is always a marker: a literal `(` right after it is written `§()`, and fixture comments never contain a literal `§` — a full-dump canary once collapsed to one line because of a `§` in its own comment. - Include-completion fixtures use a corpus-unique include prefix: the pixi env's `$PREFIX/include` leaks into the search path and `-nostdinc` does not stop it. Completion statements end with `;` — an unterminated statement drags the next marker into recovery context. - Windows file timestamps tick at ~15.6 ms: two touches inside one tick stat equal, so a test that needs distinct mtimes sets them explicitly. - LMDB reads come from a resident snapshot: `advance` after a write before expecting visibility, and a test double wrapping `BlobDatabase` must forward `advance`/`retire`/`grow` or the snapshot never moves. Probe blob keys go through the path pool's canonical spelling (Windows 8.3 short names hash differently). - `waitForIndex` cannot wait on a declaration-only name: `search_symbols` skips symbols without a definition. - Tester's module compile once wrote no BMI at all (the syntax-only overload cleared the output file) and module tests stayed green on lexical tokens alone — when touching the Tester compile path, check that a PCM is really produced. - Under `-ffreestanding` clang recognizes no library builtins (`getBuiltinID` is 0): logic keyed on builtin recognition behaves differently in unit tests than under a hosted `clice inspect`. - A unit test runs in zest's worker process: one that crashes is reported CRASHED under its own name, with what it printed, and the run goes on. A debugger attached to `unittests` sees only the runner — take a backtrace with `--no-isolation --test-filter=Suite.Case`, which runs the test in-process (`lldb --batch -o run -k "bt 40" -- ...`). - The Tester's driver is the `clang++` on PATH: the cross-built legs (arm64 Linux, x64 macOS, both Windows; `build_os` in `test.yml`) run their suites with only the `test-run` env active, so on arm64 Linux they compile against the runner's system libstdc++, not conda's, and on Windows with the runner's clang, which targets MSVC. A wrapper script named `clang++` that execs the pixi clang with another `--gcc-install-dir` reproduces such a leg locally. ## C++ unit tests (zest) - zest constructs a fresh suite object for every `ZEST_CASE`: no `reset()` / `clear()` helpers, shared initialization goes in the suite's constructor (`ZEST_SUITE(Name)` declares `struct NameTEST`). A default-constructed `Workspace` already carries the real configuration defaults (`Config` guarantees it; `BornValidDefaults` pins it) — no defaults initialization is needed. - A check takes one expression and shows the operands of its top-level comparison: `ZEXPECT(a == b)`, `ZASSERT(result)`. `&&`, `||`, bitwise operators and shifts at the top level do not compile — split the check, or parenthesize it to check one bool. - A failed `ZASSERT` ends the test's worker process at once, without unwinding: it works the same in a coroutine, a helper or another thread, but the suite's destructor does not run — cleanup that must happen then goes in a `kota::zest::FatalHook`. Under `--no-isolation` it ends the whole run. - Filter with `--test-filter=<name>`; a bare positional name exits 1 silently. - `select("m")` looks up a point while `§(m)⟦...⟧` registers only a range: a test that needs both annotates both (`§(m)⟦§(m)foo⟧`). ## Tooling code (`tools/`) - Framework > mature library > handwritten: vitest's own concurrency (`test.concurrent`, `maxConcurrency`) over a custom pool, `jsdiff` over a hand-rolled diff, `util.parseArgs` over a custom argv parser; say why a library is trustworthy (downloads, maintenance, already in the tree). The one exception is byte-level twin code — `yamlStr` mirroring the C++ zest escaping, the C++/TS annotation parsers — whose contract is byte identity and which a library would break. - `tools/` runs under node's strip-only TypeScript: erasable syntax only (no constructor parameter properties, no enums). - Naming: no abbreviated file names, no decorative section comments. New modules join the subsystem directory they belong to (`tools/client`, `tools/snap`, `tools/docs`, `tools/protocol`, each a subpath export of `tools/package.json`); a new directory needs a new subsystem, not a new file. Framework and domain logic live in `tools/`; test directories keep only what must be there (package.json, tsconfig, thin vitest glue).
在 GitHub 查看