-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathjustfile
More file actions
1804 lines (1710 loc) · 81.7 KB
/
Copy pathjustfile
File metadata and controls
1804 lines (1710 loc) · 81.7 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
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
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Capsem Justfile
#
# Internal helpers:
# _ensure-setup checks for .dev-setup sentinel, runs doctor if missing (auto first-run)
# _install-tools auto-installs rust targets, components, cargo tools
# _check-assets verifies VM assets exist, runs build-assets if not
# _pack-initrd cross-compiles guest binaries + repacks initrd
# _sign builds host binaries + codesigns (macOS only, required for VZ)
# _ensure-service kills any running service, launches a fresh one, waits for socket
#
# User-facing recipe chains:
# shell -> _check-assets + _pack-initrd + _ensure-service + TUI
# ui -> _ensure-setup + _pnpm-install + run-service (service + Tauri dev hot-reload)
# run-service -> _check-assets + _pack-initrd + _ensure-service (start daemon, idempotent)
# exec +CMD -> run-service (one-shot command in a fresh temp VM)
# build-assets -> _install-tools + _clean-stale + inline doctor (kernel + rootfs, profile-aware)
# build-ui -> _frontend-dist (pnpm build + cargo build -p capsem-app, in lockstep)
# run-ui *ARGS -> build-ui (launch ./target/debug/capsem-app)
# smoke -> _install-tools + _frontend-dist + _check-assets + _pack-initrd + _ensure-service
# (audit, doctor --fast, injection, integration, parallel pytest groups)
# test -> _install-tools + _clean-stale + _frontend-dist + _generate-settings
# + _check-assets + _pack-initrd (everything: audit, cov, cross-compile,
# frontend, python, injection, integration, bench, test-install)
# benchmark -> _ensure-setup + _check-assets + _pack-initrd + _ensure-service
# (standard artifact-recording performance suite)
# bench -> benchmark
# test-gateway -> (no deps; unit + mock UDS tests)
# test-gateway-e2e -> _check-assets + _pack-initrd + _sign (real service + VMs)
# test-install -> _build-host (Docker e2e: build .deb, dpkg -i, pytest)
# install -> _pnpm-install + _stamp-version + profile-derived asset rebuild + _pack-initrd
# (hard clean + native package install + status capture + guest DNS/HTTPS gate)
# cut-release -> test + _stamp-version (commits changelog and creates a local tag)
# release [tag] -> (waits for CI on a pushed tag)
#
# First-time setup:
# just doctor (shows what's missing; `just doctor fix` auto-installs)
# just build-assets (builds kernel + rootfs -- needs docker via Colima on macOS)
#
# Daily dev: just shell (service daemon + TUI, ~10s)
# just ui (service + Tauri GUI with hot-reload)
# just exec "<cmd>" (one-shot command in a temp VM)
# Local install: just install (hard clean + native package install + status/VM network gate)
# Releases: just cut-release (test + bump + local tag; push main/tag manually)
# Dep maintenance: just update-deps (cargo update + pnpm update)
# just update-prices (refresh genai-prices.json)
# just update-fixture <src> (rebuild test.db fixture)
# Debugging: just logs, just sandbox-logs <id>, just list-sessions,
# just inspect-session [id], just query-session "SQL"
# Disk cleanup: just clean (nuke target/ + frontend build, ~100 GB)
# just clean all (clean + docker prune)
binary := "target/debug/capsem"
cli_binary := "target/debug/capsem"
service_binary := "target/debug/capsem-service"
process_binary := "target/debug/capsem-process"
mcp_binary := "target/debug/capsem-mcp"
gateway_binary := "target/debug/capsem-gateway"
host_binaries := "target/debug/capsem target/debug/capsem-service target/debug/capsem-process target/debug/capsem-mcp target/debug/capsem-mcp-aggregator target/debug/capsem-mcp-builtin target/debug/capsem-gateway target/debug/capsem-tray target/debug/capsem-tui"
assets_dir := "assets"
default_asset_profile := "config/profiles/base/coding.profile.toml"
entitlements := "entitlements.plist"
host_crates := "-p capsem-service -p capsem-process -p capsem -p capsem-mcp -p capsem-mcp-aggregator -p capsem-mcp-builtin -p capsem-gateway -p capsem-tray -p capsem-tui"
# Stamp version as 1.2.{unix_timestamp} in Cargo.toml, tauri.conf.json, and pyproject.toml.
_stamp-version:
#!/bin/bash
set -euo pipefail
CURRENT=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
NEW="${CAPSEM_RELEASE_VERSION:-1.2.$(date +%s)}"
echo "Stamping version: ${CURRENT} -> ${NEW}"
sed -i '' "s/^version = \"${CURRENT}\"/version = \"${NEW}\"/" Cargo.toml
sed -i '' "s/\"version\": \"${CURRENT}\"/\"version\": \"${NEW}\"/" crates/capsem-app/tauri.conf.json
sed -i '' "s/^version = \"${CURRENT}\"/version = \"${NEW}\"/" pyproject.toml
# Compile all host binaries
_build-host:
cargo build {{host_crates}}
# Codesign all host binaries (macOS only, needed for Virtualization.framework)
_sign: _build-host
#!/bin/bash
if [[ "$(uname -s)" == "Darwin" ]]; then
for bin in {{host_binaries}}; do
codesign --sign - --entitlements {{entitlements}} --force "$bin"
done
fi
# Ensure capsem-service daemon is running with the current binary.
# Kills any existing dev-owned instance (via pidfile -- never pkill-by-name)
# and relaunches fresh. Honors CAPSEM_HOME / CAPSEM_RUN_DIR env vars so
# `just test` and `just smoke` can run against an isolated test home
# without ever touching the user's locally installed capsem.
_ensure-service: _sign
#!/bin/bash
set -euo pipefail
arch=$(uname -m)
[[ "$arch" == "arm64" ]] || arch="x86_64"
# Resolve capsem home + run dir from env, matching the Rust helpers.
CAPSEM_HOME_DIR="${CAPSEM_HOME:-$HOME/.capsem}"
RUN_DIR="${CAPSEM_RUN_DIR:-$CAPSEM_HOME_DIR/run}"
mkdir -p "$RUN_DIR"
PIDFILE="$RUN_DIR/service.pid"
GATEWAY_PIDFILE="$RUN_DIR/gateway.pid"
SOCKET="$RUN_DIR/service.sock"
socket_owner_pids() {
lsof -nU 2>/dev/null | awk -v socket="$SOCKET" 'index($0, socket) { print $2 }' | sort -u
}
kill_service_tree() {
local pid="$1"
if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then
return
fi
kill "$pid" 2>/dev/null || true
for _ in 1 2 3 4 5 6; do
kill -0 "$pid" 2>/dev/null || break
sleep 0.25
done
if kill -0 "$pid" 2>/dev/null; then
pgrep -P "$pid" | xargs -r kill -9 2>/dev/null || true
kill -9 "$pid" 2>/dev/null || true
fi
}
pid_command() {
local pid="$1"
ps -p "$pid" -o command= 2>/dev/null || true
}
cleanup_runtime_processes() {
# Kill ONLY the service this pidfile tracks -- no pkill by name.
# Killing by pattern would take down a user's locally installed capsem
# (or a parallel test run with a different CAPSEM_HOME).
if [ -f "$PIDFILE" ]; then
OLD_PID=$(cat "$PIDFILE" 2>/dev/null || true)
kill_service_tree "$OLD_PID"
fi
if [ -f "$GATEWAY_PIDFILE" ]; then
OLD_GATEWAY_PID=$(cat "$GATEWAY_PIDFILE" 2>/dev/null || true)
kill_service_tree "$OLD_GATEWAY_PID"
fi
# A stale pidfile is not enough proof that the socket is free: a previous
# isolated smoke/test service can survive with the same run dir and cause
# the fresh service to exit as "already running" before it owns the gateway.
if [ -S "$SOCKET" ]; then
for SOCKET_PID in $(socket_owner_pids); do
kill_service_tree "$SOCKET_PID"
done
for _ in 1 2 3 4 5 6 7 8; do
[ -z "$(socket_owner_pids)" ] && break
sleep 0.25
done
REMAINING_SOCKET_PIDS=$(socket_owner_pids)
if [ -n "$REMAINING_SOCKET_PIDS" ]; then
echo "ERROR: could not clear existing capsem-service socket owners: $REMAINING_SOCKET_PIDS" >&2
exit 1
fi
fi
if [ -f "$RUN_DIR/gateway.lock" ]; then
LOCK_PID=$(cat "$RUN_DIR/gateway.lock" 2>/dev/null || true)
if [ -z "$LOCK_PID" ] || ! kill -0 "$LOCK_PID" 2>/dev/null; then
rm -f "$RUN_DIR/gateway.lock"
else
LOCK_CMD=$(pid_command "$LOCK_PID")
if [[ "$LOCK_CMD" == *"capsem-gateway"* && "$LOCK_CMD" == *"$RUN_DIR"* ]]; then
kill_service_tree "$LOCK_PID"
else
rm -f "$RUN_DIR/gateway.lock"
fi
fi
fi
rm -f "$PIDFILE" "$GATEWAY_PIDFILE" "$SOCKET" "$RUN_DIR/gateway.lock" "$RUN_DIR/gateway.token" "$RUN_DIR/gateway.port"
}
cleanup_runtime_processes
# Symlink <capsem_home>/assets -> repo assets so installed tools (MCP, CLI)
# see the same repacked initrd as the dev service.
ASSETS_LINK="$CAPSEM_HOME_DIR/assets"
DEV_ASSETS="$(cd "{{assets_dir}}" && pwd)"
if [ -L "$ASSETS_LINK" ]; then
CURRENT=$(readlink "$ASSETS_LINK")
if [ "$CURRENT" != "$DEV_ASSETS" ]; then
ln -sfn "$DEV_ASSETS" "$ASSETS_LINK"
echo "Updated $ASSETS_LINK -> $DEV_ASSETS"
fi
elif [ -d "$ASSETS_LINK" ]; then
# Real directory from install -- replace with symlink for dev.
# Only happens on the default ~/.capsem layout; test homes start empty.
rm -rf "$ASSETS_LINK.installed"
mv "$ASSETS_LINK" "$ASSETS_LINK.installed"
ln -sfn "$DEV_ASSETS" "$ASSETS_LINK"
echo "Saved $ASSETS_LINK.installed, symlinked $ASSETS_LINK -> $DEV_ASSETS"
else
mkdir -p "$CAPSEM_HOME_DIR"
ln -sfn "$DEV_ASSETS" "$ASSETS_LINK"
echo "Symlinked $ASSETS_LINK -> $DEV_ASSETS"
fi
# Refresh the local development profile after every initrd repack. The
# profile pins asset hashes, so leaving it stale makes the service reject
# the freshly repacked initrd before the VM can boot.
CAPSEM_ASSETS_DIR="${CAPSEM_ASSETS_DIR:-$DEV_ASSETS}" {{cli_binary}} setup --non-interactive --accept-detected
cleanup_runtime_processes
GATEWAY_ARGS=(--gateway-binary {{gateway_binary}})
if [ -n "${CAPSEM_RUN_DIR:-}" ]; then
# Isolated smoke/test services must not contend with a locally
# installed gateway on the default developer port.
GATEWAY_ARGS+=(--gateway-port 0)
fi
echo "Starting capsem-service (CAPSEM_HOME=$CAPSEM_HOME_DIR)..."
cleanup_runtime_processes
# Close fd 3 on the service; otherwise the backgrounded service inherits
# the execution-lock fd from `just smoke` / `just test` and keeps the
# flock held after the outer shell exits, blocking subsequent runs.
RUST_LOG="${RUST_LOG:-capsem=debug}" nohup {{service_binary}} \
--assets-dir {{assets_dir}}/$arch \
--process-binary {{process_binary}} \
"${GATEWAY_ARGS[@]}" \
--foreground 3>&- >/dev/null 2>&1 &
SVC_PID=$!
echo "$SVC_PID" > "$PIDFILE"
for i in $(seq 1 30); do
if ! kill -0 "$SVC_PID" 2>/dev/null; then
echo "ERROR: capsem-service exited during startup"
rm -f "$PIDFILE"
exit 1
fi
if [ -S "$SOCKET" ] && curl -s --unix-socket "$SOCKET" --max-time 2 http://localhost/list >/dev/null 2>&1; then
if [ -n "${CAPSEM_RUN_DIR:-}" ]; then
for _ in 1 2 3 4 5 6 7 8 9 10; do
[ -s "$RUN_DIR/gateway.token" ] && [ -s "$RUN_DIR/gateway.port" ] && break
sleep 0.25
done
if [ ! -s "$RUN_DIR/gateway.token" ] || [ ! -s "$RUN_DIR/gateway.port" ]; then
echo "ERROR: capsem-gateway did not publish token/port files"
kill "$SVC_PID" 2>/dev/null || true
rm -f "$PIDFILE"
exit 1
fi
fi
echo "capsem-service running (PID $SVC_PID)"
exit 0
fi
sleep 0.5
done
echo "ERROR: capsem-service did not start within 15s"
kill $SVC_PID 2>/dev/null
rm -f "$PIDFILE"
exit 1
# Start service daemon + Tauri GUI with hot-reloading
ui: _ensure-setup _pnpm-install run-service
#!/bin/bash
set -euo pipefail
source {{justfile_directory()}}/scripts/lib/exec_lock.sh
acquire_exec_lock "$HOME/.capsem/run/execution.lock"
CAPSEM_ASSETS_DIR={{assets_dir}} cargo tauri dev --config crates/capsem-app/tauri.conf.json
# Frontend-only dev server with mock data (no Tauri/VM needed)
dev-frontend: _pnpm-install
cd frontend && pnpm run dev
# Standalone terminal control-plane shell.
# App-owned controls: Alt+Left/Right switch sessions; Alt+1..9 jumps;
# Alt+n new, Alt+f fork, Alt+r resume, Alt+s suspend, Alt+c checkpoint,
# Alt+t stop, Alt+d delete, Alt+q quit;
# Alt+? help, Alt+i session info, Alt+l sessions. Plain q/Ctrl-C pass to the VM.
# Pass extra args after `--`: `just dev-tui -- --snapshot`.
dev-tui *ARGS:
cargo run -p capsem-tui {{ARGS}}
# Build the Tauri desktop app (capsem-app) with a fresh frontend bundle.
# IMPORTANT: the Tauri binary embeds frontend/dist at cargo compile time via
# tauri::generate_context!(), so rebuilding only the frontend has no effect
# on the running binary. This recipe keeps the two in lockstep.
# just build-ui # debug binary at ./target/debug/capsem-app
# just build-ui release # release binary at ./target/release/capsem-app
build-ui profile="debug": _frontend-dist
#!/bin/bash
set -euo pipefail
echo "=== capsem-app ({{profile}}) build ==="
if [[ "{{profile}}" == "release" ]]; then
cargo build -p capsem-app --release
echo ""
echo "Built ./target/release/capsem-app"
else
cargo build -p capsem-app
echo ""
echo "Built ./target/debug/capsem-app"
fi
# Run the Tauri desktop app after a clean frontend+binary rebuild.
# Pass extra args after `--`: `just run-ui -- --connect <vm-id>`.
run-ui *ARGS: build-ui
#!/bin/bash
set -euo pipefail
pkill -f "target/(debug|release)/capsem-app" 2>/dev/null || true
sleep 1
./target/debug/capsem-app {{ARGS}}
# Start service daemon + open the TUI (~10s after first build)
shell: _check-assets _pack-initrd _ensure-service
#!/bin/bash
set -euo pipefail
source {{justfile_directory()}}/scripts/lib/exec_lock.sh
acquire_exec_lock "$HOME/.capsem/run/execution.lock"
{{cli_binary}} shell
# Start capsem-service daemon (builds, signs, launches or reuses running instance)
run-service: _check-assets _pack-initrd _ensure-service
# Execute a command in a fresh temporary VM (auto-provisioned and destroyed)
# Usage: just exec "echo hello" or just exec "ls -la"
exec +CMD: run-service
#!/bin/bash
set -euo pipefail
source {{justfile_directory()}}/scripts/lib/exec_lock.sh
acquire_exec_lock "$HOME/.capsem/run/execution.lock"
{{cli_binary}} run "{{CMD}}"
# Build kernel only for one arch (CI-facing primitive).
build-kernel arch profile=default_asset_profile: _install-tools
#!/bin/bash
set -euo pipefail
test -f "{{profile}}" || { echo "ERROR: profile not found: {{profile}}" >&2; exit 1; }
uv run capsem-admin image build "{{profile}}" --arch {{arch}} --template kernel --out {{assets_dir}}/ --json
# Build rootfs only for one arch (CI-facing primitive).
build-rootfs arch profile=default_asset_profile: _install-tools
#!/bin/bash
set -euo pipefail
test -f "{{profile}}" || { echo "ERROR: profile not found: {{profile}}" >&2; exit 1; }
uv run capsem-admin image build "{{profile}}" --arch {{arch}} --template rootfs --out {{assets_dir}}/ --json
# VM asset rebuild (kernel + rootfs). Default: both arches. Pass arch and profile to build one.
build-assets arch="" profile=default_asset_profile: _install-tools _clean-stale
#!/bin/bash
set -euo pipefail
CAPSEM_SKIP_ASSET_CHECK=1 just doctor
test -f "{{profile}}" || { echo "ERROR: profile not found: {{profile}}" >&2; exit 1; }
if [[ -n "{{arch}}" ]]; then
bash scripts/build-assets.sh --profile "{{profile}}" --assets-dir "{{assets_dir}}" --arch "{{arch}}"
else
bash scripts/build-assets.sh --profile "{{profile}}" --assets-dir "{{assets_dir}}"
fi
just _docker-gc
# Run vulnerability audits (cargo audit + pnpm audit). Fast standalone gate.
# `just test` runs these too; this recipe is a quick pre-push check.
audit: _install-tools _pnpm-install
#!/bin/bash
set -euo pipefail
echo "=== cargo audit ==="
cargo audit
echo ""
echo "=== pnpm audit ==="
cd frontend && pnpm audit
echo ""
echo "Audits clean."
# Update all dependencies (Rust + npm) to latest compatible versions
update-deps: _pnpm-install
#!/bin/bash
set -euo pipefail
echo "=== Cargo update ==="
cargo update
echo ""
echo "=== Frontend update ==="
cd frontend && pnpm update
echo ""
echo "Done. Run 'just smoke' to verify nothing broke."
# Run ALL tests: Rust + frontend + Python + injection + integration + bench + cross-compile + install e2e. No shortcuts.
#
# Runs against an isolated CAPSEM_HOME under target/test-home/ so the suite
# never kills or mutates the user's locally installed capsem. The flock is
# still honored for multi-agent coordination but now lives inside the test
# home, not the shared ~/.capsem/run.
# Show the latest preserved test-artifacts directory after a red `just test`.
# Lists files + sizes and prints the `cat` hint -- saves digging through
# `ls -lt test-artifacts/` after a failure.
test-artifacts:
#!/usr/bin/env bash
set -euo pipefail
if [ ! -d test-artifacts ]; then
echo "No test-artifacts/ directory yet -- nothing has failed."
exit 0
fi
LATEST=$(ls -1t test-artifacts/ 2>/dev/null | head -1 || true)
if [ -z "$LATEST" ]; then
echo "test-artifacts/ is empty."
exit 0
fi
DIR="test-artifacts/$LATEST"
echo "Latest preserved failure: $DIR"
echo
echo "Top-level layout:"
find "$DIR" -maxdepth 3 -type f -exec stat -f ' %z %N' {} \; 2>/dev/null \
|| find "$DIR" -maxdepth 3 -type f -printf ' %s %P\n'
echo
echo "Hint:"
echo " cat $DIR/.../service.log | less"
echo " cat $DIR/.../sessions/<vm>/process.log | less"
test: _install-tools _clean-stale _frontend-dist _generate-settings _check-assets _pack-initrd
#!/bin/bash
set -euo pipefail
export CAPSEM_HOME="{{justfile_directory()}}/target/test-home/.capsem"
export CAPSEM_RUN_DIR="$CAPSEM_HOME/run"
export CAPSEM_ASSETS_DIR="{{justfile_directory()}}/{{assets_dir}}"
export TMPDIR="{{justfile_directory()}}/target/tmp"
# Lockfile lives OUTSIDE $CAPSEM_HOME so it survives `rm -rf $CAPSEM_HOME`
# below. Acquired BEFORE the wipe: if a second `just test` were to run
# past this line, the first's fd would be pinned to an unlinked inode
# and the second would flock a brand-new inode unchallenged.
source {{justfile_directory()}}/scripts/lib/exec_lock.sh
acquire_exec_lock "{{justfile_directory()}}/target/capsem-test-execution.lock"
cleanup_isolated_home_processes() {
[ -e "$CAPSEM_HOME" ] || return 0
PIDS=$(lsof -n 2>/dev/null | awk -v home="$CAPSEM_HOME" 'index($0, home) { print $2 }' | sort -u)
for PID in $PIDS; do
[ "$PID" != "$$" ] || continue
kill "$PID" 2>/dev/null || true
done
sleep 0.5
for PID in $PIDS; do
[ "$PID" != "$$" ] || continue
kill -9 "$PID" 2>/dev/null || true
done
}
cleanup_isolated_home_processes
rm -rf "$CAPSEM_HOME"
rm -rf "$TMPDIR"
mkdir -p "$CAPSEM_RUN_DIR" "$CAPSEM_HOME/sessions" "$CAPSEM_HOME/logs" "$TMPDIR"
# ---- Stage 1: parallel fast-fail (audits + lint + frontend) -------------
# Cheap, independent, most-common failure class. Clippy (not cargo check)
# is the Rust lint gate per CLAUDE.md -- it's a strict superset of check
# and covers --all-targets. `set -e` does not trip on failed background
# jobs, so aggregate with FAIL=1.
echo "=== Audits + lint + frontend (parallel) ==="
cargo audit & PID_CARGO_AUDIT=$!
(cd frontend && pnpm audit) & PID_PNPM_AUDIT=$!
cargo clippy --workspace --all-targets -- -D warnings & PID_CLIPPY=$!
(
cd frontend
pnpm run check
pnpm run test
) & PID_FE=$!
FAIL=0
wait $PID_CARGO_AUDIT || { echo "cargo audit failed"; FAIL=1; }
wait $PID_PNPM_AUDIT || { echo "pnpm audit failed"; FAIL=1; }
wait $PID_CLIPPY || { echo "cargo clippy failed (warnings = error)"; FAIL=1; }
wait $PID_FE || { echo "frontend (check/test/build) failed"; FAIL=1; }
[ $FAIL -eq 0 ] || exit 1
# ---- Stage 2: cross-arch agent cross-compile ----------------------------
# _pack-initrd already built the host arch; this validates the non-host
# arch compiles cleanly against musl, so a cross-arch regression surfaces
# before the Docker-based cross-compile at Stage 7.
echo "=== Cross-compile agent (both arches) ==="
uv run capsem-builder agent
# ---- Stage 3: Rust tests + coverage -------------------------------------
# Threshold is 65, not 100. Some files (uninstall, completions) are intentionally
# at 0% because they're thin shells over OS/CLI primitives. Some defensive paths
# (capsem-process IPC handlers, run_shell exit cleanup) only exercise under live
# VM traffic and are covered by integration tests under tests/, not unit tests.
# The floor exists to catch a "we deleted half the test suite" regression, not to
# gate every honest defensive-code addition.
echo "=== Rust: test suite with coverage ==="
cargo llvm-cov --workspace --no-cfg-coverage --fail-under-lines 65
# ---- Stage 4: sign host binaries for VM tests ---------------------------
echo "=== Sign binaries for integration tests ==="
just _sign
# ---- Stage 5: Python pytest, n=4 ----------------------------------------
# Dogfooding canary: 4 concurrent VMs. --dist=loadfile keeps per-file
# fixtures on the same worker. Any concurrency flake here is a Capsem-side
# bug.
#
# Serial/timing tests are intentionally excluded from this phase and run
# in Stage 6. They have their own load profiles and timing gates; mixing
# them into n=4 turns the gates into host-contention measurements instead
# of product regressions.
#
# --ignore=tests/capsem-recipes -- recipe meta-tests invoke `cargo build
# --workspace` via subprocess, which atomically replaces the codesigned
# binaries concurrent VM tests need. All their assertions are already
# covered by Stage 1 clippy + Stage 3 llvm-cov + Stage 4 _build-host.
# Still runnable standalone via `uv run pytest -m recipe`.
# --ignore=tests/capsem-install -- install-suite tests also spawn `cargo
# build -p capsem` from within pytest. This directory is owned by
# Stage 7's `just test-install`, which runs it inside Docker with
# CAPSEM_DEB_INSTALLED=1 (the skip flag live_system tests respect).
echo "=== Python: ALL tests (n=4 parallel) ==="
# CAPSEM_REQUIRE_ARTIFACTS=1: fail the suite if any of assets/<arch>/
# manifest.json, initrd.img, entitlements.plist, or target/linux-agent/
# <arch>/ is missing. Stages 1-4 already produced them (this recipe
# depends on _check-assets + _pack-initrd + _sign); if anything is
# absent it means an earlier stage silently dropped its output, and
# we want that to fail loudly here rather than manifest as a pile of
# individually-skipped tests whose absence goes unnoticed.
CAPSEM_REQUIRE_ARTIFACTS=1 uv run python -m pytest tests/ -v --tb=short -n 4 --dist=loadfile -m "not benchmark and not serial" \
--ignore=tests/capsem-recipes \
--ignore=tests/capsem-install \
--ignore=tests/capsem-build-chain \
--cov=src/capsem --cov-report=xml:codecov-python.xml --cov-fail-under=89
echo "=== Python: Build chain tests (serial) ==="
CAPSEM_REQUIRE_ARTIFACTS=1 uv run python -m pytest tests/capsem-build-chain/ -v --tb=short
# ---- Stage 6: legacy VM scripts + bench ---------------------------------
echo "=== Injection test ==="
python3 scripts/injection_test.py --binary {{binary}} --assets {{assets_dir}}
echo "=== Verify local asset manifest signature ==="
bash scripts/verify-local-manifest-signature.sh {{assets_dir}} config/manifest-sign.pub
echo "=== Integration test ==="
python3 scripts/integration_test.py --binary {{binary}} --assets {{assets_dir}}
echo "=== Serial timing + benchmarks ==="
# Runs host-side timing gates and diagnostics serially, plus records
# /tmp/capsem-benchmark.json to benchmarks/capsem-bench/data_<ver>_<arch>.json
# on every run so we accumulate a baseline.
CAPSEM_ASSETS_DIR={{assets_dir}} uv run python -m pytest \
tests/capsem-serial/ \
tests/capsem-e2e/test_e2e_lifecycle.py::TestDoctor::test_doctor_passes \
-v --tb=short -m "serial or benchmark"
# ---- Stage 7: Docker e2e ------------------------------------------------
echo "=== Cross-compile Linux release (Docker) ==="
just cross-compile
echo "=== Install e2e tests (Docker + systemd) ==="
just test-install
# ---- Stage 8: cleanup ---------------------------------------------------
echo "=== Pruning stale build artifacts ==="
just _clean-stale
# Build the capsem-host-builder Docker image (cached, only rebuilds changed layers).
# See docker/Dockerfile.host-builder for contents.
build-host-image:
#!/bin/bash
set -euo pipefail
echo "=== Building capsem-host-builder image ==="
docker build \
-t capsem-host-builder:latest \
-f docker/Dockerfile.host-builder \
docker/
# Remove cross-compilation image and cached volumes.
_clean-host-image:
#!/bin/bash
set -euo pipefail
docker rmi capsem-host-builder:latest 2>/dev/null || true
docker rmi capsem-install-test:latest 2>/dev/null || true
for vol in capsem-cargo-registry capsem-cargo-git capsem-host-target-arm64 capsem-host-target-x86_64 capsem-rustup capsem-install-target capsem-install-cargo capsem-install-rustup; do
docker volume rm "$vol" 2>/dev/null || true
done
echo "Cleaned host builder image and volumes."
# Build the full Linux release in a container (agent + deb).
# Uses the pre-built capsem-host-builder image (just build-host-image).
# Supports arm64 and x86_64 via native cross-compilation (no QEMU).
#
# The image runs natively on the host arch and cross-compiles via
# Rust --target + multiarch system libs. Named volumes cache cargo
# registry and build artifacts between runs. CARGO_TARGET_DIR=/cargo-target
# inside the container isolates from host macOS target/ directory.
#
# CI vs local divergences (keep in sync when changing either):
# - CI runs on bare ubuntu runners; this runs in capsem-host-builder via docker
# - Tauri signing keys: CI from secrets, local from private/tauri/
# - See: .github/workflows/release.yaml build-app-linux job
cross-compile arch="": _clean-stale _check-assets _generate-settings
#!/bin/bash
set -euo pipefail
ROOT="{{justfile_directory()}}"
# Default to host architecture
if [ -z "{{arch}}" ]; then
TARGET_ARCH=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x86_64/')
else
TARGET_ARCH="{{arch}}"
fi
if [ "$TARGET_ARCH" != "arm64" ] && [ "$TARGET_ARCH" != "x86_64" ]; then
echo "ERROR: unsupported arch '$TARGET_ARCH' (arm64 or x86_64)"
exit 1
fi
# Ensure build image exists
if ! docker image inspect capsem-host-builder:latest &>/dev/null; then
echo "=== Build image not found, building... ==="
just build-host-image
fi
# Sync container VM clock on macOS (prevents apt "not valid yet" errors)
if [[ "$(uname -s)" = "Darwin" ]]; then
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
docker run --rm --privileged alpine date -s "$NOW" 2>/dev/null || true
fi
# Map target arch to Rust triple, dpkg arch, and pkg-config paths
case "$TARGET_ARCH" in
x86_64)
RUST_TARGET="x86_64-unknown-linux-gnu"
DPKG_ARCH="amd64"
PKG_CONFIG_PATH_CROSS="/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig"
;;
arm64)
RUST_TARGET="aarch64-unknown-linux-gnu"
DPKG_ARCH="arm64"
PKG_CONFIG_PATH_CROSS="/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig"
;;
esac
# Sync assets layout for Tauri build
rm -rf assets/current
if [ -d "assets/$TARGET_ARCH" ]; then
cp -r "assets/$TARGET_ARCH" assets/current
: > assets/B3SUMS
for arch_dir in assets/*; do
[ -d "$arch_dir" ] || continue
arch_name=$(basename "$arch_dir")
if [ -f "$arch_dir/vmlinuz" ] && [ -f "$arch_dir/initrd.img" ] && [ -f "$arch_dir/rootfs.squashfs" ]; then
(cd assets && b3sum "$arch_name/vmlinuz" "$arch_name/initrd.img" "$arch_name/rootfs.squashfs" >> B3SUMS)
fi
done
python3 scripts/gen_manifest.py assets Cargo.toml
python3 scripts/create_hash_assets.py assets
bash scripts/sync-dev-assets.sh assets assets
bash scripts/verify-local-manifest-signature.sh assets config/manifest-sign.pub
touch crates/capsem-app/build.rs
fi
# If the host has the real release signing keys under private/tauri/,
# inject them into the container. Otherwise the container generates a
# throwaway dev-only key inline -- the authoritative release keys
# live in GitHub Actions secrets (TAURI_SIGNING_PRIVATE_KEY +
# TAURI_SIGNING_PRIVATE_KEY_PASSWORD in
# .github/workflows/release.yaml) and are only applied on publish.
# Dev builds just need SOME key so `cargo tauri build` can complete.
SIGNING_ARGS=()
if [ -f "$ROOT/private/tauri/capsem.key" ] && [ -f "$ROOT/private/tauri/password.txt" ]; then
TAURI_KEY=$(cat "$ROOT/private/tauri/capsem.key")
TAURI_PWD=$(cat "$ROOT/private/tauri/password.txt")
SIGNING_ARGS=(
-e "TAURI_SIGNING_PRIVATE_KEY=$TAURI_KEY"
-e "TAURI_SIGNING_PRIVATE_KEY_PASSWORD=$TAURI_PWD"
)
fi
echo "=== Building Linux deb ($TARGET_ARCH via docker, target=$RUST_TARGET) ==="
mkdir -p "$ROOT/dist"
# KVM boot test: pass host virtualization devices if available (Linux host)
# or skip on macOS/cross-arch builds.
KVM_FLAG=""
if [ -e /dev/kvm ]; then
KVM_FLAG="--device /dev/kvm"
fi
VSOCK_FLAG=""
if [ -e /dev/vhost-vsock ]; then
VSOCK_FLAG="--device /dev/vhost-vsock"
# Docker's default seccomp profile denies AF_VSOCK bind even when the
# vhost-vsock device is passed through, so the KVM boot test cannot
# accept guest vsock connections without this.
VSOCK_SECURITY_FLAG="--security-opt seccomp=unconfined"
else
VSOCK_SECURITY_FLAG=""
fi
# macOS ships Bash 3.2, where expanding an empty array under nounset
# raises "unbound variable". The signing args are intentionally optional.
set +u
docker run --rm \
$KVM_FLAG \
"${SIGNING_ARGS[@]}" \
$VSOCK_FLAG \
$VSOCK_SECURITY_FLAG \
-e "TARGET_ARCH=$TARGET_ARCH" \
-e "RUST_TARGET=$RUST_TARGET" \
-e "DPKG_ARCH=$DPKG_ARCH" \
-e "PKG_CONFIG_PATH=$PKG_CONFIG_PATH_CROSS" \
-v "$ROOT:/src" \
-v "capsem-cargo-registry:/usr/local/cargo/registry" \
-v "capsem-cargo-git:/usr/local/cargo/git" \
-v "capsem-host-target-$TARGET_ARCH:/cargo-target" \
-v "capsem-frontend-node-modules-$TARGET_ARCH:/src/frontend/node_modules" \
-v "capsem-rustup:/usr/local/rustup" \
-w /src \
capsem-host-builder:latest \
bash -c "swap-dev-libs \$DPKG_ARCH && \
echo '--- Build agent binaries ---' && \
cargo build --release --target \$RUST_TARGET -p capsem-agent && \
mkdir -p /cargo-target/linux-agent/\$TARGET_ARCH && \
cp /cargo-target/\$RUST_TARGET/release/capsem-pty-agent /cargo-target/\$RUST_TARGET/release/capsem-mcp-server /cargo-target/\$RUST_TARGET/release/capsem-net-proxy /cargo-target/\$RUST_TARGET/release/capsem-dns-proxy /cargo-target/\$RUST_TARGET/release/capsem-sysutil /cargo-target/linux-agent/\$TARGET_ARCH/ && \
echo '--- Build host binaries ---' && \
cargo build --release --target \$RUST_TARGET {{host_crates}} && \
UV_PROJECT_ENVIRONMENT=/cargo-target/capsem-package-venv bash scripts/prepare-admin-cli.sh /cargo-target/\$RUST_TARGET/release && \
echo '--- Build frontend ---' && \
cd frontend && CI=true pnpm install && pnpm build && cd .. && \
echo '--- Resolve Tauri signing key ---' && \
DEV_KEY=/cargo-target/dev-tauri-private && \
if [ -z \"\${TAURI_SIGNING_PRIVATE_KEY:-}\" ]; then \
if [ ! -f \"\$DEV_KEY\" ]; then \
echo ' no host signing key; generating dev-only key (not for release distribution)' && \
cargo tauri signer generate --ci --force -w \"\$DEV_KEY\" -p 'dev' >/dev/null; \
else \
echo \" reusing dev key at \$DEV_KEY\"; \
fi && \
export TAURI_SIGNING_PRIVATE_KEY=\$(cat \"\$DEV_KEY\") && \
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD='dev'; \
else \
echo ' using host-injected signing key'; \
fi && \
echo '--- Build Tauri app ---' && \
DEB_DIR=/cargo-target/\$RUST_TARGET/release/bundle/deb && \
rm -f \"\$DEB_DIR\"/*.deb && \
cd crates/capsem-app && cargo tauri build --target \$RUST_TARGET --bundles deb && cd ../.. && \
echo '--- Validate artifacts ---' && \
DEBS=(\"\$DEB_DIR\"/*.deb) && \
if [ \"\${#DEBS[@]}\" -ne 1 ] || [ ! -f \"\${DEBS[0]}\" ]; then \
echo \"ERROR: expected exactly one deb artifact in \$DEB_DIR\" >&2; \
ls -lah \"\$DEB_DIR\" >&2 || true; \
exit 1; \
fi && \
DEB=\"\${DEBS[0]}\" && \
PACKAGE_VERSION=\$(sed -n 's/^version = \"\\(.*\\)\"/\\1/p' Cargo.toml | head -1) && \
bash scripts/repack-deb.sh \"\$DEB\" /cargo-target/\$RUST_TARGET/release assets \"\$DEB\" && \
UV_PROJECT_ENVIRONMENT=/cargo-target/capsem-package-venv uv run python scripts/verify_deb_payload.py \"\$DEB\" --version \"\$PACKAGE_VERSION\" --architecture \"\$DPKG_ARCH\" --minisign-pubkey assets/manifest-sign.dev.pub && \
dpkg-deb --info \"\$DEB\" && \
rm -f /src/dist/Capsem_*_\"\$DPKG_ARCH\".deb && \
cp \"\$DEB\" /src/dist/ && \
cp /cargo-target/linux-agent/\$TARGET_ARCH/* /src/dist/ && \
echo '--- Boot test ---' && \
if [ -e /dev/kvm ] && [ \"\$TARGET_ARCH\" = \"\$(uname -m | sed 's/aarch64/arm64/')\" ]; then \
echo 'KVM available + native arch: running boot test' && \
dpkg --unpack \"\$DEB\" && \
timeout 120 python3 scripts/doctor_session_test.py --binary /usr/bin/capsem --assets assets; \
else \
echo 'Skipping boot test (no KVM or cross-arch -- CI will test)'; \
fi"
set -u
echo ""
echo "=== Artifacts ==="
ls -lh "$ROOT/dist/"
just _docker-gc
# Generate settings-schema.json, defaults.json, mcp-tools.json, and mock-data.generated.ts
_generate-settings:
#!/bin/bash
set -euo pipefail
LOG="target/build.log"
mkdir -p target
echo "[generate] $(date +%H:%M:%S) exporting MCP tool defs" >> "$LOG"
cargo run --bin mcp_export 2>>"$LOG" > config/mcp-tools.json
echo "[generate] $(date +%H:%M:%S) generating schema + defaults + mock" >> "$LOG"
uv run python scripts/generate_schema.py >> "$LOG" 2>&1
# Fast path: audit, doctor, injection, integration tests (no Docker, no cross-compile)
smoke: _install-tools _frontend-dist _check-assets _pack-initrd
#!/bin/bash
set -euo pipefail
# Smoke runs against an isolated CAPSEM_HOME so it doesn't stomp on a
# locally installed capsem daemon. _ensure-service is invoked below
# (not as a just dep) so it inherits the exported env vars.
export CAPSEM_HOME="{{justfile_directory()}}/target/test-home/.capsem"
export CAPSEM_RUN_DIR="$CAPSEM_HOME/run"
export CAPSEM_ASSETS_DIR="{{justfile_directory()}}/{{assets_dir}}"
# Lockfile lives OUTSIDE $CAPSEM_HOME so it survives `rm -rf $CAPSEM_HOME`
# below. Acquired BEFORE the wipe: if a second `just smoke` were to run
# past this line, the first's fd would be pinned to an unlinked inode
# and the second would flock a brand-new inode unchallenged.
source {{justfile_directory()}}/scripts/lib/exec_lock.sh
acquire_exec_lock "{{justfile_directory()}}/target/capsem-test-execution.lock"
# Wipe stale state so assertions that read <capsem_home>/logs or
# <capsem_home>/sessions don't trip on artifacts from a previous run
# (e.g. a 0-entry capsem-app launch log left by a crashed Tauri shell).
# Matches the `just test` preamble; smoke inherited the leak when
# CAPSEM_HOME isolation was introduced.
cleanup_isolated_home_processes() {
[ -e "$CAPSEM_HOME" ] || return 0
PIDS=$(lsof -n 2>/dev/null | awk -v home="$CAPSEM_HOME" 'index($0, home) { print $2 }' | sort -u)
for PID in $PIDS; do
[ "$PID" != "$$" ] || continue
kill "$PID" 2>/dev/null || true
done
sleep 0.5
for PID in $PIDS; do
[ "$PID" != "$$" ] || continue
kill -9 "$PID" 2>/dev/null || true
done
}
cleanup_isolated_home_processes
rm -rf "$CAPSEM_HOME"
mkdir -p "$CAPSEM_RUN_DIR" "$CAPSEM_HOME/sessions" "$CAPSEM_HOME/logs"
just _ensure-service
SMOKE_LOG="{{justfile_directory()}}/target/smoke.log"
mkdir -p "$(dirname "$SMOKE_LOG")"
exec > >(tee "$SMOKE_LOG") 2>&1
SMOKE_START=$SECONDS
step() { STEP_START=$SECONDS; echo "=== $1 ==="; }
step_done() { echo " -> $(( SECONDS - STEP_START ))s"; echo ""; }
step "Rust clippy + audits + frontend lint (parallel)"
# Clippy (superset of cargo check) is the lint gate per CLAUDE.md.
# Frontend `pnpm run check` runs here too so a broken Svelte/TS type
# fails smoke in seconds instead of only surfacing under `just test`.
# Background jobs don't trip `set -e`, so aggregate via FAIL=1.
cargo clippy --workspace --all-targets -- -D warnings & CLIPPY_PID=$!
cargo audit & AUDIT_PID=$!
(cd frontend && pnpm audit) & PNPM_AUDIT_PID=$!
(cd frontend && pnpm run check) & FE_CHECK_PID=$!
FAIL=0
wait $CLIPPY_PID || { echo "cargo clippy failed"; FAIL=1; }
wait $AUDIT_PID || { echo "cargo audit failed"; FAIL=1; }
wait $PNPM_AUDIT_PID || { echo "pnpm audit failed"; FAIL=1; }
wait $FE_CHECK_PID || { echo "pnpm check failed"; FAIL=1; }
[ $FAIL -eq 0 ] || exit 1
step_done
step "capsem-doctor --fast (in-VM diagnostics, no throughput)"
{{cli_binary}} doctor --fast
step_done
step "Injection test"
python3 scripts/injection_test.py --binary {{binary}} --assets {{assets_dir}}
step_done
step "Integration test"
python3 scripts/integration_test.py --binary {{binary}} --assets {{assets_dir}}
step_done
step "Python integration tests (MCP + service + CLI + gateway, parallel groups)"
# Pre-sign binaries so parallel test groups don't race on codesign
for b in {{service_binary}} {{process_binary}}; do
codesign --sign - --entitlements {{entitlements}} --force "$b" 2>/dev/null || true
done
# service+cli is the longest group (~67s serial) -- the big lever.
# -n 2 + --dist=loadfile cuts it to ~36s. loadfile keeps all tests in
# a file on the same worker so module-scoped fixtures don't rebuild.
# Suspend/resume is host-resource sensitive under Apple VZ. Keep those
# files out of the parallel phase and run them serially after the other
# service/gateway/MCP tests finish; otherwise unrelated VMs can make
# resume fail before the guest signals ready.
MCP_SERIAL="tests/capsem-mcp/test_state_transitions.py"
SVC_SERIAL=(
"tests/capsem-service/test_svc_resume_paths.py"
"tests/capsem-service/test_svc_suspend_corruption.py"
"tests/capsem-service/test_svc_loop_device_after_resume.py"
)
# Keep the two VM-heavy groups from overlapping. Both service+CLI and MCP
# boot/resume real VMs; running them at the same time can starve Apple VZ
# enough that otherwise healthy service requests hit client timeouts.
CAPSEM_TEST_RUN_ID=smoke-service-cli uv run python -m pytest tests/capsem-service/ tests/capsem-cli/ \
-v --tb=short -m "integration" -n 2 --dist=loadfile \
--ignore="${SVC_SERIAL[0]}" \
--ignore="${SVC_SERIAL[1]}" \
--ignore="${SVC_SERIAL[2]}" &
PID_SVC=$!
CAPSEM_TEST_RUN_ID=smoke-gateway uv run python -m pytest tests/capsem-gateway/ -v --tb=short -m "gateway" &
PID_GW=$!
FAIL=0
wait $PID_SVC || FAIL=1
wait $PID_GW || FAIL=1
[ $FAIL -eq 0 ] || { echo "Python tests failed"; exit 1; }
CAPSEM_TEST_RUN_ID=smoke-mcp uv run python -m pytest tests/capsem-mcp/ -v --tb=short -m "mcp" \
--ignore="$MCP_SERIAL"
CAPSEM_TEST_RUN_ID=smoke-mcp-serial uv run python -m pytest "$MCP_SERIAL" -v --tb=short -m "mcp"
CAPSEM_TEST_RUN_ID=smoke-service-serial uv run python -m pytest "${SVC_SERIAL[@]}" -v --tb=short -m "integration"
step_done
echo "Smoke test passed in $(( SECONDS - SMOKE_START ))s"
just _clean-stale
# Gateway unit + integration tests (no VM needed)
test-gateway:
#!/bin/bash
set -euo pipefail
echo "=== Gateway: Rust unit tests ==="
cargo test -p capsem-gateway -- --nocapture
echo ""
echo "=== Gateway: build binary ==="
cargo build -p capsem-gateway
echo ""
echo "=== Gateway: Python integration tests (mock UDS) ==="
uv run python -m pytest tests/capsem-gateway/ -v --tb=short -m "gateway and not e2e"
echo ""
echo "Gateway tests passed"
# Gateway E2E tests (requires capsem-service + VM assets)
test-gateway-e2e: _check-assets _pack-initrd _sign
#!/bin/bash
set -euo pipefail
source {{justfile_directory()}}/scripts/lib/exec_lock.sh
acquire_exec_lock "$HOME/.capsem/run/execution.lock"
cargo build -p capsem-gateway {{host_crates}}
echo "=== Gateway: E2E tests (real service + VMs) ==="
uv run python -m pytest tests/capsem-gateway/ -v --tb=short -m "gateway and e2e"
# Local HTML coverage report across all Rust crates
coverage:
#!/bin/bash
set -euo pipefail
cargo llvm-cov --workspace --no-cfg-coverage --html
echo "Coverage report: target/llvm-cov/html/index.html"
open target/llvm-cov/html/index.html 2>/dev/null || true
# Run the standard artifact-recording benchmark suite.
benchmark: _ensure-setup _check-assets _pack-initrd _ensure-service
#!/bin/bash
set -euo pipefail
source {{justfile_directory()}}/scripts/lib/exec_lock.sh
acquire_exec_lock "$HOME/.capsem/run/execution.lock"
echo "=== Preserve current benchmark artifacts ==="
uv run python scripts/archive_superseded_benchmark_artifacts.py --archive-current-arch
echo "=== Criterion microbenchmarks ==="
cargo bench -p capsem-security-engine --bench security_engine_cel
cargo bench -p capsem-core --bench security_packs
uv run python scripts/archive_criterion_benchmarks.py
echo "=== VM-originated and in-VM benchmark artifacts ==="
CAPSEM_ASSETS_DIR={{assets_dir}} uv run python -m pytest tests/capsem-serial/ -v --tb=short -m benchmark
echo "=== Archive superseded benchmark artifacts ==="
uv run python scripts/archive_superseded_benchmark_artifacts.py
# Backward-compatible alias for the canonical benchmark suite.
bench: benchmark
# Compare committed benchmark artifacts across Linux x86_64 and macOS arm64.
benchmark-compare:
uv run python scripts/compare_benchmark_artifacts.py
# Build package, runtime-clean local install, use the install.sh native command,
# then verify installed status, service, gateway, and guest DNS/HTTPS.
install: _pnpm-install _stamp-version _check-assets
#!/bin/bash
set -euo pipefail
# Strip test-isolation env vars so the installer never bakes a transient
# target/test-home path into the LaunchAgent / systemd unit. If the user
# was just running `just test` in this shell and exports lingered, the
# install would permanently embed a path that gets wiped on the next
# test run. `capsem install` also refuses these vars defensively.
unset CAPSEM_HOME CAPSEM_RUN_DIR CAPSEM_ASSETS_DIR
ROOT="{{justfile_directory()}}"
source "$ROOT/scripts/lib/exec_lock.sh"
acquire_exec_lock "$HOME/.capsem/run/execution.lock"
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
export CAPSEM_BUILD_TS=$(date +%y%m%d%H%M)
INSTALL_ASSETS_DIR="$ROOT/.capsem-assets/install"
CAPSEM_SETTINGS_BACKUP="$(mktemp -d "${TMPDIR:-/tmp}/capsem-settings.XXXXXX")"
cleanup_settings_backup() {
rm -rf "$CAPSEM_SETTINGS_BACKUP"
}
trap cleanup_settings_backup EXIT
preserve_setting() {
local rel="$1"
local src="$HOME/.capsem/$rel"
local dst="$CAPSEM_SETTINGS_BACKUP/$rel"
if [ -f "$src" ]; then
mkdir -p "$(dirname "$dst")"
cp -p "$src" "$dst"
elif [ -d "$src" ]; then
mkdir -p "$(dirname "$dst")"
cp -a "$src/." "$dst/"
fi
}
restore_setting() {
local rel="$1"
local src="$CAPSEM_SETTINGS_BACKUP/$rel"
local dst="$HOME/.capsem/$rel"
if [ -f "$src" ]; then
mkdir -p "$(dirname "$dst")"
cp -p "$src" "$dst"
elif [ -d "$src" ]; then
mkdir -p "$dst"
cp -a "$src/." "$dst/"
fi
}
assert_clean_uninstall() {
local failed=0
if [ -d "$HOME/.capsem/bin" ]; then
echo "ERROR: runtime bin dir still exists after uninstall:" >&2
find "$HOME/.capsem/bin" -maxdepth 2 -print >&2 || true
failed=1
fi
if [ -e "$HOME/Library/LaunchAgents/com.capsem.service.plist" ]; then
echo "ERROR: LaunchAgent still exists after uninstall" >&2
failed=1
fi
if [ -e "$HOME/.config/systemd/user/capsem.service" ]; then
echo "ERROR: systemd user unit still exists after uninstall" >&2
failed=1
fi
for name in capsem-service capsem-process capsem-gateway capsem-tray; do
if pgrep -f "$HOME/.capsem/bin/$name" >/dev/null 2>&1; then
echo "ERROR: $name from ~/.capsem/bin is still running after uninstall" >&2
failed=1
fi
done
if [ -d "$HOME/.capsem/run" ]; then
local runtime_left
runtime_left=$(find "$HOME/.capsem/run" -mindepth 1 -maxdepth 1 \
! -name persistent \
! -name persistent_registry.json \
-print 2>/dev/null || true)
if [ -n "$runtime_left" ]; then
echo "ERROR: runtime run-state still exists after uninstall:" >&2
echo "$runtime_left" >&2
failed=1
fi