ic-testkit 0.14.3

PocketIC-oriented test utilities for IC canister tests
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
# ic-testkit package changelog and migration guide

This file ships in the crate archive so upgrades can be completed without the
repository checkout. The complete historical changelog remains at
<https://github.com/dragginzgame/ic-testkit/blob/main/CHANGELOG.md>.

## 0.14.3

This patch release simplifies benchmark processing and Wasm cache finalization.
Public APIs and persisted layouts are unchanged; existing `0.14` callers need
no source migration. Repository-owned format identifiers remain `v1`.

- Benchmark pairing borrows markers until constructing owned spans or
  diagnostics. Aggregation stores group identity only in map keys, and
  comparison and Markdown lookups borrow existing row keys. Nested pairing,
  suite boundaries, deterministic ordering, overflow rejection, missing rows,
  and duplicate-row precedence retain their existing behavior.
- Cold builds and missing exact-cache reconstruction use one success/failure
  finalization path. Successful entries survive, incomplete entries are cleaned
  up, and original errors, cleanup diagnostics, and timings are preserved.
- Canister installation moves the existing label into failure diagnostics
  instead of cloning it before every install. Original causes and caller-owned
  PocketIC instances remain available after failures.
- Digest text has one hexadecimal formatter that writes directly to its
  destination. The owned-string API and persisted stamps, manifests, and cache
  paths keep the same lowercase, zero-padded representation.

Twenty-five benchmark integration checks, one overflow check, three Wasm
cleanup/reconstruction checks, four artifact handoff checks, two live PocketIC
16 install checks, six digest checks, and two stamp/manifest checks pass.
Targeted Clippy, formatting, and Wasm compile checks also pass. No downstream
suite speedup is claimed; full pre-push validation remains maintainer-owned.

## 0.14.2

This patch release simplifies fixture and artifact ownership and adds opt-in
fixture performance measurements. Public library APIs and persisted layouts are
unchanged; existing `0.14` callers need no source migration. Repository-owned
format identifiers remain `v1`.

- Standalone fixture pools derive rebuild reasons from shared slot state,
  removing duplicated invalidation metadata while preserving restore-failure
  rebuilding and distinct panic recovery outcomes.
- Wasm build records expose the cache path held by their retention owner instead
  of allocating a second copy. Cloned records continue retaining immutable
  artifacts until their last drop.
- The repository includes a Linux benchmark comparing fresh fixtures with
  baseline pools of capacity one and two using a caller-supplied PocketIC 16
  binary. It validates every task and reports phase timings, setup/teardown,
  sampled process-tree RSS, raw samples, and provenance. The workload and
  measured capacity tradeoffs are documented in the repository's
  [fixture benchmark guide]https://github.com/dragginzgame/ic-testkit/blob/main/docs/fixture-reuse-benchmark.md.
  This tooling is outside ordinary tests/CI.
- Observed Cargo subprocess tests read their fixtures through the existing
  system shell, avoiding intermittent Linux "Text file busy" launch failures
  under parallel load. Output forwarding, captured diagnostics, exit events,
  and host build-progress notifications retain real subprocess coverage;
  production Cargo execution is unchanged.

Seven targeted PocketIC checks and four artifact handoff checks pass, covering
reuse, capacity, restore failures, panic recovery, cloned records, cross-process
retention, pruning, and terminated consumers. Two Python sampler checks,
targeted Clippy, and formatting also pass. The measurements compare fixture
strategies; they do not establish a library-version or downstream suite speedup.
Five focused output/progress checks pass, along with 500 parallel repetitions
of the two affected subprocess tests (1,000 test executions).

## 0.14.1

This patch release reduces artifact acquisition and fixture-pool overhead without
changing public APIs or persisted layouts. Existing `0.14.0` callers need no
source migration. Repository-owned cache, stamp, and digest identifiers remain
`v1`.

- Wasm stamps and transactional manifests reject mismatched format/build
  identities before hashing outputs. Matching entries still require full content
  validation and exact metadata; live retained entries cannot be replaced during
  corruption recovery.
- Input hashing borrows declared paths and Unix-native filename bytes, and
  computes directory sort keys once per entry. Native ordering and Windows
  UTF-16 little-endian encoding retain their existing digest semantics.
- Digest-cache hits compare borrowed exclusion paths without reconstructing
  cloned lists. Changes to relevant exclusions still rehash inputs, excluded
  input roots are rejected, and external symlinks retain conservative checks.
- Cache size scans queue only directories while preserving logical file sizes
  and counting symlink bytes without following their targets.
- Bounded fixture pools register and claim FIFO slots under one coordinator
  lock, with one optional cancellation ticket and safe unwind cleanup. Capacity,
  waiter ordering, cancellation wakeups, and invalidation remain unchanged.

Targeted checks cover native filenames, exact digests and stamps, corruption
recovery, retained consumers, pruning, cross-process handoff, FIFO cancellation,
and PocketIC 16 reuse with 100 consecutive baseline restores. Whole-suite
performance improvements are not yet measured.

## 0.14.0

Standalone fixture pools own one builder, removing the per-acquisition builder
that warm slots ignored. This is a source API hard cut. Snapshot funding,
capacity, restoration, recovery, guard types, and persisted formats are unchanged.
Repository-owned format identifiers remain `v1`.

### Migration

| Previous usage | Replacement |
| --- | --- |
| `CachedStandaloneCanisterFixturePool::<N>::new()` or `default()` | `CachedStandaloneCanisterFixturePool::<N>::new(build_fixture)` |
| `pool.acquire(build_fixture)` | `pool.acquire()` |
| Pass a closure capturing configuration to every acquisition | Own it once with `CachedStandaloneCanisterFixturePool::<N, _>::new(move || build_fixture(&config))`. |

