Isolated Test Processes
Tests that spawn Deno can update the repository even when the test only means to
verify behavior. Deno commands resolve dependencies and may refresh deno.lock
metadata. Generated configs, output files, and golden updates can also leave
workspace files behind if cleanup is not tied to failure paths.
Repo Map
Use @commonfabric/test-support/isolated-deno for nested Deno commands that
need lockfile isolation.
For nested Deno checks that need a generated config:
import {
runDenoCheckWithTemporaryConfig,
} from "@commonfabric/test-support/isolated-deno";
That helper keeps the generated Deno workspace config in the repository root,
where Deno requires workspace members to be nested under the config directory.
It points Deno at a temporary copy of deno.lock, so dependency metadata writes
do not touch the real lockfile. It also removes the generated root config in a
finally block. The nested check uses frozen dependency resolution, so its
generated config must preserve the dependency graph in the checked-in lockfile.
Start with the root config and change only the compiler settings needed by the
test. Workspace member imports already come from their package configs. Do not
copy them into the generated root config.
For nested Deno commands that do not need a generated config:
import {
runDenoCommandWithTemporaryLock,
} from "@commonfabric/test-support/isolated-deno";
Pass an argument builder and place the temporary lock path in the child Deno
command's --lock flag.
Values
- A verification test must not change
deno.lockor repository files. - A verification test must use the dependency graph already recorded in
deno.lock. Dependency fetching belongs in setup before the test runs. - A generated config may change compiler options. It must preserve imports, workspace members, and other dependency declarations.
- If a test needs mutable inputs or outputs, put them under
Deno.makeTempDir()or an explicit test fixture copy. - If a generated file must briefly live in the repository root for tool
semantics, give it a unique dot-prefixed name and remove it in
finally. - If a test intentionally updates fixtures or goldens, gate the write behind an
explicit environment variable such as
UPDATE_GOLDENS=1. - Treat
Deno.Command(Deno.execPath())as a side-effect boundary. The child Deno process does not inherit the parent test runner's lockfile flags. - Spawn
Deno.execPath(), never the program name"deno". A name resolves throughPATH, which is a different Deno than the one running the test whenever the shell's Deno is not the version pinned inmise.toml. The two versions share one cache directory but read transpiled sources only from their own part of it, so a coverage profile written by one and reported by the other yields a report with every file missing. (In adeno compiled binaryDeno.execPath()is that binary rather than Deno, so code that has to run both compiled and under Deno needs its own way to find a Deno.) - A
--allow-run=denogrant does not permit spawningDeno.execPath(), because Deno resolves the allowlist entry throughPATHas well. Name the binary rather than widening the grant to a bare--allow-run. In a task line,--allow-run=$(deno eval "console.log(Deno.execPath())")computes it, becausedenoinside a task runs the Deno running the task whateverPATHsays —packages/test-support/src/isolated-deno.test.tspins that with a decoydenoon the child'sPATH. From a script, readDeno.execPath()directly, aspackages/dashboard/test/runner.tsdoes with--allow-run=${Deno.execPath()},git.
Common Tells
Risky tests often contain Deno.Command(Deno.execPath()), deno check,
deno task, deno install, generated deno.json files, generated build
outputs, or direct writes to paths under the repository root.