Write Tests

SkillDev tools

Lets your agent write tests for Syncpack commands and validation logic using the TestBuilder pattern.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Write Tests skill

About this capability

Write tests for Syncpack using the TestBuilder pattern. Use when adding tests for commands, validation logic, or any new functionality. Covers TestBuilder API, assertion patterns, and common test scenarios.

What this skill tells your AI

The instructions your AI receives, as published by jamiemason/syncpack in .claude/skills/write-tests/SKILL.md and read by ahel’s review.

Golden rules

  • Use TestBuilder — never construct Context manually.
  • Assert with expect(&ctx).to_have_instances(vec![...]) — never index ctx.instances.
  • Async-first: #[tokio::test] + .run().await is the canonical entry.

TDD

  1. Read 2-3 tests in the same *_test.rs and copy the pattern
  2. Write failing test → just test → confirm RED
  3. Ask before implementing
  4. Implement minimal code → GREEN → just format

Quick start

use {
  crate::{
    instance::{FixableInstance::*, InstanceState, SuspectInstance::*, UnfixableInstance::*, ValidInstance::*},
    test::{
      builder::TestBuilder,
      expect::{expect, ExpectedInstance},
    },
  },
  serde_json::json,
};

#[tokio::test]
async fn pinned_version_replaces_anything_different() {
  let ctx = TestBuilder::new()
    .with_package(json!({
      "name": "package-a",
      "version": "1.0.0",
      "devDependencies": {"foo": "workspace:*"}
    }))
    .with_version_group(json!({
      "dependencies": ["foo"],
      "pinVersion": "1.2.0"
    }))
    .run()
    .await;
  expect(&ctx).to_have_instances(vec![
    ExpectedInstance {
      state: InstanceState::valid(IsLocalAndValid),
      dependency_name: "package-a",
      id: "package-a in /version of package-a",
      actual: "1.0.0",
      expected: Some("1.0.0"),
      overridden: None,
    },
    ExpectedInstance {
      state: InstanceState::fixable(DiffersToPin),
      dependency_name: "foo",
      id: "foo in /devDependencies of package-a",
      actual: "workspace:*",
      expected: Some("1.2.0"),
      overridden: None,
    },
  ]);
}

Sub-module organisation

Group related scenarios under nested modules — see pinned_test.rs, catalog_defs_test.rs:

mod local {
  use super::*;
  #[tokio::test] async fn refuses_to_pin_local_version() { ... }
}

mod normal {
  use super::*;
  #[tokio::test] async fn an_already_pinned_version_is_valid() { ... }
}

mod registry_updates {
  use super::*;
  #[tokio::test] async fn def_marked_outdated_when_registry_has_newer_version() { ... }
}

File location

Test typeLocation
Version-group behaviour (pin, ban, ranges …)src/version_group/<group>_test.rs
Catalog discovery wiringsrc/version_group/{catalog,bun_catalog}_test.rs
Fix mutationssrc/commands/fix_test.rs
Format passsrc/visit_formatting/format_test.rs
Other unit testsCo-located: src/foo.rssrc/foo_test.rs

Builder methods

Source of truth: src/test/builder.rs.

MethodPurpose
.with_package(json!({...}))Add one package.json
.with_packages(vec![...])Add many
.with_version_group(json!({...}))Add one version group
.with_version_groups(vec![...])Add many
.with_semver_group(json!({...}))Add a semver group
.with_config(json!({...}))Base config (e.g. customTypes, dependencyGroups)
.with_strict(bool)Strict mode (Suspect → error)
.with_subcommand("update")Override subcommand (default: lint, or update if registry set)
.with_pnpm_catalogs(yaml)Inject pnpm-workspace.yaml; implies pnpm PM
.with_bun_catalogs(json!({...}))Synthetic Bun root with /catalog, /catalogs/{n}; implies Bun PM
.with_bun_workspaces_catalogs(json!({...}))Same, nested under /workspaces/
.with_{pnpm,bun,npm,yarn,unknown}_package_manager()Force PM detection
.with_registry_updates(json!({"react":[...]}))Mock npm registry; implies subcommand=update
.with_update_target(UpdateTarget::Minor)Bound update target
.run().awaitContextPrimary end-to-end (full pipeline through disk + discovery)
.build()Sync, no visit — context-wiring tests
.build_and_visit_packages()Sync + visit_packages — fix tests, older suites
.build_and_visit_formatting()Sync + visit_formatting
.build_with_registry_and_visit().awaitSync wiring + async registry mock + visit

ExpectedInstance fields

ExpectedInstance {
  state: InstanceState::fixable(DiffersToPin),  // valid / fixable / unfixable / suspect
  dependency_name: "react",                     // = `internal_name` (alias-aware)
  id: "react in /dependencies of package-a",    // {dep} in {/path} of {package_or_yaml}
  actual: "17.0.0",                             // raw specifier on disk
  expected: Some("18.0.0"),                     // None = ignore; Some("") = remove
  overridden: None,                             // semver-group override target, if any
}

id location examples:

  • /dependencies, /devDependencies, /peerDependencies
  • /version of package-a (local version)
  • /packageManager, /engines/node
  • /catalog of pnpm-workspace.yaml, /catalogs/<name> of pnpm-workspace.yaml
  • /customVersion, /custom/config/version (via customTypes)

Patterns

patterns.md: banned, pinned, sameRange, pnpm catalogs, bun catalogs, semver ranges, registry updates.

Fix tests

src/commands/fix_test.rs builds with .build_and_visit_packages() then runs fix::run(ctx, &SilentReporter, &disk). dry_run = true is the default (set by mock::config_from_mock), so is_dirty() and post-fix contents stay observable. Set ctx.config.cli.dry_run = false only when asserting writes through a recording MockDiskIo (see pnpm_fix_writes_yaml_to_disk).

Common mistakes

WrongRight
#[test] fn foo() + .run().await#[tokio::test] async fn foo()
use crate::instance_state::*use crate::instance::*
"pinned": "1.0.0""pinVersion": "1.0.0"
Context { ... }TestBuilder::new()...
ctx.instances[0]expect(&ctx).to_have_instances(vec![...])
.build() then check states.run().await (or .build_and_visit_packages() for sync)
Missing SuspectInstance::* importImport all 4: FixableInstance::*, ValidInstance::*, SuspectInstance::*, UnfixableInstance::*

Running

just test                            # all
cargo test pinned_test               # pattern match
cargo test test_name -- --nocapture  # with stdout

Reference tests

  • src/version_group/banned_test.rs — banned + custom types
  • src/version_group/pinned_test.rs — sub-modules, semver-group interaction
  • src/version_group/same_range_test.rs — range satisfaction
  • src/version_group/catalog_test.rs — pnpm catalogs
  • src/version_group/bun_catalog_test.rs — bun catalogs (sync .build())
  • src/version_group/preferred_semver_test.rs — registry updates, update targets

Signals

GitHub stars
2k
Forks
73
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
write-tests-jamiemason
Source
github.com/jamiemason/syncpack