Static pools can use a function pointer or a noncapturing closure:

```rust
use ic_testkit::pic::{CachedStandaloneCanisterFixturePool, StandaloneCanisterFixture};

static POOL: CachedStandaloneCanisterFixturePool<8> =
    CachedStandaloneCanisterFixturePool::<8>::new(build_fixture);

fn build_fixture() -> StandaloneCanisterFixture {
    // Install and seed the canister here.
    todo!()
}

let (fixture, outcome) = POOL.acquire()?;
```

The builder must produce the same Wasm, initialization, topology, and seeded
state each time it runs. Use separate pools for different recipes. Cold and
replacement slots invoke the owned builder; warm acquisitions restore the
captured snapshot. Apply the constructor and acquisition changes together when
downstream suites adopt `0.14`.

For statics that chain `with_restore_funding`, specify the constructor capacity
as above so Rust selects the default function-pointer builder type before
applying the funding policy.

### Implementation simplification

Wasm batches parse Cargo package, membership, and dependency indexes once per
resolution group. Each specification still selects its own dependency closure
and validates its filesystem inputs; differing features remain separate groups.
Standalone Wasm builds use the same parsed representation.

CI runs the canister integration target through the ordinary test stage, removing
its separate repeat and unused preliminary fixture build. `make test-canisters`
remains a focused entry point whose tests acquire their own artifacts;
`make build-test-canisters` remains available for manual builds.

Release-push guards exercise clean, dirty, untracked, stale-tag, and failed-push
behavior using harmless command doubles, replacing the exact recipe-text check.

## 0.13.0

This release gives benchmark identity and averages one authoritative
representation and verifies canister restore evidence without copying it into
non-snapshot reset receipts. The source API changes are hard cuts. Report schemas
and persisted cache layouts are unchanged; cache, stamp, protocol, and digest
identifiers remain `v1`.

### Migration

| Previous usage | Replacement |
| --- | --- |
| `BenchmarkAggregateRow::suite`, `BenchmarkComparisonRow::suite`, or `BenchmarkAggregateError::suite` field | Call `suite()`. The label derives from the private scope; use `is_all_suites()` to distinguish an authored `ALL` suite from the cross-suite aggregate. |
| Read or assign `BenchmarkAggregateRow::average` | Read `average()`. Averages derive from `total` and `runs`; there is no separately writable average. |
| `ResetRequirements::try_new([CanisterSnapshots, CanisterCycles(policy), ...])` | `ResetRequirements::try_new(policy, [...])`, passing only non-snapshot requirements in the collection. Read the explicit cycle policy with `cycle_policy()`; `get()` and `iter()` cover non-snapshot domains. |
| Snapshot/cycle variants of `ResetDomainKind`, `ResetRequirement`, or `ResetAchievement` | Snapshot restoration is unconditional. Report restored canisters and achieved cycle policy in `CanisterRestoreReceipt`; report other guarantees in `ResetReceipt`. |
| Inspect snapshot/cycle achievements in `PreparedBaseline::Restored::reset` | Inspect its `canisters` receipt with `canister_ids()` and `cycle_policy()`. The `reset` receipt contains only non-snapshot achievements. |
| Match cycle failures through `ResetPolicyMismatch` | Match `CyclePolicyMismatch { required, achieved }`. `ResetPolicyMismatch` still reports non-snapshot policy mismatches. |
| Match `UndeclaredRequiredResetDomain` | Remove this constructor-error branch. The required cycle policy is a constructor argument, and complete snapshot restoration is enforced during preparation. |

For example:

```rust
use ic_testkit::pic::{
    CycleResetPolicy, ResetRequirement, ResetRequirements, TimeResetPolicy,
};

let requirements = ResetRequirements::try_new(
    CycleResetPolicy::PreserveCurrent,
    [ResetRequirement::PocketIcTime(TimeResetPolicy::PreserveCurrent)],
)?;
```

Update downstream recipes when adopting this release. Restore, non-snapshot
reset, readiness, and final validation keep their ordering. Canister-set and
cycle-policy mismatches retain the `ResetCoverageMismatch` rebuild reason;
recoverable preparation failures still permit one rebuild, and failed recovery
retains both failures.

### Simplification and verification

The observed Cargo output/heartbeat test keeps its fixture running until the
observer receives a heartbeat, with a bounded timeout. This removes a scheduling
race caused by a fixed sleep under parallel test load; runtime heartbeat behavior
is unchanged.

Benchmark labels and comparison keys derive from one scope. Report writers and
comparisons calculate averages from totals and runs, preserving CSV columns and
named-`ALL` identity. Arithmetic overflow checks remain in place.

Wasm batch acquisition and reporting use one failure-details representation,
owned alongside each error. Existing public result/accessor and `into_parts()`
signatures, partial timings, captured output, entry order, and retained successful
records are preserved.

Targeted checks cover benchmark updates and report schemas, restored canister-set
and cycle-policy mismatches, recovery and panic invalidation, 100 consecutive
restores, mixed Wasm batch results, captured Cargo diagnostics, retained-output
handoff, and concurrent prepared readers. PocketIC 16 baseline reuse and isolated
dead-server recovery, Clippy, rustdoc, formatting, and Wasm compilation pass.

## 0.12.0

