用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/gravity-ui/create --skill writing-unit-tests命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | writing-unit-tests |
| description | Use when writing or editing *.test.ts files in this repo (@gravity-ui/create). |
Reference skill distilled from this repo's AGENTS.md files. Native node:test, colocated *.test.ts, never touch real disk.
| Rule | Why |
|---|---|
Use test.describe + test(name, (t: TestContext) => ...) with t.assert.* | Not top-level node:assert/strict + nested await t.test() — lint/prettier normalizes to the former anyway |
Colocate foo.test.ts next to foo.ts | Repo convention, no __tests__ dirs |
Generators: never call real node:fs | Always take a FileSystem param; tests pass memfs.ts |
Generator tests: runGenerators({dryRun: true}), assert on captured files | Never write to real disk in a test |
nvm use && npm test after any behavior change | .nvmrc pins Node; wrong Node breaks the TS loader |
| Chicago school: assert on state (captured files, return values), not call-args | London-style interaction mocks aren't used anywhere in this repo's tests |
Verify state/output of the code under test, not "was function X called with args Y" on its collaborators. Concretely in this repo:
mock.module('node:fs', ...) in destination.test.ts mocks a builtin, but still fits Chicago school: it substitutes a working fake (backed by memfs from the memfs npm package) at the one true I/O boundary, and the tests assert on validateDestination's return value (state) — never on whether existsSync was called. The distinction is state-vs-interaction verification, not "mocking is banned."t.mock.fn/t.mock.method + assert(called with...)) for code that takes a FileSystem param — pass a real memfs instance and assert on what it captured instead.node:fs directlyeslint.config.js's no-restricted-imports scopes which file(s) may import node:fs/node:fs/promises directly — check that rule to find the current file(s). When testing one of those:
mock.module('node:fs', ...) only rebinds modules that import the mocked specifier directly. Once such a module has been evaluated and its top-level import {existsSync} from 'node:fs' linked, mock.restore() + a fresh mock.module() does not rebind it — Node's ESM cache never re-evaluates the module for the same specifier. Remocking per test silently reuses the first test's mock in every later test, with no error.
Working pattern:
node:fs once at module top-level, backed by a single memfs() volume./case-<name>/cwd/...) inside that shared volume instead of remocking per test.When a module mixes pure decision logic with unavoidable I/O (terminal prompts, p.note formatting), pull the decision logic into a small pure function over plain data and unit-test that directly — e.g. buildNextSteps(model, cwd) extracted from main() in src/index.ts. Don't try to test the I/O wrapper.
{dryRun: true} + memfs.node:fs per test instead of once at module top-level.await t.test() instead of flat test.describe/test.mock.module-ing utils/isModulePackage.js while testing generators/base.ts) instead of driving it with real input data. These are plain functions over the model/args already in scope — construct a model that produces the branch you want, don't intercept the import.