Last reviewed: 2026-09-14

Direct answer

Resource leak testing for coding agent patches should compare the same lifecycle on the base revision and the proposed revision. Run a focused test that acquires a resource, exercises it, releases it, and exits normally. Keep the runtime, test input, detector configuration, and repetition count constant. A repeatable leak on the proposed revision, paired with a clean base revision, gives the coding agent a specific regression to repair.

Choose a detector that matches the resource. Clang’s LeakSanitizer documentation describes end-of-process memory-leak detection for instrumented native binaries. Python’s tracemalloc documentation shows how to compare allocation snapshots by line or traceback. The goleak package documentation covers per-test and package-wide checks for goroutines left running. Jest’s CLI documentation describes targeted open-handle diagnosis for test processes that do not exit cleanly.

Do not treat a passing functional assertion as proof that cleanup succeeded. Conversely, do not treat one increase in memory or one delayed shutdown as proof of a leak. Establish a clean baseline, reproduce the candidate-only signal, retain the detector output, and verify that the repair removes the signal without weakening the behavior test.

Who this is for

This workflow is for developers, CI maintainers, and reviewers handling agent-written changes to workers, clients, caches, connection pools, timers, background tasks, or shutdown code. It is especially useful when a patch passes ordinary unit tests but leaves a process alive, retains allocations across repeated operations, or strands goroutines after the test package finishes.

The method assumes that a human owns the merge decision. A coding agent can investigate stacks and implement cleanup, but the gate must define what counts as clean, what counts as detector failure, and which evidence must survive the run.

Key takeaways

  • Compare the base and proposed revisions under the same detector contract.
  • Separate clean, leak, harness_error, and inconclusive outcomes. A broken detector is not a clean run.
  • Exercise an explicit acquire-use-release boundary and allow normal process shutdown.
  • Preserve a small, sanitized evidence record plus the full detector artifact.
  • Use focused diagnostic modes where their cost or execution model makes broad use impractical.
  • Reject repairs that merely force process exit, broaden a suppression, or remove the assertion that exposed the lifecycle defect.
  • Rerun both the minimal reproduction and the related test suite before merge.

Sources checked

  • LeakSanitizer from Clang documents runtime memory-leak detection, integration with AddressSanitizer, standalone instrumentation, exit-time detection, and suppression controls.
  • tracemalloc from the Python standard library documents allocation tracebacks, statistics, early tracing, snapshot comparison, and offline snapshot storage.
  • goleak from Uber Open Source documents unexpected-goroutine checks, package-wide verification, options for known goroutines, and the parallel-test limitation of per-test verification.
  • Jest CLI options document open-handle collection, serial execution in that mode, asynchronous-resource tracing, its debugging cost, and why forced exit is an escape hatch rather than cleanup.

Contract details to verify

Define the leak gate before running it

Write down six properties before asking an agent to diagnose anything:

  1. Lifecycle boundary: Name the operation that acquires and releases the resource, such as starting and stopping a worker.
  2. Resource scope: State whether the gate examines native allocations, Python allocations, Go goroutines, or open asynchronous handles.
  3. Baseline: Identify the base revision and require it to complete under the same detector configuration.
  4. Repetition: Choose a fixed warm-up count and measured cycle count. Five measured cycles are a practical starting point, not a statistical guarantee.
  5. Outcome classes: Reserve distinct wrapper exit codes for clean execution, detected leaks, invalid baselines, and harness failures.
  6. Evidence: Require the test target, revision, detector version, configuration digest, attempt number, exit status, and artifact path.

This contract prevents two common reasoning errors: blaming a patch for a pre-existing leak and reporting detector startup failure as a passing test.