This update tightens artifact path boundaries, fixes Unix managed-server
descendant cleanup, and rejects benchmark aggregate overflow. Cache, stamp,
protocol and digest identifiers remain `v1`.

### Migration

These source API and validation changes are hard cuts:

| Previous usage | Replacement |
| --- | --- |
| `aggregate_benchmark_spans(...) -> BenchmarkAggregateReport` | `Result<BenchmarkAggregateReport, BenchmarkAggregateError>`; handle or propagate overflow before comparing aggregates or writing reports. |
| Override `CARGO_TARGET_DIR` in `with_extra_env` or pass `--target-dir` in `with_cargo_profile_args` | Select the exact target with `WasmBuildSpec::new` or the shared target with `with_shared_incremental_target`; command overrides return `InvalidSpec` before acquisition. |
| Configure one public artifact output beneath another | Use distinct, non-nested output destinations; overlapping destinations are rejected during preparation. |

### Fixes and simplification

Observed Cargo output capture retries interrupted pipe reads, preserving captured
bytes while propagating permanent read errors.

Compact Cargo feature arguments (`-Fextra` and `-F=extra`) now reach metadata
resolution as well as compilation. Optional dependencies enabled by these
arguments are watched inputs, and batches resolve distinct feature graphs
separately.

Benchmark aggregation rejects counter and run-count overflow with a typed
error identifying the scope, span and counter. No partial, wrapped or saturated
totals are returned. `is_all_suites()` distinguishes a cross-suite failure from
a named suite `ALL`.

Shared-target maintenance now rejects layouts that could delete retained exact
Wasm artifacts. Batches maintain each resolved target directory once, including
workspace-relative paths and filesystem aliases. Relative exact targets use the
caller's working directory consistently for Cargo output and cache operations.
Conflicting `CARGO_TARGET_DIR` environment or `--target-dir` command overrides
are rejected before acquisition; select target directories through the build spec.

Artifact preparation rejects nested file destinations. Output boundary checks
validate the directory entry replaced by atomic publication rather than a final
symlink's referent; declared input and tool symlink entries remain protected.

On Unix, managed-server teardown terminates descendants in its owned process
group on handle drop, startup failure, and natural server exit. Cleanup reserves
the leader's PID until after signaling the group, then reaps the child.

Benchmark metadata now derives its JSON fields from `BenchmarkRunMetadata`.
Existing JSON field names, object shape, field types, optional fields and integer
bounds are preserved; invalid metadata returns `InvalidData` with Serde field
diagnostics.
Wasm warm hits share input validation and retained-record completion, and batch
input reuse has one internal representation for sessions and prepared snapshots.
Batch entry points call the same runner directly, and transport errors and panic
payloads share one message classifier.
Batch maintenance-policy errors use the common report-entry construction while
retaining their failure phases and timings. Internal attempts store the phase
and timings together; standalone and batch resolution share toolchain
identification.
ICP readiness delegates file validation to the watched-input snapshot.
Background server reaping retains the original managed-server owner and its
startup files. Shared-target lock acquisition owns progress events for both
ordinary builds and scheduled maintenance.

### Verification and documentation

Targeted regressions cover retained-cache and output boundaries, relative target
paths, Cargo override rejection, benchmark overflow and metadata validation,
compact-feature dependency discovery and batch grouping, and observed
session/prepared-reader batches. Unix descendant cleanup is checked
on drop, startup timeout, natural exit and background reaping, with a real
PocketIC 16 startup/reaping check. Maintenance behavior tests use outcome and
filesystem assertions, with a positive-interval control for heartbeat validation.

Installation examples select `0.11`. The README documents the updated path and
cleanup rules and explains recipe-pool timings for profiling long suites.
`POCKET-IC.md` refreshes upstream tracking.

## 0.11.0

This minor release fixes cache-hit input races and batch failure isolation,
consolidates baseline reuse and installation APIs, and makes operation failures
and benchmark aggregate identity explicit. The API and CSV schema changes are
hard cuts; cache, stamp, protocol, and digest identifiers remain `v1`.

### Migration

Update downstream code for these API and report-schema changes:

| Previous API or schema | 0.11.0 replacement |
| --- | --- |
| `restore_or_rebuild_cached_pocket_ic_baseline` and `CachedPocketIcBaselineGuard` | `CachedPocketIcBaselinePool::new(capacity, recipe)` and `acquire()`. Use capacity one for sequential reuse; acquisition returns a typed outcome and an exclusive lease. |
| `create_and_install_with_args` and `try_create_and_install_with_args` | `create_and_install(InstallSpec::new(...))` and `try_create_and_install(InstallSpec::new(...))` |
| `CanisterInstallError::canister_id() -> Principal` | `Option<Principal>`; creation failures have no id. Inspect `phase()` for `CreateCanister`, `AddCycles`, or `InstallCode`. |
| `CanisterInstallError::new(id, message)` / `labeled(...)` | `new(phase, optional_id, optional_label, PocketIcOperationError)` |
| Snapshot panic variants' `message` field | `source: PocketIcOperationError`; inspect `message()` and `is_transport()` on the cause, or follow `Error::source()`. |
| `WasmBuildError::InputsChangedDuringBuild` and `ArtifactCacheError::InputsChangedDuringBuild` | `InputsChangedDuringAcquisition` |
| `comparison.csv` without aggregate scope | Leading `scope` column, containing `suite` or `all`. Named suite `ALL` and the cross-suite aggregate are distinct. |

`CachedPocketIcBaseline<T>` remains the owned snapshot-and-metadata value used
by recipe pools. A recipe declares its restore/reset/readiness/validation
contract and recovery policy; pool leases prevent another acquisition from
replacing or mutating the slot during recovery.

