-
Notifications
You must be signed in to change notification settings - Fork 31
786 lines (671 loc) · 29.5 KB
/
Copy path_unit-suite.yml
File metadata and controls
786 lines (671 loc) · 29.5 KB
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
name: Unit suite
# Reusable workflow that runs all unit-level coverage (Linux, macOS, Windows
# cargo, Windows bash e2e). Called by both tests.yml (PR / push-to-main) and
# release.yml (tag push). Single source of truth — PR-time and release-time
# unit jobs cannot drift.
#
# The `strict` input toggles whether the currently-scoped Windows bash e2e job
# gates the caller:
# strict=false → PR mode. Linux, macOS, and Windows cargo block; Windows bash
# e2e remains non-blocking (continue-on-error: true).
# strict=true → release mode. ALL four jobs gate. A failure in any of them
# fails the called-workflow's overall status, blocking downstream `needs:`
# jobs in the caller (e2e, build-*, publish-*).
#
# The release pipeline runs in strict mode so we don't ship with known
# regressions on any platform. Currently-flaky tests in the strict-only path
# must be either fixed or explicitly excluded before release time, surfacing
# real bugs rather than silently masking them.
on:
workflow_call:
inputs:
strict:
description: "When true, all jobs gate the caller; when false, only Windows bash e2e is continue-on-error"
type: boolean
default: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ---------------------------------------------------------------------------
# Rust + plugin unit tests on Linux (fast inner loop)
#
# Three warm-cache main runs spent 6.1-6.8 minutes in the Rust gate while the
# explicit debug build took only 0.7-1.1 minutes. The nextest slice alone ran
# 1,690 tests for 4.4-4.7 minutes, so compile-cache tuning cannot move the
# dominant execution cost. Build one archive and hash-partition that exact
# inventory; libtest and watcher phases retain their original execution.
# ---------------------------------------------------------------------------
unit-linux-nextest-archive:
name: Cargo test archive (Linux)
runs-on: ubuntu-22.04
timeout-minutes: 25
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
CARGO_INCREMENTAL: "0"
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.11
- uses: Swatinem/rust-cache@v2
with:
shared-key: unit-linux
cache-on-failure: true
- uses: taiki-e/install-action@nextest
- name: Build nextest archive (Linux integration suite)
run: cargo nextest archive --workspace -E 'kind(test) - binary(=watcher_integration)' --archive-file "$LINUX_NEXTEST_ARCHIVE"
env:
LINUX_NEXTEST_ARCHIVE: ${{ runner.temp }}/linux-nextest.tar.zst
- name: Prove split Rust test inventory is exact
run: python3 scripts/rust-test-inventory-proof.py
- name: Report nextest archive size
run: |
bytes=$(stat -c %s "$LINUX_NEXTEST_ARCHIVE")
printf 'Linux nextest archive: %.2f MiB (%s bytes)\n' "$(awk "BEGIN { print $bytes / 1048576 }")" "$bytes"
env:
LINUX_NEXTEST_ARCHIVE: ${{ runner.temp }}/linux-nextest.tar.zst
# nextest has already compressed the tarball with zstd.
- uses: actions/upload-artifact@v4
with:
name: linux-nextest-archive
path: ${{ runner.temp }}/linux-nextest.tar.zst
compression-level: 0
if-no-files-found: error
unit-linux:
name: Unit tests (Linux)
runs-on: ubuntu-22.04
timeout-minutes: 25
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
CARGO_INCREMENTAL: "0"
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.11
- uses: Swatinem/rust-cache@v2
with:
shared-key: unit-linux
cache-on-failure: true
- uses: taiki-e/install-action@nextest
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v5
with:
node-version: "24"
- name: Install workspace deps
run: bun install --frozen-lockfile
# Fail fast: lint + typecheck are the cheapest checks and the most
# common push mistakes — surface them in the first minute instead of
# after a multi-minute cargo build.
- name: Bun lint
run: bun run lint
- name: Build aft-bridge dist (workspace consumers depend on it)
run: bun run --cwd packages/aft-bridge build
- name: Build pi-plugin dist (Pi RPC tests need it)
# tests/pi-rpc spawns real Pi processes that load
# packages/pi-plugin/dist/index.js. Build it before bun test recurses
# into that workspace.
run: bun run --cwd packages/pi-plugin build
- name: Bun typecheck
run: bun run typecheck
# Governed publication surfaces (tool schemas, descriptions, workflow
# hints) are byte-pinned in docs/ manifests that live OUTSIDE the normal
# code-change workflow, so a merge touching a governed file leaves them
# stale silently. Unchecked, that drift only surfaces inside the release
# workflow's publish job — after the full test and build matrices have
# run — costing a dead tag and a re-cut. Checking it here fails the merge
# that caused the drift, next to the change that explains it.
#
# Linux-only: the audit reads source bytes and builds tool maps in-process,
# so it is platform-independent and does not need repeating per OS.
- name: Agent surface audit (governed manifest drift)
run: |
set -euo pipefail
shopt -s nullglob
audits=(scripts/audit-v[0-9]*-agent-surface.ts)
if [ ${#audits[@]} -eq 0 ]; then
echo "no versioned agent-surface audit present; skipping"
exit 0
fi
for audit in "${audits[@]}"; do
echo "→ bun $audit"
bun "$audit"
done
- name: Cargo build (debug — needed by plugin e2e tests)
run: cargo build -p agent-file-tools
- name: Rust test gate (unit on libtest, integration on nextest, watcher isolated)
run: ./scripts/rust-test-gate.sh
env:
# The checked-in profile reserves 2 cores for workstation headroom,
# which starves ~4-core CI runners. CI runners are otherwise idle,
# so use every core.
NEXTEST_TEST_THREADS: "num-cpus"
# The archive shards own the nextest integration inventory; keep the
# libtest and serialized watcher invocations byte-for-byte unchanged.
AFT_GATE_PHASES: lib,watcher
# The release-storm phase runs in its own parallel job
# (unit-linux-storm) so its release-profile build is off this
# job's critical path.
AFT_GATE_SKIP_RELEASE_STORM: "1"
- name: Build all JS packages (release gate)
# Only at release time: every published JS package must build. At PR
# time, individual package builds run inside their own test scripts.
if: inputs.strict == true
run: bun run --filter '*' build
# cortexkit/subconscious is a private repo, so the job-scoped
# GITHUB_TOKEN cannot read its releases. Family convention (also used
# by cortexkit-e2e / llm-runner / ai-proxy): mint a ~1h fine-grained
# cortexkit-ci App installation token per job — no PAT rotation.
# Secrets are absent on fork/contributor PRs (and until the repo gets
# them): skip the mint + fetch then (the subc e2e suites skip loudly)
# instead of failing CI. When the token IS minted, any fetch failure
# stays a hard failure.
- name: Check cross-repo app secrets
id: app_secrets
env:
APP_ID: ${{ secrets.CK_CI_APP_ID }}
run: echo "present=${APP_ID:+true}" >> "$GITHUB_OUTPUT"
- name: Mint cross-repo read token
id: app_token
if: steps.app_secrets.outputs.present == 'true'
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CK_CI_APP_ID }}
private-key: ${{ secrets.CK_CI_APP_PRIVATE_KEY }}
owner: cortexkit
repositories: subconscious
- name: Fetch pinned subc-core
id: subc_core
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo "::notice::cross-repo token unavailable (fork PR?) — subc e2e suites will skip"
echo "path=" >> "$GITHUB_OUTPUT"
exit 0
fi
path="$(./scripts/fetch-subc-core.sh)"
echo "path=$path" >> "$GITHUB_OUTPUT"
- name: Bun test (all packages)
run: bun run test:unit
env:
# Tests don't need real cache locations — keep them out of the runner's
# ~/.cache to avoid cross-test pollution.
AFT_CACHE_DIR: ${{ runner.temp }}/aft-cache
AFT_BINARY_PATH: ${{ github.workspace }}/target/debug/aft
SUBC_CORE_BIN: ${{ steps.subc_core.outputs.path }}
unit-linux-nextest:
name: Cargo integration shard ${{ matrix.shard }}/3 (Linux)
needs: unit-linux-nextest-archive
runs-on: ubuntu-22.04
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@nextest
- uses: actions/download-artifact@v4
with:
name: linux-nextest-archive
path: ${{ runner.temp }}
- name: Rust integration shard (nextest archive)
run: ./scripts/rust-test-gate.sh
env:
NEXTEST_TEST_THREADS: "num-cpus"
AFT_GATE_PHASES: nextest
AFT_NEXTEST_ARCHIVE_FILE: ${{ runner.temp }}/linux-nextest.tar.zst
# Extract under the checkout so compile-time CARGO_BIN_EXE paths from
# the archive builder resolve to the downloaded helper binaries.
AFT_NEXTEST_EXTRACT_TO: ${{ github.workspace }}
AFT_NEXTEST_PARTITION: hash:${{ matrix.shard }}/3
AFT_GATE_SKIP_RELEASE_STORM: "1"
# ---------------------------------------------------------------------------
# Real-kernel Landlock probes. This stays separate from the general unit lane
# so a kernel without the required ABI cannot turn sandbox coverage into a
# collection of silent skips.
# ---------------------------------------------------------------------------
sandbox-landlock-probes:
name: Sandbox real-kernel Landlock probes
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Build aft binary for sandbox probes
run: cargo build -p agent-file-tools --bin aft
- name: Run sandbox launch probes
run: cargo test -p agent-file-tools --test sandbox_launch_probe -- --test-threads=1 --nocapture
env:
AFT_REQUIRE_LANDLOCK: "1"
# ---------------------------------------------------------------------------
# Release-storm latency calibration (Linux, release profile).
# Split from unit-linux: the storm phase needs a release-profile build of
# the integration binary, which serialized 2-5 minutes behind the debug
# gate. Three warm-cache runs spent 9.8-10.9 minutes compiling/linking but
# only 1.0-1.1 minutes executing the 15 calibrated tests. sccache covers the
# workspace crates rust-cache deliberately excludes; the actual storm still
# runs alone on its dedicated runner so noisy shards cannot distort absolute
# latency bounds. Linux remains the release-storm arbiter; macOS and Windows
# skip the phase entirely.
# ---------------------------------------------------------------------------
unit-linux-storm:
name: Release storm (Linux)
runs-on: ubuntu-22.04
timeout-minutes: 25
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
CARGO_INCREMENTAL: "0"
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.11
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- uses: taiki-e/install-action@nextest
- name: Release storm tests (production-calibrated latency bounds)
run: cargo nextest run --cargo-profile release -p agent-file-tools --test integration -E 'test(subc_storm)'
# ---------------------------------------------------------------------------
# MSRV floor check (Linux, pinned to the declared rust-version).
#
# The locked oxc 0.126 dependency family requires rustc 1.92, so the real
# floor is set by a transitive dependency, not by AFT's own code. This job
# pins the toolchain to the declared rust-version and runs `cargo check
# --locked` so a floor bump in the lockfile fails here with a clear
# AFT-owned signal instead of a confusing transitive-dependency error on
# contributor machines. It runs in parallel with the other unit jobs and
# gates nothing, so it never extends the critical path.
# ---------------------------------------------------------------------------
unit-linux-msrv:
name: MSRV check (rustc 1.92)
runs-on: ubuntu-22.04
timeout-minutes: 25
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
CARGO_INCREMENTAL: "0"
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@1.92
- uses: mozilla-actions/sccache-action@v0.0.11
- uses: Swatinem/rust-cache@v2
with:
shared-key: unit-linux-msrv
cache-on-failure: true
- name: Cargo check (locked, MSRV floor)
run: cargo check --workspace --all-targets --locked
# ---------------------------------------------------------------------------
# Rust + plugin tests on macOS
#
# Warm-cache runs spent 7.5-9.2 minutes in the Rust gate and only 0.7-1.3
# minutes in the explicit debug build. Its 1,683-test nextest slice alone ran
# for 4.8-5.0 minutes, so it uses the same exact-inventory archive sharding as
# Linux. Extracted binaries are signed and prewarmed before timed execution;
# the keychain-sensitive libtest and serialized watcher stay together.
#
# Catches macOS-specific code paths: FSEvents watcher behavior (different
# coalescing latency from inotify), /var vs /private/var symlink
# canonicalization, broken-symlink-chain fallback, bash_background SIGTERM
# behavior, and Apple Silicon-specific Rust compilation. The build-darwin-*
# jobs in release.yml only run `cargo build` — this is the only place we
# actually execute tests on macOS in CI.
# ---------------------------------------------------------------------------
unit-macos-nextest-archive:
name: Cargo test archive (macOS)
runs-on: macos-26
timeout-minutes: 25
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
CARGO_INCREMENTAL: "0"
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.11
- uses: taiki-e/install-action@nextest
- uses: Swatinem/rust-cache@v2
with:
shared-key: unit-macos
cache-bin: false
cache-on-failure: true
- name: Build nextest archive (macOS integration suite)
run: cargo nextest archive --workspace -E 'kind(test) - binary(=watcher_integration)' --archive-file "$MACOS_NEXTEST_ARCHIVE"
env:
MACOS_NEXTEST_ARCHIVE: ${{ runner.temp }}/macos-nextest.tar.zst
- name: Prove split Rust test inventory is exact
run: python3 scripts/rust-test-inventory-proof.py
- name: Report nextest archive size
run: |
bytes=$(stat -f %z "$MACOS_NEXTEST_ARCHIVE")
printf 'macOS nextest archive: %.2f MiB (%s bytes)\n' "$(awk "BEGIN { print $bytes / 1048576 }")" "$bytes"
env:
MACOS_NEXTEST_ARCHIVE: ${{ runner.temp }}/macos-nextest.tar.zst
- uses: actions/upload-artifact@v4
with:
name: macos-nextest-archive
path: ${{ runner.temp }}/macos-nextest.tar.zst
compression-level: 0
if-no-files-found: error
unit-macos:
name: Unit tests (macOS)
runs-on: macos-26
timeout-minutes: 25
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
CARGO_INCREMENTAL: "0"
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@nextest
- uses: mozilla-actions/sccache-action@v0.0.11
# cache-bin: false is what makes rust-cache safe on macOS. The failure
# that previously banned it here (release runs 25839773442 and
# 25840332625) was the cached ~/.cargo/bin shim overwriting the
# just-installed cargo shim, which then misread its argv as
# rustup-init. Excluding the bin directory keeps the dependency
# artifacts (the multi-minute win) without touching the shims.
- uses: Swatinem/rust-cache@v2
with:
shared-key: unit-macos
cache-bin: false
cache-on-failure: true
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v5
with:
node-version: "24"
- name: Install workspace deps
run: bun install --frozen-lockfile
# Fail fast: cheapest checks first (see the Linux job).
- name: Bun lint
run: bun run lint
- name: Build aft-bridge dist (workspace consumers depend on it)
run: bun run --cwd packages/aft-bridge build
- name: Build pi-plugin dist (Pi RPC tests need it)
run: bun run --cwd packages/pi-plugin build
- name: Bun typecheck
run: bun run typecheck
- name: Cargo build (debug — needed by plugin e2e tests)
run: cargo build -p agent-file-tools
- name: Rust test gate (unit on libtest, integration on nextest, watcher isolated)
run: ./scripts/rust-test-gate.sh
env:
AFT_TEST_DIAG: "1"
# See the Linux job: full core count on otherwise-idle CI runners.
NEXTEST_TEST_THREADS: "num-cpus"
# Archive shards own the integration inventory. These phases preserve
# the keychain-sensitive libtest and serialized watcher behavior.
AFT_GATE_PHASES: lib,watcher
# The storm tests exercise no macOS-specific paths; the dedicated
# unit-linux-storm job is the release-storm arbiter.
AFT_GATE_SKIP_RELEASE_STORM: "1"
# See the Linux job: private-repo release read via the cortexkit-ci
# App token; absent secrets (fork PR / not yet configured) skip
# instead of failing.
- name: Check cross-repo app secrets
id: app_secrets
env:
APP_ID: ${{ secrets.CK_CI_APP_ID }}
run: echo "present=${APP_ID:+true}" >> "$GITHUB_OUTPUT"
- name: Mint cross-repo read token
id: app_token
if: steps.app_secrets.outputs.present == 'true'
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CK_CI_APP_ID }}
private-key: ${{ secrets.CK_CI_APP_PRIVATE_KEY }}
owner: cortexkit
repositories: subconscious
- name: Fetch pinned subc-core
id: subc_core
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo "::notice::cross-repo token unavailable (fork PR?) — subc e2e suites will skip"
echo "path=" >> "$GITHUB_OUTPUT"
exit 0
fi
path="$(./scripts/fetch-subc-core.sh)"
echo "path=$path" >> "$GITHUB_OUTPUT"
- name: Bun test (all packages)
run: bun run test:unit
env:
AFT_CACHE_DIR: ${{ runner.temp }}/aft-cache
AFT_BINARY_PATH: ${{ github.workspace }}/target/debug/aft
SUBC_CORE_BIN: ${{ steps.subc_core.outputs.path }}
unit-macos-nextest:
name: Cargo integration shard ${{ matrix.shard }}/3 (macOS)
needs: unit-macos-nextest-archive
runs-on: macos-26
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@nextest
- uses: actions/download-artifact@v4
with:
name: macos-nextest-archive
path: ${{ runner.temp }}
- name: Rust integration shard (nextest archive)
run: ./scripts/rust-test-gate.sh
env:
NEXTEST_TEST_THREADS: "num-cpus"
AFT_GATE_PHASES: nextest
AFT_NEXTEST_ARCHIVE_FILE: ${{ runner.temp }}/macos-nextest.tar.zst
# GitHub gives every job the same checkout path. Recreating target/
# there preserves compile-time helper paths embedded in test binaries.
AFT_NEXTEST_EXTRACT_TO: ${{ github.workspace }}
AFT_NEXTEST_PARTITION: hash:${{ matrix.shard }}/3
AFT_GATE_SKIP_RELEASE_STORM: "1"
# ---------------------------------------------------------------------------
# Rust integration tests on Windows
# Catches platform-conditional code paths (#[cfg(target_os = "windows")])
# and Windows process/path/signal differences. Same suite as Linux but on a
# real Windows runner — does NOT spin up OpenCode, that's the e2e job.
#
# The integration phase is the Windows critical path: recent warm-cache runs
# spent 9–16 minutes executing it, versus roughly four minutes compiling.
# Build its nextest archive once, then partition the same test inventory across
# three runners. Libtest phases remain together because their thousands of
# short tests are faster in one process, and watcher tests keep their existing
# single-threaded libtest execution.
#
# PR and release modes are blocking. Integration tests serialize filesystem
# paths through serde_json before writing NDJSON requests, so Windows
# backslashes are escaped correctly instead of being parsed as JSON escapes.
# ---------------------------------------------------------------------------
unit-windows-nextest-archive:
name: Cargo test archive (Windows)
runs-on: windows-2022
timeout-minutes: 25
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
# Both the archive builder and the libtest job use the established
# unit-windows-cargo cache key. Keeping that key stable preserves the
# existing hot MSVC dependency cache while the jobs run independently.
- uses: Swatinem/rust-cache@v2
with:
shared-key: unit-windows-cargo
cache-on-failure: true
- uses: taiki-e/install-action@nextest
- name: Build nextest archive (Windows integration suite)
run: cargo nextest archive --workspace -E "kind(test) - binary(=watcher_integration)" --archive-file "$env:WINDOWS_NEXTEST_ARCHIVE"
shell: pwsh
env:
WINDOWS_NEXTEST_ARCHIVE: ${{ runner.temp }}/windows-nextest.tar.zst
- name: Report nextest archive size
run: |
$bytes = (Get-Item $env:WINDOWS_NEXTEST_ARCHIVE).Length
"Windows nextest archive: {0:N2} MiB ({1:N0} bytes)" -f ($bytes / 1MB), $bytes
shell: pwsh
env:
WINDOWS_NEXTEST_ARCHIVE: ${{ runner.temp }}/windows-nextest.tar.zst
# The archive is already zstd-compressed. Disabling the artifact action's
# second compression avoids spending CPU to recompress incompressible data.
- uses: actions/upload-artifact@v4
with:
name: windows-nextest-archive
path: ${{ runner.temp }}/windows-nextest.tar.zst
compression-level: 0
if-no-files-found: error
unit-windows-libtest:
name: Cargo libtest + watcher (Windows)
runs-on: windows-2022
timeout-minutes: 25
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- uses: Swatinem/rust-cache@v2
with:
shared-key: unit-windows-cargo
cache-on-failure: true
- uses: taiki-e/install-action@nextest
- name: Rust libtest + watcher gate (Windows)
# Keep the libtest and watcher invocations from the normal gate exactly:
# libtest avoids per-test process fan-out, while watcher tests share
# fixtures and must remain single-threaded.
run: ./scripts/rust-test-gate.sh
shell: bash
env:
AFT_TEST_DIAG: "1"
AFT_GATE_PHASES: lib,watcher
AFT_GATE_SKIP_RELEASE_STORM: "1"
unit-windows-cargo:
name: Cargo integration shard ${{ matrix.shard }}/3 (Windows)
needs: unit-windows-nextest-archive
runs-on: windows-2022
timeout-minutes: 25
strategy:
fail-fast: false
# Release also schedules two shipping builds and native E2E on Windows.
# Capping strict mode at two avoids leaving one shard queued behind that
# fleet burst; PR mode keeps all three-way parallelism.
max-parallel: ${{ inputs.strict && 2 || 3 }}
matrix:
shard: [1, 2, 3]
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- uses: taiki-e/install-action@nextest
- uses: actions/download-artifact@v4
with:
name: windows-nextest-archive
path: ${{ runner.temp }}
- name: Rust integration shard (nextest archive)
# The archive exports runtime paths for helper binaries. Hash partitioning
# assigns every non-watcher integration test to one shard.
run: ./scripts/rust-test-gate.sh
shell: bash
env:
AFT_TEST_DIAG: "1"
NEXTEST_TEST_THREADS: "4"
AFT_GATE_PHASES: nextest
AFT_NEXTEST_ARCHIVE_FILE: ${{ runner.temp }}/windows-nextest.tar.zst
AFT_NEXTEST_PARTITION: hash:${{ matrix.shard }}/3
AFT_GATE_SKIP_RELEASE_STORM: "1"
# ---------------------------------------------------------------------------
# Cross-target warning gate (Windows, compile-only, DENIES warnings).
# Split from unit-windows-cargo: with unique RUSTFLAGS it shares no
# artifacts with the test build, so running it inline added its full
# duration to the Windows critical path. Catches unix-cfg dead-code
# warnings the mac/Linux clippy gates structurally cannot see.
# ---------------------------------------------------------------------------
unit-windows-check:
name: Cargo check deny-warnings (Windows)
runs-on: windows-2022
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Cargo check (deny warnings)
run: cargo check --workspace --all-targets
shell: pwsh
env:
RUSTFLAGS: "-D warnings"
# ---------------------------------------------------------------------------
# Bash permission flow on Windows
# Scoped to bash.test.ts only: covers the runAsk + Effect runtime path on
# Windows for OS-parity peace of mind. The runAsk fix itself is platform-
# agnostic JavaScript semantics — Linux/macOS in the unit-linux and
# unit-macos jobs already cover that. This Windows job is intentionally
# scoped to the bash permission bridge; Rust integration coverage is handled
# by unit-windows-cargo.
#
# Why a real bun runner on Windows: this exercises the BinaryBridge
# spawning aft.exe over NDJSON via stdio on the Windows process model. It
# catches a class of bugs that Linux+macOS bun + Windows native E2E miss
# individually — Effect-runtime semantics under bun's Windows process
# spawn implementation.
#
# Within bash.test.ts, tests asserting Unix-shell OUTPUT SEMANTICS (exact raw
# byte equality, POSIX `pwd` shape, cat/grep rewrite over forward-slash paths)
# are skipOnWindows-skipped by design: AFT's hoisted bash runs through
# PowerShell with backslash Windows paths, those rewrite scenarios are already
# Unix-only (crates/aft/tests/integration/bash_rewrite_test.rs), and Linux+macOS
# cover them. The permission-flow tests this job exists for run and gate here.
#
# PR mode (strict=false): non-blocking.
# Release mode (strict=true): blocking.
# ---------------------------------------------------------------------------
unit-windows-bash:
name: Bash permission e2e (Windows)
runs-on: windows-2022
timeout-minutes: 20
continue-on-error: ${{ inputs.strict != true }}
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v5
with:
node-version: "24"
- name: Install workspace deps
run: bun install --frozen-lockfile
shell: pwsh
- name: Build aft-bridge dist (workspace consumers depend on it)
run: bun run --cwd packages/aft-bridge build
- name: Build pi-plugin dist (Pi RPC tests need it)
run: bun run --cwd packages/pi-plugin build
shell: pwsh
- name: Cargo build (debug — bash.test.ts spawns the real aft binary)
run: cargo build -p agent-file-tools
shell: pwsh
- name: Bun test (bash permission flow — full bridge + plugin + Rust)
run: bun test src/__tests__/e2e/bash.test.ts
working-directory: packages/opencode-plugin
shell: pwsh
env:
AFT_CACHE_DIR: ${{ runner.temp }}/aft-cache