Happy-path operator workflow

  1. Select the smallest test that crosses the suspected shutdown boundary. Confirm that its functional assertions pass on the base revision.
  2. Build the base and proposed revisions with matching runtime and instrumentation settings. Use the same test data and concurrency mode.
  3. Run one unmeasured warm-up when the runtime or application performs one-time initialization. Record that the warm-up happened.
  4. Run the detector on the base revision for the fixed measured count. If every run completes and no unexpected resource remains, store a clean baseline artifact.
  5. Run the same detector sequence on the proposed revision. Do not change timeouts, suppressions, or test selection between revisions.
  6. If the proposed revision is also clean, run the related package or subsystem suite. Preserve both results as merge evidence.
  7. If the proposed revision leaks, give the coding agent the minimal reproduction command, the clean baseline summary, the failing summary, and the symbolized top frames.
  8. After a repair, rerun the exact reproduction first. Then rerun the broader suite and inspect the diff to ensure the agent repaired ownership or cleanup instead of weakening detection.

A successful result is therefore more than a green test: the detector completed, the base was valid, the proposed revision was clean, functional behavior remained intact, and the evidence artifacts were retained.

Error-path operator workflow

Treat operational failures as their own path:

  1. If instrumentation cannot load, symbols are unavailable, or the wrapper cannot start the test, emit harness_error. Repair the test environment before changing product code.
  2. If the base revision leaks, emit baseline_invalid. Determine whether the leak is known, isolate a smaller target, or document a narrowly reviewed suppression. Do not attribute the existing signal to the patch.
  3. If both revisions fail intermittently, emit inconclusive. Preserve every attempt and reduce nondeterminism before assigning the repair.
  4. If the proposed revision times out, retain the partial output but do not call the run clean. Some detectors need normal shutdown to produce their final report.
  5. If a coding-agent repair adds forced exit, broad filtering, or an unconditional suppression, reject it unless the underlying resource is demonstrably released and the exception has a documented owner.
  6. If the focused check becomes clean but the related suite fails, keep the change out of the merge path. Cleanup ordering can affect callers outside the minimal reproduction.

Apply the detector at the correct boundary

For a native test binary, AddressSanitizer can include leak detection on supported platforms. The Clang source explains that the leak phase occurs near process exit, so the test must reach normal termination:

clang -fsanitize=address -g src/worker.c tests/worker_test.c -o build/worker_test
ASAN_OPTIONS=detect_leaks=1 ./build/worker_test

Use instrumented binaries for testing rather than production deployment. If CI terminates the process before its final detection phase, classify the run as incomplete instead of interpreting the missing report as clean.

For Python-owned allocations, start tracing before the lifecycle under examination. Comparing snapshots identifies lines or tracebacks whose traced allocation totals grew:

import tracemalloc

tracemalloc.start(25)
start_worker()
stop_worker()

before = tracemalloc.take_snapshot()
for _ in range(5):
    start_worker()
    stop_worker()
after = tracemalloc.take_snapshot()

for delta in after.compare_to(before, 'lineno')[:10]:
    print(delta)

A positive snapshot delta is a lead, not an automatic merge failure. Initialization caches and other retained objects may be intentional. Reproduce the growth across controlled lifecycle cycles and inspect the allocation traceback before classifying it.

For Go packages, package-wide verification is a useful default when tests run in parallel:

func TestMain(m *testing.M) {
    goleak.VerifyTestMain(m)
}

The goleak documentation warns that per-test VerifyNone cannot associate goroutines with individual tests when t.Parallel is active. Use package-wide verification for parallel suites, then isolate tests individually when a package-level failure needs attribution.

For a Jest process that refuses to exit, run a focused diagnostic command:

npx jest path/to/worker.test.js --detectOpenHandles

That option runs tests serially and carries a significant performance cost, so use it for diagnosis or a deliberately scoped gate. Do not replace cleanup with --forceExit; forced termination can hide the handle that should have been closed.

Keep logs useful and sanitized

Emit an allowlisted summary rather than copying the complete process environment, payloads, or arbitrary command output into a log record. A compact record can look like this:

schema_version: leak-evidence-v1
run_id: run-1042
revision: abc1234
test_target: worker_shutdown
detector: goleak
detector_version: v1.3.0
attempt: 3
outcome: leak
exit_code: 20
leak_kind: goroutine
leaked_units: 1
top_frame: worker.loop
duration_ms: 842
baseline_artifact: artifacts/leaks/base.json
result_artifact: artifacts/leaks/patch.json
config_digest: sha256:7c2f9a