Fallible installation now captures upstream failures during creation and cycle
funding as well as installation. Standalone errors retain the caller's PocketIC
instance at every failed stage. Snapshot and installation errors retain a shared
operation cause so `is_dead_pocket_ic_transport_error` can classify it through
contextual wrappers without broadening the strict transport parser.

Ordinary warm Wasm acquisitions reject inputs that change after initial
resolution, including conservative workspace inputs. Immutable-source sessions
and prepared readers retain their explicit lease contract and skip repeated
warm validation. Batches report input hashing/discovery failures per entry and
continue with valid compatible entries. Input-race errors invalidate leased
readers as before.

Server diagnostic reads allocate only the bounded log prefix. Benchmark indices
continue past `9999`, previous-run selection compares them numerically, and
exhaustion returns an error rather than reusing an existing path. Empty commit
hashes consistently use the `unknown` directory prefix.

### Repository tooling and documentation

Release CI cleanup now matches the selected server binary's device/inode,
including renamed binaries, alongside its private port-file path. Unknown or
unavailable executable identities retain scratch without signalling unrelated
processes. Focused process/socket regressions cover configured and default
binary selection, identity failures and ownership races. This changes repository
release tooling; the runtime changes are described above.

README examples and API guidance are updated against the implementation, with
all 27 Rust examples checked. The documentation index separates current usage
from historical design records. Targeted regressions cover the cache, transport,
installation, output-read, and benchmark boundaries described above, alongside
existing live PocketIC recovery and concurrency checks.

## 0.10.4

The repository's release CI runner now stops invocation-owned PocketIC servers
before removing its temporary directory. This prevents server HTTP adapter
teardown from panicking on sockets already deleted by release cleanup. The
published crate's runtime API and dependency selection are unchanged.

The isolated PocketIC teardown patch and development probe are removed.
Instance teardown improvements will wait for a future upstream release; the
repository does not maintain a patched PocketIC client. Seven targeted
process/socket regressions cover the repository's release cleanup behavior.
Fixture sockets use short relative bind paths to support nested release
temporary directories without exceeding the Unix socket pathname limit.

## 0.10.3

The repository includes an isolated PocketIC 16.0.0 upstream teardown proposal
and repeatable synthetic HTTP probe. Seven targeted parent tests qualify
fallible shutdown deadlines, acknowledgement checks, ownership retained for
retry, bounded best-effort drop and borrowed gateway cleanup.

This is a development experiment. The published crate still uses the registry
PocketIC dependency and adds no production shutdown API or teardown fix.
The original Busy/tick cause remains unproven. The experiment was subsequently
removed in 0.10.4 in favor of waiting for an upstream release.

## 0.10.2

The repository and CI now use Rust 1.99.0. The published MSRV remains Rust
1.88.0.

Transport classification now requires a maintained reqwest error shape with a
PocketIC instance URL and recognized transport source, or a structured testkit
call transport kind. Generic `channel closed` / `ConnectionRefused` application
text, quoted error variants and bare I/O errors do not qualify. Use the public
classifier only for PocketIC-originating errors; it remains a heuristic rather
than proof of a dead instance. Unrelated call panics retain their original
payload.

Empty/nonempty collection assertions comply with Rust 1.99's `assert_is_empty`
lint and show collection values on failure.

Heartbeat tests now use event coordination. A synthetic HTTP/subprocess test
demonstrates that PocketIC 16's instance destructor waits for DELETE, independently
of the construction deadline and operation budget. This records an upstream
limitation; no bounded teardown API or simulator wrapper is added.

## 0.10.1

`PocketIcManagedServer::process_id()` exposes the owned server child's OS PID
for caller-managed resource monitoring alongside its URL and captured output.
It identifies only the child, does not establish liveness, and retains no
ownership when copied. The OS may reuse it after the child exits and is reaped.

## 0.10.0

This minor release hard-cuts artifact consumption to retained exact outputs,
fixing the lifetime gaps reported by IcyDB in issue #2. The original missing
input's precise racing operation has not been established.

`WasmBuildRecord::artifacts()` and `ArtifactCacheRecord::artifacts()` now return
read-only exact-cache paths instead of caller-selected materialization paths.
A successful cold build or warm hit acquires retention before releasing its
producer locks. Keep its record, outcome, or batch report alive while consuming
those paths. Cloning a record shares retention; cloning a path or an
`ArtifactCacheArtifact` descriptor does not. `exact_cache_path()` is also
protected for the Wasm record's lifetime.

Age/size pruning skips live entries across threads and processes. Configured
bounds may be exceeded while consumers retain entries; after the last owner
drops, the next maintenance pass can reclaim them normally. OS locks release
on process exit, including a crash, without stale pins. A corrupt retained
entry fails closed instead of being replaced. Treat exact paths as read-only;
manual cache deletion and external mutation are outside the ownership contract.
All cache and digest formats remain `v1`.

### Hard-cut migration

| Previous consumption | 0.10 consumption |
| --- | --- |
| Read the configured compiler output after a build | Read `outcome.record().artifacts()` and keep the outcome or a cloned record alive through post-link commit |
| Read the configured post-link destination later | Keep the returned `ArtifactCacheRecord` and read its artifacts until staging/reading finishes |
| Reduce successful batch results to indexes or paths | Keep the report, move out successful outcomes, or clone their records; later failed entries do not invalidate successful records |
| Transform an artifact in place | Write into the post-link transaction's output staging paths |

