cortexkit-test-support 0.1.0

Portable scratch storage, executable staging, and fenced daemon helpers for tests.
Documentation

cortexkit-test-support

Portable CortexKit test helpers. Use as a dev-dependency only, not in production. Licensed under MIT. This crate contains no application-store dependencies.

[dev-dependencies]
cortexkit-test-support = "0.1.0"

Scratch fixtures

ScratchDir::new("label") creates a unique directory beneath temp_dir()/cortexkit-tests/k<PID>-<kernel-start-identity>. Normal drop removes it. keep() and panic unwinding preserve it and print its path. Failed tests therefore retain their evidence. Unsupported process-identity platforms use an unknown start marker; such roots are never reaped on uncertain evidence.

Initialization sweeps process roots only when subc_os::process_identity::liveness positively reports Dead. Live owners, unknown or incompatible identities, malformed process-root names and legacy encodings survive. Non-process fixtures retain the 24-hour age rule; directories with young descendants and symlinks survive. The stable-bin executable directory is exempt from the entire sweep. wait_until_gone uses the same liveness API; unknown is never considered gone (including unsupported platforms).

Executables and spawn guard

ckdev_binary(built) publishes a content-addressed, read-only copy, never a hard link. ckdev_file_name maps ck-example to ckdev-example, ck to ckdev-ck, and preserves .exe. Windows staging ensures an .exe suffix. stage_test_binary is the narrower compatibility helper: only ck-* names are staged; other names pass through.

Unix staging uses a per-user root under /tmp, never $TMPDIR; Windows uses the system temp directory. A cp child writes Unix staging files so the test process holds no writable descriptor to an executable. Windows uses fs::copy. A private staging directory is atomically renamed into its content address; concurrent requests reuse one published file. Digests are checked before use, including reuse. Publications older than three days are pruned on a new build. Do not modify a publication. A source rebuild receives a different path.

use cortexkit_test_support::{ckdev_binary, dev_command, checked_output};
let placed = ckdev_binary("target/debug/ck-example");
let output = checked_output(dev_command(placed).arg("--version"))?;
# Ok::<(), std::io::Error>(())

dev_command refuses production names (ck and ck-*, case-insensitively), except Cargo's hashed test harnesses. Its return type is a standard Command: use checked_output rather than plain .output() when a failed child should be an error naming its exit code or Unix signal number and name. Explicit production-name exemptions take a caller-owned exact (test, stem) table and verify the calling test thread as well.

Consumers should call assert_test_binary_spawns(&[test_directory, ...]) from one test. This recursive lexical scan ignores comments and catches direct production artifact spawns, adjacent unwrapped spawns, and simple raw-path bindings. It is not a Rust data-flow analyzer: also use the runtime name guard. Missing directories and an empty Rust scan fail instead of silently passing.

On Unix, stable_executable and stable_executable_dir provide reusable content-addressed fake executable files in the exempt stable-bin directory.

Fenced daemon lifecycle

TestDaemonCommand refuses programs not named ckdev-* at spawn and refuses kill_on_drop(false). It clears inherited credentials and module identity, allows only tool/locale variables, and redirects HOME, all XDG roots, and TMPDIR into scratch. Call .fenced_root("EXAMPLE_STORAGE_ROOT", "storage") to add a caller-selected directory variable; fixed roots never name a consuming repository. These roots, including caller additions, are reapplied immediately before spawning: .env and .env_remove cannot bypass the daemon fence.

Unix consumers require python3 on PATH for lifecycle supervision, plus ps and cp. If python3 cannot be spawned, the error names that requirement. A pipe watcher owns the daemon's process group, terminates descendants on EOF (even when the harness is killed), and reaps the daemon on normal exit or panic. Drop is synchronous and bounded. Windows uses Tokio child kill-on-drop; it does not promise Unix process-group or orphan-descendant supervision. SubcDaemonBinary specifically builds the platform daemon ck-subc from the subconscious sibling checkout. Its build and try_build methods take the caller-selected dependency checkout names to include in the cache key. SubcDaemonBinary::command returns only the guarded builder; its path is private. fenced_command provides the ambient environment fence for other tools, but as a standard command its roots can subsequently be overridden by the caller. fenced_command_with_roots(program, scratch, &[("EXAMPLE_STORAGE_ROOT", "storage")]) adds caller-selected roots to that standard command. Extra subdirectories must be plain names, built-in variables cannot be replaced, and symlink roots are refused.

sign_test_binary(path, identity) signs best-effort on macOS. An explicit identity wins over CORTEXKIT_TEST_SIGNING_IDENTITY; no configured identity means no signing. Callers can explicitly request ad-hoc signing with Some(OsStr::new("-")). Ad-hoc signing uses no certificate or keychain signing identity. Already-valid signatures are reused. Sign built binaries before staging, not read-only publications. warm_exec_fenced runs --version through the fence and reports failures without making warm-up mandatory.

Sibling builds and privileged tests

Sibling cache helpers key builds by clean/dirty source content and caller-selected dependency revisions. sibling_build_revision(repo, revision, dependency_checkouts) and build_sibling_binary(repo, sibling, bin, dependency_checkouts) take the adjacent dependency checkout names explicitly; pass &[] for no dependencies. An OS file lock serializes building and publication across processes; the four most recently used revisions are retained. CachedSiblingBinary separately exposes the published build target and the staged executable path. sibling_cargo_build always uses --locked, so a stale sibling lock fails without rewriting it. The shared cache lives in the user's .cache/cortexkit-tests/sibling-target directory.

sibling_checkout resolves an adjacent repository, including from linked worktrees. Workspace resolution is runtime-based, not this dependency's source location: git rev-parse --show-toplevel in the current directory, or explicit CORTEXKIT_TEST_WORKSPACE_ROOT. sibling_target_dir uses that workspace's target/sibling-target, not the sibling's active target directory.

privileged_tests_enabled(test, needs) opts in only when CORTEXKIT_PRIVILEGED_TESTS=1, otherwise prints the skip reason. Callers return early when it is false. No privileged operations are performed by this crate.