Normalize workspace locations to repository-relative paths. Retain numeric counts and project-owned stack frames, but exclude environment dumps, request bodies, user data, and unrelated process arguments. Keep the full symbolized detector report as a separate artifact so the summary stays stable while investigators can still inspect the evidence.

Use an explicit acceptance rule

Approve the leak gate only when the detector initialized, the base revision produced a valid result, the repaired revision completed cleanly for the defined cycle count, and the original behavior assertions still pass. Any new suppression must be narrow, reviewed, and represented in the configuration digest. This rule makes the result reproducible for both the coding agent and the human reviewer.

Failure modes

  • The process is killed before final analysis. LeakSanitizer performs an extra detection phase near process exit. A timeout or abrupt termination can prevent a conclusive report.
  • Warm-up allocations are mistaken for a regression. Python imports, traceback support, and application caches can create one-time growth. Take the measured baseline after controlled initialization and compare repeated lifecycle cycles.
  • Parallel goroutines are attributed to the wrong test. A per-test goroutine check can observe work owned by another parallel test. Use package-wide verification, then isolate the failing test for attribution.
  • Forced exit turns a hang green. A runner may terminate successfully while a timer, connection, or other handle is still live. Treat forced exit as diagnostic debt, not a fix.
  • Suppressions become a blanket exception. A wildcard that matches an entire module can hide new leaks. Match the narrowest known location, record the owner, and review changes to the suppression set.
  • Only aggregate memory is retained. A byte count without revision, cycle, detector configuration, or stack evidence gives the agent little basis for a safe repair.
  • The agent edits the test instead of ownership code. Removing a shutdown assertion or reducing repetitions may erase the symptom while leaving the leak. Review the diff and rerun the original reproduction unchanged.
  • The minimal test is the only verification. A cleanup change can alter shared lifecycle ordering. Run related tests after the focused leak check succeeds.

FAQ

Does increasing process memory prove there is a leak?

No. A process can retain caches or allocator-managed memory without violating the lifecycle contract. Use a detector that exposes retained allocations or live resources, compare equivalent base and proposed runs, and look for repeatable growth tied to the exercised path. For Python, snapshot differences and tracebacks help locate the growth, but a human still has to decide whether retention is unintended.

Should every test run in the most expensive diagnostic mode?

Not necessarily. Scope the normal merge gate to lifecycle-sensitive tests and use deeper diagnostics when that gate fails. Jest explicitly describes open-handle collection as a debugging option with significant overhead and serial execution. A scheduled broader leak suite can complement focused pull-request checks without making every test pay the same cost.

What should a coding agent receive with a leak task?

Provide the exact test target, the base and proposed revision identifiers, one reproduction command, detector and runtime versions, cycle count, outcome classification, clean baseline summary, failing summary, and relevant project-owned stack frames. State which behavior must remain unchanged. Exclude unrelated logs and sensitive runtime data.

Are suppressions ever acceptable?

Yes, but they should be exceptions with reviewable scope. LeakSanitizer supports patterns that match functions, source files, or modules. Use the narrowest pattern that represents a known external or deferred issue, record why it exists, and ensure the patch does not silently broaden it.

When is the repair complete?

The repair is complete when the original reproduction reaches normal shutdown without the unexpected resource, repeated measured cycles remain clean, the behavior test still passes, and the related suite shows no cleanup-order regression. A single retry that happens to pass is insufficient evidence.

Reader next step

Choose one test that starts and stops a worker, client, or background task. Add the matching detector, define the four outcome classes, capture a clean base result, and run the proposed revision with the same measured cycles. Store the sanitized summary and full detector output as CI artifacts.

When the gate finds a repeatable regression, feed that evidence through the CI repair-loop workflow . If cleanup depends on repository automation, align it with the deterministic lifecycle-hook guide before enabling the gate for merge decisions.