Materialization still populates configured destinations, but those remain
mutable and may be replaced by another acquisition. No compatibility accessor
or alternate cache protocol is added. Declared-input/tool hashing errors now
include the failing path. Source-mutation and input-identity checks remain in
force.

The `artifacts` module documentation demonstrates retained Wasm-to-post-link
handoff. IcyDB must adopt the release in both single and batch flows and rerun
its concurrent lifecycle tests; that downstream validation is not claimed here.

## 0.9.1

The workspace `toml` dependency moves from 0.9 to 1, with the refreshed
lockfile resolving `toml` to 1.1.6.

Release CI no longer runs `cargo clean` after a successful gate. Cargo build
artifacts are preserved for incremental reuse after success as well as for
diagnosis after failure; only the release wrapper's isolated temporary
directory is removed. Release-flow guards keep the standalone `make clean`
target outside CI and reject Cargo cleanup from CI, release, and publish
scripts.

## 0.9.0

The workspace now uses PocketIC 16. Managed servers started through
`PocketIcStartupConfig::spawn` no longer receive a ten-minute `--hard-ttl` by
default, matching the upstream lifetime policy. An active test suite is not
terminated merely because ten minutes have elapsed; PocketIC's activity-based
soft TTL still bounds an orphaned server, and a caller-owned
`PocketIcManagedServer` is still terminated and waited for on drop.

PocketIC 16 also waits for the first certified time update before returning
from automatic-progress startup, accounts for mocked HTTP response cycle spend,
rejects mocked HTTP reject messages larger than 1 KiB, and adds flexible HTTP
mocking plus the `SubnetCoolingDown` and `CanisterStatusAccessDenied` error
codes. These remain part of the direct upstream runtime surface; ic-testkit
does not add parallel wrappers for them.

The complete host-only upstream crate is now re-exported as
`ic_testkit::pocket_ic`. Use that path for native PocketIC types outside the
focused `ic_testkit::pic` convenience surface:

```rust
use ic_testkit::pocket_ic::{
    CanisterSettings, CreateCanisterParams, PocketIc,
    common::rest::{BlobCompression, IcpFeatures, IcpFeaturesConfig},
};
```

These are the types from the exact PocketIC dependency selected by ic-testkit;
there is no copied type or parallel wrapper. The complete re-export, like
`ic_testkit::pic`, is unavailable on `wasm32`.

Call `with_server_hard_ttl(duration)` when an absolute server deadline is
required. Subsecond explicit values remain invalid.

### Hard-cut migration

| 0.8 API | 0.9 API |
| --- | --- |
| `PocketIcStartupConfig::server_hard_ttl() -> Duration` returned the default ten-minute hard TTL | `server_hard_ttl() -> Option<Duration>` returns `None` by default and `Some(duration)` after `with_server_hard_ttl(duration)` |

There is no compatibility accessor or implicit ten-minute fallback. Startup
and instance-creation deadlines remain independently bounded by
`PocketIcStartupConfig::timeout`.

## 0.8.9

`WasmBuildInputSnapshot::prepare_assuming_sources_immutable` resolves a fixed
set of exact `WasmBuildSpec` values once under a caller-held source
write-exclusion guard. Its `build_batch` and `build_batch_with_progress`
methods take `&self`, so separate sequential batches may read the prepared
inputs concurrently. Reader metrics distinguish prepared reuse from ordinary
batch and mutable-session reuse.

Every reader specification must have been declared during preparation;
`SpecificationNotPrepared` rejects an undeclared entry before progress or build
work. A detected post-build input mutation invalidates the snapshot for every
later reader, and publication is coordinated with that shared invalidation
boundary. Ordinary batch calls remain independently resolved. Do not construct
a snapshot with an unrelated token: the borrowed value is a lifetime boundary,
not guard-provenance validation performed by `ic-testkit`.

## 0.8.8

`WasmBuildSession::new(&guard)` is hard-cut to
`WasmBuildSession::assume_sources_immutable(&guard)`. The constructor name now
makes the caller assertion explicit: `ic-testkit` lifetime-binds the session to
the supplied reference but cannot prove that the value is a genuine workspace
write-exclusion guard. An unrelated token still violates the contract and can
permit stale reuse. There is no `new` alias or deprecated bridge.

A concurrent-reader resolution snapshot remains a documented future design,
not an ambient cache or parallel batch mode. It would prepare a declared spec
set under one genuine source lease, freeze resolution state before sharing,
propagate invalidation to every reader, retain existing Cargo target locking,
and require consumer benchmarks before implementation.

## 0.8.7

Managed spawn now allocates stdout, stderr, and the server-owned port path
inside a unique private directory, but creates only the output files before
launch. The actual `--port-file` path remains absent until PocketIC publishes
it; a missing path is treated as pending readiness. This fixes PocketIC 15,
which exits successfully without starting when the supplied port path already
exists.

`PocketIcStartupConfig::start_managed_server` returns a caller-owned
`PocketIcManagedServer`. Its `url()` can feed any number of bounded
`PocketIcStartupConfig::connect` calls in a serial suite, `output()` returns
the first 16 KiB of lossy stdout/stderr per stream with an omitted-byte marker,
and dropping the handle terminates and waits for the managed child. Keep the
handle alive until its connected instances are dropped. This is explicit
process-local ownership rather than a process-global server or an implicit
retry path. CI spanning multiple Cargo or test-runner processes should retain
an external runner-owned server and use bounded connect mode in each process.

An ignored real-server regression test accepts the exact caller-resolved
binary through `IC_TESTKIT_POCKET_IC_SERVER`. It verifies port publication,
bounded instance construction, owned shutdown, and startup-directory cleanup
without adding binary discovery or download behavior to the crate.

`WasmBuildSession` is an explicit caller-owned cross-call input snapshot. Its
constructor borrows a source write-exclusion guard for the session lifetime;
the caller must ensure that all Cargo/rustc executables, manifests and Cargo
configuration, discovered sources, declared additional inputs, and relevant
environment values are immutable while the session exists. Exact resolution
snapshots and content digests may then be reused by separate sequential
`build_batch` or `build_batch_with_progress` calls. Ordinary batch functions
retain their current per-call validation, and there is no ambient or
process-global cache.

If revalidation around a Cargo build detects an input mutation, the session is
permanently invalidated, all pending pre-race snapshots are discarded, and a
later call returns `WasmBuildBatchContractError::SourceLeaseInvalidated`.
`WasmBuildSession::metrics` exposes retained snapshots, successful snapshot
reuses, and invalidation state; each batch separately reports
`input_resolution_session_reuses`.

Failed Wasm batch entries now expose `WasmBuildFailurePhase` and partial
`WasmBuildFailureTimings` through `WasmBuildBatchFailure::phase` and `timings`.
The timings retain completed exact/shared coordination, tool identity, Cargo
metadata, input discovery, content hashing, shared maintenance, Cargo,
publication, exact-cache maintenance, explicit cleanup, and total wall time.
Successful build-record timing remains unchanged.

### Hard-cut migration

| 0.8.6 API | 0.8.7 API |
| --- | --- |
| `WasmBuildBatchEntry::into_parts() -> (usize, String, Result<_, _>, Duration)` | Destructure `(index, label, result, failure_details, entry_elapsed)`; failed entries carry `Some(WasmBuildFailureDetails)` |
| `WasmBuildBatchFailure` exposes only label/index/error/elapsed | Use `phase()` and `timings()` for the primary failed phase and partial work |
| Separate batch calls always resolve their inputs independently | Hold the real source write-exclusion guard and call `WasmBuildSession::assume_sources_immutable(&guard)` when the immutable-source contract can be guaranteed |

There is no four-field `into_parts` alias, deprecated session-free overload,
implicit guard, global cache, or reset-after-race shim. Batches remain
sequential and collect-all; recipe and observer panics continue unwinding.

## 0.8.6

Wasm batches now require `LabeledWasmBuildSpec`. Labels must be nonempty and
unique and are retained in canonical report entries, successful outcomes,
failures, progress events, and shared-target maintenance outcomes. Label
preflight completes before metadata resolution, progress, or build work;
labels do not alter exact Wasm fingerprints. Valid entries remain sequential
and collect-all.

Diagnostic batch labels now follow the same contract. Empty or duplicate
labels return `CanisterDiagnosticsBatchContractError` before any status or log
call starts. Valid targets retain their exact controllers and continue after
independent failures as before.

`PocketIcBuilderExt::try_build` now requires an explicit
`PocketIcStartupConfig`. `spawn` launches one exact caller-resolved binary,
detects child exit while waiting for readiness or instance construction,
terminates the child at the complete startup deadline, and returns structured
errors with bounded lossy stdout/stderr. `connect` applies the same construction
deadline to a caller-owned existing server. Both policies force an explicit
server URL onto the upstream builder, so it cannot spawn an unobservable child.
ic-testkit does not discover, download, cache, or compatibility-check server
binaries.

Exact Cargo Wasm identity now uses a validated semantic workspace projection.
The projection retains selected resolve nodes, enabled features, exact external
source/checksum/revision identity, effective package fields, workspace
profiles/resolver/lints, selected local sources, tools, Cargo configuration,
and declared inputs. An unrelated host-only workspace dependency or lockfile
update can therefore reuse the same selected Wasm entry.

The complete workspace manifest and lockfile remain conservative validation
inputs. `ResolvedCargoBuildInputs::validation_digest` exposes that raw mutation
guard; `input_digest` is now semantic cache identity. Cargo builds and attached
artifact transactions reject any raw input change during their operation.
Workspace-root packages and local packages outside the normalizable workspace
boundary fall back to the complete input identity.

### Hard-cut migration

| 0.8.5 API | 0.8.6 API |
| --- | --- |
| Wasm batch functions accept `&[WasmBuildSpec]` and return `WasmBuildBatchReport` | Wrap every spec with `LabeledWasmBuildSpec`; handle `Result<WasmBuildBatchReport, WasmBuildBatchContractError>` |
| `results()`, `into_results()`, and parallel `entry_elapsed()` access | Use canonical `entries()` or `into_entries()`; each `WasmBuildBatchEntry` owns index, label, result, and elapsed time |
| Wasm `outcomes()` and shared maintenance yield indexed tuples; failures have no label | Use the structured entry accessors `index()`, `label()`, `outcome()` or `error()`, and `entry_elapsed()` where available |
| Batch progress variants contain only an index | Match the required `label` field as well, or use `..` when the label is intentionally ignored |
| Diagnostics batch returns `CanisterDiagnosticsBatchReport` directly | Handle `Result<CanisterDiagnosticsBatchReport, CanisterDiagnosticsBatchContractError>`; labels must be nonempty and unique |
| Configure `with_server_binary`/`with_server_url`, then call `try_build()` | Call `try_build(PocketIcStartupConfig::spawn(path, timeout))` or `try_build(PocketIcStartupConfig::connect(url, timeout))` |
| Read `PocketIcStartupError::message()` | Match the structured `PocketIcStartupError` variants and their public fields |

No index-only overloads, parallel label slices, deprecated report accessors,
zero-argument `try_build`, implicit binary fallback, or compatibility aliases
are retained.

### Semantic identity migration

- Treat `ResolvedCargoBuildInputs::input_digest` and
  `WasmBuildRecord::input_digest` as semantic selected-build identity. Use the
  new `validation_digest` when retaining or comparing a conservative raw input
  snapshot.
- Expect one new exact key for workspaces eligible for projection. Repository
  digest domains and cache formats remain `v1`; no legacy key lookup, shim,
  alias, or dual reader is retained.
- Continue declaring build-script, procedural-macro, source-include, or tool
  inputs that live outside Cargo's selected package graph.

## 0.8.5

`0.8.5` hard-cuts generic artifact batches to caller-labeled specifications.
`LabeledArtifactCacheSpec` labels must be nonempty and unique; they are retained
in cache-miss callbacks and every ordered report entry. Labels are report and
composition identity only and do not alter exact artifact-cache keys. Invalid
label structure returns `ArtifactCacheBatchContractError` before any entry
starts.

`ArtifactCacheBatchFailureTimings` distinguishes preparation, callback,
explicit abort cleanup, commit, and total time. The failure also exposes its
primary `ArtifactCacheBatchFailurePhase`. Commit timing includes cleanup owned
internally by a failed commit. Recipe panics still unwind, and independent
entries remain sequential and collect-all.

`CanisterDiagnosticsBatchEntry::entry_elapsed` retains each target's complete
diagnostic collection time, while `CanisterDiagnosticsBatchReport::total`
retains total sequential batch time. The compact renderer includes both. Exact
controllers, bounded logs, collect-all behavior, and the absence of anonymous
fallback remain unchanged.

### Hard-cut migration

| 0.8.4 API | 0.8.5 API |
| --- | --- |
| `build_artifact_caches_batch(&[ArtifactCacheSpec], FnMut(usize, ...)) -> ArtifactCacheBatchReport<E>` | Wrap specs with `LabeledArtifactCacheSpec`; the callback receives `&str`; handle `Result<ArtifactCacheBatchReport<E>, ArtifactCacheBatchContractError>` |
| `results()`, `into_results()`, and `entry_elapsed()` parallel report slices | `entries()` and `into_entries()` return canonical `ArtifactCacheBatchEntry<E>` values with `index()`, `label()`, `result()`, and `entry_elapsed()` |
| `outcomes()` yields `(usize, &ArtifactCacheOutcome)` | Yields `ArtifactCacheBatchOutcomeEntry`; use `index()`, `label()`, `outcome()`, and `entry_elapsed()` |
| `ArtifactCacheBatchFailure` without phase timing fields | Match the hard-cut variants with `..`, then use `phase()` and `timings()`; failed-entry views also expose `label()` and `timings()` |
| `CanisterDiagnosticsBatchEntry::into_parts() -> (String, CanisterDiagnosticsReport)` | Returns `(String, CanisterDiagnosticsReport, Duration)`; borrowed callers may use `entry_elapsed()` and the batch `total()` |

No index-only overloads, parallel label sidecars, tuple aliases, or deprecated
bridges are retained.

The Wasm resolver already discovers the selected local dependency closure, but
the complete workspace manifest and lockfile remain exact inputs because
workspace inheritance, profiles, patches, resolver state, external revisions,
build scripts, proc macros, and includes can cross the apparent closure. A
future narrower fingerprint must use a validated semantic projection with a
conservative fallback. Likewise, digest reuse across incompatible batch groups
requires an explicit caller-held immutable-source lease or validated snapshot;
`0.8.5` adds no ambient or unsafe digest cache.

## 0.8.4

`0.8.4` adds sequential collect-all diagnostics for caller-labeled exact
requests. `PocketIcDiagnosticsExt::collect_canister_diagnostics_batch` attempts
every target and returns ordered `CanisterDiagnosticsBatchEntry` values. Each
entry retains its label, exact controller-aware request, independent status and
log outcomes, bounded lossy UTF-8 log content, and omitted-byte/record counts.
An earlier rejection, dead PocketIC transport, or panic does not prevent later
entries from being attempted. There is no anonymous retry and
`dump_canister_debug` is not restored.

Wasm and generic artifact collect-all reports now expose structured failed
entries with specification index, error or failure, and complete entry wall
time. Generic reports also retain an ordered `entry_elapsed` value for every
success and failure. Detailed partial phase timings for failed preparation or
commit paths remain a future error-contract change.

### Hard-cut migration

| 0.8.3 API | 0.8.4 API |
| --- | --- |
| `WasmBuildBatchReport::failures()` yields `(usize, &WasmBuildError)` | Yields `WasmBuildBatchFailure`; use `index()`, `error()`, and `entry_elapsed()` |
| `ArtifactCacheBatchReport::failures()` yields `(usize, &ArtifactCacheBatchFailure<E>)` | Yields `ArtifactCacheBatchFailedEntry<E>`; use `index()`, `failure()`, and `entry_elapsed()` |

The tuple iterators have no aliases or deprecated bridges. Generic batch input
hashing is not memoized across entries because current preparation rehashes to
detect mutations. Safe reuse requires a caller-supplied source-immutability
lease or explicit revalidation. Caller-supplied stable artifact entry keys are
likewise reserved for one future labeled-spec hard cut rather than a parallel
key sidecar.

## 0.8.3

`0.8.3` is a behavior-preserving code-hygiene patch. It consolidates optional
phase-timing aggregation and the indexed result iteration shared by collect-all
batch reports, removing duplicate internal implementations.

Release tooling now uses one reader for the `[workspace.package]` version
across Make, changelog, bump, tag, publish, and release guards. Exact stable
versions are required for release operations while bump preparation retains
its prerelease-compatible parsing.

There are no public API, cache-format, schema, or runtime behavior changes in
this patch, and no migration or pre-1.0 API hard cut is required.

## 0.8.2

`0.8.2` is a release-process and CI-stability patch. It makes exact-cache
lock-heartbeat coverage scheduling-independent and ensures the complete release
gate runs before version metadata changes. A committed, tagged release is no
longer subjected to a redundant second validation pass that can strand a local
patch version after failure.

There are no public API, cache-format, schema, or runtime behavior changes in
this patch.

## 0.8.1

`0.8.1` continues the pre-1.0 hard-cut policy with structured controller-aware
diagnostics, collect-all generic artifact batches, and aggregate batch
observability. Removed APIs have no aliases, deprecated bridges, dual entry
points, or compatibility readers.

### Changes

- `build_artifact_caches_batch` is sequential collect-all and returns
  `ArtifactCacheBatchReport<E>`. Preparation, callback, and commit failures are
  indexed and later independent entries continue.
- Wasm and generic artifact reports expose aggregate built/reused/failed
  counters and successful timings. Wasm metrics also distinguish compatible
  input-resolution runs from reused snapshots.
- `WasmBuildBatchReport::entry_elapsed` retains wall time for every entry,
  including failures. Detailed phase timings remain available on successful
  records; partial failed-phase timing is a documented follow-up.
- `PocketIcDiagnosticsExt::collect_canister_diagnostics` takes exact,
  independent status and log senders and returns a structured report. Status
  and logs retain separate success/failure results. Log content is bounded
  lossy UTF-8 with explicit omitted-record and omitted-byte counts.
- The anonymous-only, printing `dump_canister_debug` entry point is removed.
  Install-failure diagnostics pass the install sender through and remain
  best-effort so they cannot replace the original failure.

### Additional hard-cut migration

| Earlier API | 0.8.1 API |
| --- | --- |
| `Result<ArtifactCacheBatchOutcome, ArtifactCacheBatchError<E>>` | `ArtifactCacheBatchReport<E>` with indexed `ArtifactCacheBatchFailure<E>` values |
| `PocketIcDiagnosticsExt::dump_canister_debug(canister_id, context)` | `collect_canister_diagnostics(CanisterDiagnosticsRequest::new(canister_id, status_sender, log_sender))`; inspect `status` and `logs`, then use `Display` or `render_compact` when text is needed |

Compatible Wasm input memoization remains scoped to one batch call. There is no
silent global cache or cross-call session in `0.8.1`. A proposed explicit
session must require a caller-guaranteed source-immutability lease (or pay for
revalidation); it is documented rather than partially implemented.

## 0.8.0

`0.8.0` is a pre-1.0 hard cut. It adds collect-all Wasm batches, compatible
input resolution reuse within one batch, and public immutable exact-cache
paths. Removed APIs have no aliases or deprecated bridges.

### Migration

| Before 0.8 | 0.8.0 |
| --- | --- |
| Wasm batch returned `Result<WasmBuildBatchOutcome, WasmBuildBatchError>` | It returns `WasmBuildBatchReport`; inspect `results`, indexed `outcomes`/`failures`, and `is_success` |
| `WasmBuildCachePrunePolicy`, `WasmBuildCachePruneReport`, `WasmBuildCacheMaintenance` | `ArtifactCachePrunePolicy`, `ArtifactCachePruneReport`, `ArtifactCacheMaintenance` |
| `CargoHeartbeat { elapsed }` | `Heartbeat { phase: WasmBuildProgressPhase::CargoBuild, elapsed }` |
| Wasm builders ending in `_os` | Use `with_cargo_profile_args`, `with_extra_env`, and `with_inherited_env` directly |
| `with_additional_input_paths` | `with_additional_inputs` |
| Transaction builders ending in `_os` | Use `with_arguments`, `with_environment`, and `with_unset_environment` directly |
| `CachedStandaloneCanisterFixturePool::acquire_with_outcome` | `acquire`, which returns the structured lifecycle outcome |
| Boolean result from fixture-pool `acquire` | Call `outcome.is_reused()` on the structured result |
| `PocketIcCapturedSnapshotExt` | Import `PocketIcSnapshotExt`; it owns both controller-fallback and exact-sender methods |
| `WasmBuildTimings::input_resolution_detail` plus aggregate `input_resolution` | `input_resolution` returns `WasmInputResolutionTimings` directly; call `.total()` for the aggregate |
| Panicking `build_wasm_canisters` wrapper | Construct `WasmBuildSpec` and call `build_wasm_canisters_cached` |

Wasm batch functions no longer return a top-level error. Handle all entries
after the sequential batch completes:

```rust,no_run
# use ic_testkit::artifacts::{WasmBuildSpec, build_wasm_canisters_cached_batch};
# let specs: Vec<WasmBuildSpec> = Vec::new();
let report = build_wasm_canisters_cached_batch(&specs);
for (index, error) in report.failures() {
    eprintln!("Wasm build {index} failed: {error}");
}
```

All repository-owned cache, stamp, and digest-domain identifiers remain at
`v1`. No migration reader is provided; disposable build caches may rebuild
under the current `v1` semantics.