Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 1 | #!/usr/bin/env bash |
| 2 | # End-to-end checks of hosted projects: two people, a reader, two machines, |
| 3 | # takeovers, waiting changes, offline work and 32 agents landing at once. |
| 4 | # Against the self-hosted onset-server by default; with ONSET_TARGET=worker, |
| 5 | # against the hosted service (apps/onset) under `wrangler dev`, in its test |
| 6 | # sign-in mode. |
| 7 | # Usage: [ONSET_TARGET=worker] tests/hosted.sh [path/to/target/debug] |
| 8 | set -uo pipefail |
| 9 |
| 10 | ROOT=$(cd "$(dirname "$0")/.." && pwd) |
| 11 | TESTS=$ROOT/tests |
| 12 | BIN=${1:-$ROOT/target/debug} |
| 13 | TARGET=${ONSET_TARGET:-server} |
| 14 | # Windows (WINDOWS=1, or Git Bash there) has no executable bit on disk; Onset keeps it in the tree. |
| 15 | case "${WINDOWS:-$(uname -s)}" in MINGW* | MSYS* | CYGWIN* | 1) WINDOWS=1 ;; *) WINDOWS= ;; esac |
| 16 | BIN_EXE= |
| 17 | [ -n "$WINDOWS" ] && [ -x "$BIN/on.exe" ] && BIN_EXE=.exe |
| 18 | VCS=$BIN/on$BIN_EXE |
| 19 | SERVER=$BIN/onset-server$BIN_EXE |
| 20 | WORK=$(mktemp -d) |
| 21 | export ONSET_SAVER=0 |
| 22 | # Run from inside an agent, its own session would be kept with the tests' lands. |
| 23 | unset CLAUDE_CODE_SESSION_ID CLAUDECODE CLAUDE_CONFIG_DIR ONSET_SESSION_FILE ONSET_SESSIONS VCS_SESSION_FILE VCS_SESSIONS VCS_HOME VCS_TOKEN |
| 24 | export CODEX_HOME="$WORK/no-codex" |
| 25 | PORT=$((20000 + RANDOM % 20000)) |
| 26 | HOST="127.0.0.1:$PORT" |
| 27 | SERVER_PID= |
| 28 | stop_server() { |
| 29 | [ -z "$SERVER_PID" ] && return |
| 30 | if [ "$TARGET" = worker ]; then kill -- "-$SERVER_PID" 2>/dev/null; else kill "$SERVER_PID" 2>/dev/null; fi |
| 31 | wait "$SERVER_PID" 2>/dev/null |
| 32 | SERVER_PID= |
| 33 | } |
| 34 | cleanup() { |
| 35 | stop_server |
| 36 | # Lazy workspaces' mounts first. |
| 37 | grep -o " $WORK/[^ ]*" /proc/self/mountinfo 2>/dev/null | while read -r m; do fusermount3 -u "$m" 2>/dev/null; done |
| 38 | rm -rf "$WORK" |
| 39 | } |
| 40 | trap cleanup EXIT |
| 41 | cd "$WORK" |
| 42 |
| 43 | if [ "$TARGET" = worker ]; then |
| 44 | APP=$ROOT/../apps/onset |
| 45 | ALICE=vcs_test_alice_0123456789 |
| 46 | BOB=vcs_test_bob_0123456789 |
| 47 | CAROL=vcs_test_carol_0123456789 |
| 48 | # Packs start plain; the restart for the offline checks turns encryption |
| 49 | # on, and the packs from before are encrypted as the project's used. |
| 50 | DATA_KEY=off |
| 51 | USERS="{\"$ALICE\":{\"handle\":\"alice\",\"name\":\"Alice\",\"email\":\"alice@acme.test\"},\"$BOB\":{\"handle\":\"bob\",\"name\":\"Bob\",\"email\":\"bob@acme.test\"},\"$CAROL\":{\"handle\":\"carol\",\"name\":\"Carol\",\"email\":\"carol@acme.test\"}}" |
| 52 | start_server() { |
| 53 | # Its own process group, so stopping it stops workerd too, and its own |
| 54 | # name, so a dev process left from another run never stands in for it. |
| 55 | WRANGLER_SEND_METRICS=false setsid "$APP/node_modules/.bin/wrangler" dev --cwd "$APP" --name "vcs-test-$PORT" --port "$PORT" --ip 127.0.0.1 --inspector-port "$((PORT + 1))" \ |
| 56 | --persist-to "$WORK/wrangler" --var AUTH_MODE:test --var "TEST_USERS:$USERS" --var COMPACT_AT:4096 --var MAX_PROJECT_BYTES:60000000 --var "VCS_DATA_KEY:$DATA_KEY" >"$WORK/server.log" 2>&1 & |
| 57 | SERVER_PID=$! |
| 58 | for _ in $(seq 300); do curl -s "http://$HOST/.well-known/onset.json" 2>/dev/null | grep -q api && return; sleep 0.2; done |
| 59 | cat "$WORK/server.log"; echo "worker didn't start"; exit 1 |
| 60 | } |
| 61 | add_member() { # project, handle, role |
| 62 | curl -sf -X POST -H "authorization: Bearer $ALICE" -H 'content-type: application/json' -d "{\"handle\":\"$2\",\"role\":\"$3\"}" "http://$HOST/v1/projects/$1/members" >/dev/null |
| 63 | } |
| 64 | else |
| 65 | start_server() { |
| 66 | "$SERVER" serve --data "$WORK/server" --listen "$HOST" --threads 32 2>"$WORK/server.log" & |
| 67 | SERVER_PID=$! |
| 68 | for _ in $(seq 50); do curl -s "http://$HOST/.well-known/onset.json" >/dev/null && return; sleep 0.1; done |
| 69 | echo "server didn't start"; exit 1 |
| 70 | } |
| 71 | add_member() { |
| 72 | "$SERVER" member add "$1" "$2" --role "$3" --data "$WORK/server" |
| 73 | } |
| 74 | ALICE=$("$SERVER" user add alice --name Alice --email alice@acme.test --data "$WORK/server") |
| 75 | BOB=$("$SERVER" user add bob --name Bob --email bob@acme.test --data "$WORK/server") |
| 76 | CAROL=$("$SERVER" user add carol --name Carol --email carol@acme.test --data "$WORK/server") |
| 77 | fi |
| 78 |
| 79 | pass=0 |
| 80 | fail=0 |
| 81 | check() { |
| 82 | local name=$1 |
| 83 | shift |
| 84 | if "$@" >/dev/null 2>&1; then pass=$((pass + 1)); else fail=$((fail + 1)); echo "FAIL: $name"; fi |
| 85 | } |
| 86 | code() { |
| 87 | local want=$1 |
| 88 | shift |
| 89 | "$@" >/dev/null 2>&1 |
| 90 | [ "$?" = "$want" ] |
| 91 | } |
| 92 | jq_() { python3 -c "import json,sys; d=json.load(sys.stdin); print($1)"; } |
| 93 |
| 94 | # People and machines: each has a ONSET_HOME. |
| 95 | as() { # who, command... |
| 96 | local who=$1 |
| 97 | shift |
| 98 | case $who in |
| 99 | laptop) ONSET_HOME="$WORK/laptop" ONSET_NAME="Alice" ONSET_EMAIL="alice@acme.test" "$@" ;; |
| 100 | cloud) ONSET_HOME="$WORK/cloud" ONSET_NAME="Alice" ONSET_EMAIL="alice@acme.test" "$@" ;; |
| 101 | bob) ONSET_HOME="$WORK/bob" ONSET_NAME="Bob" ONSET_EMAIL="bob@acme.test" "$@" ;; |
| 102 | carol) ONSET_HOME="$WORK/carol" ONSET_NAME="Carol" ONSET_EMAIL="carol@acme.test" "$@" ;; |
| 103 | esac |
| 104 | } |
| 105 |
| 106 | start_server |
| 107 |
| 108 | check "the host still answers to vcs.json (from before Onset had its name)" sh -c "curl -s http://$HOST/.well-known/vcs.json | grep -q api" |
| 109 | check "laptop login" as laptop "$VCS" login "$HOST" --token "$ALICE" |
| 110 | check "cloud login" as cloud "$VCS" login "$HOST" --token "$ALICE" |
| 111 | check "bob login" as bob "$VCS" login "$HOST" --token "$BOB" |
| 112 | check "carol login" as carol "$VCS" login "$HOST" --token "$CAROL" |
| 113 |
| 114 | # Signing in through the browser (a stand-in sign-in page approves it). |
| 115 | LPORT=$((PORT + 3)) |
| 116 | python3 "$TESTS/login-server.py" "$LPORT" "$ALICE" & |
| 117 | LOGIN_PID=$! |
| 118 | for _ in $(seq 100); do curl -s -o /dev/null -X POST "http://127.0.0.1:$LPORT/login" && break; sleep 0.1; done |
| 119 | check "browser login" sh -c "ONSET_HOME=$WORK/browser ONSET_LOGIN=http://127.0.0.1:$LPORT/login '$VCS' login '$HOST' 2>/dev/null | grep -q 'as alice'" |
| 120 | kill "$LOGIN_PID" 2>/dev/null |
| 121 | check "no browser login without a sign-in page" sh -c "ONSET_HOME=$WORK/browser2 '$VCS' login '$HOST' 2>&1 | grep -q -- '--token'" |
| 122 |
| 123 | # Alice starts locally and publishes. |
| 124 | mkdir shop && cd shop |
| 125 | printf 'line 1\nline 2\nline 3\n' > shared.txt |
| 126 | printf 'SECRET=alice\n' > .env |
| 127 | check "init" as laptop "$VCS" init |
| 128 | check "first version" as laptop "$VCS" land -m "First version" |
| 129 | printf 'line 1\nline 2\nline 3\nline 4\n' > shared.txt |
| 130 | check "second version" as laptop "$VCS" land -m "Second version" |
| 131 | check "publish" as laptop "$VCS" publish "$HOST/acme/shop" |
| 132 | check "main is 2 on the host" test "$(curl -s -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/shop" | jq_ 'd["main"]["n"]')" = 2 |
| 133 | add_member acme/shop bob member |
| 134 | add_member acme/shop carol reader |
| 135 |
| 136 | # Bob gets it. |
| 137 | cd "$WORK" |
| 138 | check "bob gets" as bob "$VCS" get "$HOST/acme/shop" bobshop |
| 139 | check "bob has the files" test "$(tail -1 bobshop/shared.txt)" = "line 4" |
| 140 | check "bob doesn't get alice's .env" test ! -e bobshop/.env |
| 141 |
| 142 | # Parallel landings from two people. |
| 143 | printf 'alice\n' > shop/alice.txt |
| 144 | printf 'bob\n' > bobshop/bob.txt |
| 145 | check "alice lands" as laptop "$VCS" -C shop land -m "Alice's file" |
| 146 | check "bob lands without syncing" as bob "$VCS" -C bobshop land -m "Bob's file" |
| 147 | check "main is 4" test "$(as bob "$VCS" --json -C bobshop log | jq_ 'd["versions"][0]["n"]')" = 4 |
| 148 |
| 149 | # A conflict between them. |
| 150 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 151 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 152 | perl -pi -e 's/^line 2$/line two (alice)/' shop/shared.txt |
| 153 | perl -pi -e 's/^line 2$/line two (bob)/' bobshop/shared.txt |
| 154 | check "alice lands line 2" as laptop "$VCS" -C shop land -m "Alice's line 2" |
| 155 | check "bob conflicts (exit 3)" code 3 as bob "$VCS" -C bobshop land -m "Bob's line 2" |
| 156 | check "bob syncs (exit 3)" code 3 as bob "$VCS" -C bobshop sync |
| 157 | check "markers" grep -q '<<<<<<< main' bobshop/shared.txt |
| 158 | perl -ni -e 'print unless /^(<<<<<<< |=======$|>>>>>>> )/ or /line two \(alice\)/' bobshop/shared.txt |
| 159 | check "bob lands after fixing" as bob "$VCS" -C bobshop land |
| 160 |
| 161 | # A reader's change waits for someone who can land it. |
| 162 | check "carol gets" as carol "$VCS" get "$HOST/acme/shop" carolshop |
| 163 | printf 'fix from carol\n' > carolshop/carol.txt |
| 164 | check "carol's land waits (exit 5)" code 5 as carol "$VCS" -C carolshop land -m "Carol's fix" |
| 165 | CAROL_CHANGE=$(as carol "$VCS" --json -C carolshop log --changes | jq_ '[c["change"]["id"] for c in d["changes"] if c["change"]["state"]=="waiting"][0]') |
| 166 | check "alice sees the waiting change" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop log --changes | grep -q waiting" |
| 167 | as laptop "$VCS" -C shop land "$CAROL_CHANGE" >"$WORK/carol-land.out" 2>&1; cat "$WORK/carol-land.out" >&2 |
| 168 | check "alice lands carol's change" grep -q landed "$WORK/carol-land.out" |
| 169 | check "carol is the author" test "$(as laptop "$VCS" --json -C shop log -n 1 | jq_ 'd["versions"][0]["author"].split(" <")[0]')" = Carol |
| 170 |
| 171 | # Alice moves from her laptop to a cloud machine mid-change. |
| 172 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 173 | printf 'started on the laptop\n' > shop/feature.txt |
| 174 | check "describe on laptop" as laptop "$VCS" -C shop describe "Feature" |
| 175 | FEATURE=$(as laptop "$VCS" --json -C shop status | jq_ 'd["change"]["id"]') |
| 176 | check "cloud picks it up" as cloud "$VCS" get "$HOST/acme/shop" cloudshop --change "$FEATURE" |
| 177 | check "the edit came along" test "$(cat cloudshop/feature.txt)" = "started on the laptop" |
| 178 | check ".env came along" test "$(cat cloudshop/.env)" = "SECRET=alice" |
| 179 | printf 'continued in the cloud\n' >> cloudshop/feature.txt |
| 180 | check "cloud saves" as cloud "$VCS" -C cloudshop status |
| 181 | printf 'late laptop edit\n' > shop/late.txt |
| 182 | OUT=$(as laptop "$VCS" --json -C shop status 2>/dev/null) |
| 183 | check "laptop's late edit is rescued into a new change" sh -c "echo '$OUT' | grep -q 'Rescued from'" |
| 184 | check "cloud lands the feature" as cloud "$VCS" -C cloudshop land |
| 185 | check "feature has both edits" test "$(as bob "$VCS" -C bobshop sync >/dev/null 2>&1; cat bobshop/feature.txt | wc -l | tr -d " ")" = 2 |
| 186 |
| 187 | # Sharing a change. |
| 188 | printf 'draft\n' > bobshop/draft.txt |
| 189 | check "bob describes" as bob "$VCS" -C bobshop describe "Draft for Alice" |
| 190 | DRAFT=$(as bob "$VCS" --json -C bobshop status | jq_ 'd["change"]["id"]') |
| 191 | check "bob shares it" as bob "$VCS" -C bobshop access "$DRAFT" @alice |
| 192 | check "alice sees it" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop log --changes | grep -q 'Draft for Alice'" |
| 193 |
| 194 | # Private paths: only Alice sees billing/. (First drop the rescued change, |
| 195 | # which still has the takeover's conflict in it.) |
| 196 | as laptop "$VCS" -C shop drop >/dev/null 2>&1 |
| 197 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 198 | mkdir -p shop/billing && printf 'rate 10\n' > shop/billing/rates.txt |
| 199 | check "alice lands billing" as laptop "$VCS" -C shop land -m "Billing rates" |
| 200 | check "bob sees billing before the rule" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sync >/dev/null 2>&1; test -e $WORK/bobshop/billing/rates.txt" |
| 201 | check "alice makes billing/ hers" as laptop "$VCS" -C shop access billing @alice |
| 202 | check "bob's sync takes billing away" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sync >/dev/null 2>&1; test ! -e $WORK/bobshop/billing" |
| 203 | printf 'bob was here\n' >> bobshop/README.md 2>/dev/null || printf 'bob was here\n' > bobshop/README.md |
| 204 | check "bob lands outside billing" as bob "$VCS" -C bobshop land -m "Bob's readme" |
| 205 | check "alice still has billing" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop sync >/dev/null 2>&1; test -e $WORK/shop/billing/rates.txt" |
| 206 | mkdir -p bobshop/billing && printf 'sneaky\n' > bobshop/billing/new.txt |
| 207 | check "bob can't write into billing (exit 7)" code 7 as bob "$VCS" -C bobshop land -m "Sneaky" |
| 208 | rm -rf bobshop/billing |
| 209 | as bob "$VCS" -C bobshop drop >/dev/null 2>&1 |
| 210 | printf 'rate 12\n' > shop/billing/rates.txt |
| 211 | check "alice changes billing" as laptop "$VCS" -C shop land -m "New billing rates" |
| 212 | check "bob's log shows a private change" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop log -n 1 | grep -q 'a private change'" |
| 213 | check "carol never gets billing" sh -c "ONSET_HOME=$WORK/carol $VCS get $HOST/acme/shop $WORK/carol2 >/dev/null 2>&1; test -e $WORK/carol2/README.md -o -e $WORK/carol2/shared.txt; test ! -e $WORK/carol2/billing" |
| 214 | check "bob can't read billing over the API" sh -c "! curl -sf -H 'authorization: Bearer $BOB' http://$HOST/v1/projects/acme/shop/trees/\$(curl -s -H 'authorization: Bearer $ALICE' http://$HOST/v1/projects/acme/shop | python3 -c 'import json,sys; print(json.load(sys.stdin)[\"main\"][\"tree\"])') >/dev/null" |
| 215 | # Naming the full tree in a change of his own doesn't get bob in either. |
| 216 | FULL=$(curl -s -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/shop" | jq_ 'd["main"]["tree"]') |
| 217 | CLAIM="{\"change\":{\"id\":\"bcdfghjkmnpq\",\"base_tree\":\"$FULL\",\"head_tree\":\"$FULL\"},\"workspace\":\"w1\"}" |
| 218 | check "bob can't claim the full tree" sh -c "curl -s -o /dev/null -w '%{http_code}' -X PUT -H 'authorization: Bearer $BOB' -H 'content-type: application/json' -d '$CLAIM' http://$HOST/v1/projects/acme/shop/changes/bcdfghjkmnpq | grep -q 403" |
| 219 | check "and still can't read it" sh -c "! curl -sf -H 'authorization: Bearer $BOB' http://$HOST/v1/projects/acme/shop/trees/$FULL >/dev/null" |
| 220 |
| 221 | # Undo a hosted land. |
| 222 | as cloud "$VCS" -C cloudshop sync >/dev/null 2>&1 |
| 223 | printf 'oops\n' > cloudshop/oops.txt |
| 224 | check "cloud lands oops" as cloud "$VCS" -C cloudshop land -m "Oops" |
| 225 | check "cloud undoes it" as cloud "$VCS" -C cloudshop undo |
| 226 | check "main took it back" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop log -n 1 | grep -q 'Take back'" |
| 227 |
| 228 | # Offline: saves stay local, land says offline, later it all goes up. |
| 229 | if [ "$TARGET" = worker ]; then |
| 230 | # A pack written plain, before encryption is on. |
| 231 | { printf 'PLAIN-MARKER-6c1e\n'; head -c 700000 /dev/urandom; } > shop/plain.bin |
| 232 | as laptop "$VCS" -C shop land -m "Written plain" >/dev/null 2>&1 |
| 233 | fi |
| 234 | stop_server |
| 235 | printf 'written offline\n' > cloudshop/offline.txt |
| 236 | check "status works offline" as cloud "$VCS" -C cloudshop status |
| 237 | check "land says offline (exit 6)" code 6 as cloud "$VCS" -C cloudshop land -m "Offline work" |
| 238 | DATA_KEY=test-data-key-2026 |
| 239 | start_server |
| 240 | check "land works once back" as cloud "$VCS" -C cloudshop land -m "Offline work" |
| 241 |
| 242 | # 32 agents land at once on one main, none refused. |
| 243 | BEFORE=$(as bob "$VCS" --json -C bobshop log -n 1 | jq_ 'd["versions"][0]["n"]') |
| 244 | for i in $(seq 1 32); do |
| 245 | as bob "$VCS" get "$HOST/acme/shop" "agent$i" >/dev/null 2>&1 |
| 246 | printf 'agent %s\n' "$i" > "agent$i/agent-$i.txt" |
| 247 | done |
| 248 | pids=() |
| 249 | for i in $(seq 1 32); do |
| 250 | (ONSET_HOME="$WORK/bob" ONSET_NAME="Bob" ONSET_AGENT="agent-$i" "$VCS" -C "agent$i" land -m "Agent $i" >"$WORK/land$i.out" 2>&1; echo $? >"$WORK/land$i.code") & |
| 251 | pids+=($!) |
| 252 | done |
| 253 | wait "${pids[@]}" |
| 254 | OK=$(grep -l '^0$' "$WORK"/land*.code | wc -l | tr -d " ") |
| 255 | check "all 32 landed" test "$OK" = 32 |
| 256 | for i in $(seq 1 32); do |
| 257 | [ "$(cat "$WORK/land$i.code")" = 0 ] || { echo "agent $i:"; cat "$WORK/land$i.out"; } |
| 258 | done |
| 259 | AFTER=$(as bob "$VCS" --json -C bobshop log -n 1 | jq_ 'd["versions"][0]["n"]') |
| 260 | check "main moved by 32" test "$((AFTER - BEFORE))" = 32 |
| 261 | # An agent that landed in the middle still sees every version, in order. |
| 262 | check "log has no gaps" sh -c "ONSET_HOME=$WORK/bob '$VCS' --json -C agent1 log -n 40 | python3 -c 'import json,sys; n=[v[\"n\"] for v in json.load(sys.stdin)[\"versions\"]]; assert n == list(range(n[0], n[0]-len(n), -1)), n'" |
| 263 |
| 264 | # Landing without a workspace: file edits in one call. |
| 265 | edits() { # token, json |
| 266 | curl -s -X POST -H "authorization: Bearer $1" -H 'content-type: application/json' -d "$2" "http://$HOST/v1/projects/acme/shop/edits" |
| 267 | } |
| 268 | check "edits land" sh -c "$(declare -f edits); HOST=$HOST; edits $ALICE '{\"title\":\"From the API\",\"files\":{\"api/hello.txt\":\"hello\\n\",\"api/run.sh\":\"echo hi\\n\"},\"executable\":[\"api/run.sh\"]}' | grep -q '\"outcome\":\"landed\"'" |
| 269 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 270 | check "edits reach workspaces" test "$(cat bobshop/api/hello.txt 2>/dev/null)" = hello |
| 271 | [ -z "$WINDOWS" ] && check "edits keep the executable bit" test -x bobshop/api/run.sh |
| 272 | check "edits can delete" sh -c "$(declare -f edits); HOST=$HOST; edits $ALICE '{\"title\":\"Remove hello\",\"files\":{\"api/hello.txt\":null}}' | grep -q '\"outcome\":\"landed\"'" |
| 273 | check "secrets refused in edits" sh -c "$(declare -f edits); HOST=$HOST; edits $ALICE '{\"title\":\"Key\",\"files\":{\"api/key.txt\":\"sk_live_$(printf 'a%.0s' $(seq 1 24))\"}}' | grep -q '\"code\":\"secret\"'" |
| 274 | check "bad paths refused in edits" sh -c "$(declare -f edits); HOST=$HOST; edits $ALICE '{\"title\":\"Escape\",\"files\":{\"../x\":\"x\"}}' | grep -q bad_usage" |
| 275 | check "git's own folder refused in edits" sh -c "$(declare -f edits); HOST=$HOST; edits $ALICE '{\"title\":\"Hook\",\"files\":{\"sub/.GIT/hooks/post-checkout\":\"x\"}}' | grep -q bad_usage" |
| 276 | # A nested git checkout lands as files, never its .git. |
| 277 | mkdir -p bobshop/vendor/lib/.git && printf 'ref: refs/heads/main\n' > bobshop/vendor/lib/.git/HEAD && printf 'lib\n' > bobshop/vendor/lib/lib.txt |
| 278 | check "a nested git checkout lands" as bob "$VCS" -C bobshop land -m "Vendored lib" |
| 279 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 280 | check "its files reach workspaces" test -f shop/vendor/lib/lib.txt |
| 281 | check "its .git doesn't" test ! -e shop/vendor/lib/.git |
| 282 | check "log of a file on the host" test "$(as bob "$VCS" --json -C bobshop log shared.txt | jq_ '",".join(str(v["n"]) for v in d["versions"])')" = 6,5,2,1 |
| 283 | check "a removed file's history" test "$(as bob "$VCS" --json -C bobshop log api/hello.txt | jq_ '",".join(v["kind"] for v in d["versions"])')" = removed,added |
| 284 | check "blame on the host" test "$(as bob "$VCS" --json -C bobshop blame shared.txt | jq_ '",".join(str(l["version"]) for l in d["lines"])')" = 1,6,1,2 |
| 285 | check "no history of a private folder" sh -c "! ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop log billing/rates.txt >/dev/null 2>&1" |
| 286 | check "show a file at a version on the host" test "$(as bob "$VCS" -C bobshop show shared.txt --from 1 | tail -1)" = "line 3" |
| 287 | check "no show of a private file" sh -c "! ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop show billing/rates.txt >/dev/null 2>&1" |
| 288 | check "no blame of a private file" sh -c "! ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop blame billing/rates.txt >/dev/null 2>&1" |
| 289 | check "tag on the host" as laptop "$VCS" -C shop tag v1.0 2 |
| 290 | check "bob sees the tag" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop tag | grep -q 'v1.0'" |
| 291 | check "show at a tag on the host" test "$(as bob "$VCS" -C bobshop show shared.txt --from v1.0 | tail -1)" = "line 4" |
| 292 | check "a reader can't tag (exit 7)" code 7 as carol "$VCS" -C carolshop tag nope |
| 293 | check "a reader's edits wait" sh -c "$(declare -f edits); HOST=$HOST; edits $CAROL '{\"title\":\"Suggestion\",\"files\":{\"docs/idea.txt\":\"idea\\n\"}}' | grep -q '\"outcome\":\"waiting\"'" |
| 294 |
| 295 | # Groups: a folder for a group. |
| 296 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 297 | check "alice makes a group" as laptop "$VCS" -C shop group ops @bob |
| 298 | check "the group lists bob" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop group | grep -q '@ops.*@bob'" |
| 299 | mkdir -p shop/ops && printf 'runbook\n' > shop/ops/runbook.txt |
| 300 | check "alice lands ops/" as laptop "$VCS" -C shop land -m "Runbook" |
| 301 | check "ops/ is the group's" as laptop "$VCS" -C shop access ops @ops |
| 302 | check "bob, in the group, sees ops/" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sync >/dev/null 2>&1; test -e $WORK/bobshop/ops/runbook.txt" |
| 303 | check "carol, outside it, doesn't" sh -c "ONSET_HOME=$WORK/carol $VCS -C $WORK/carolshop sync >/dev/null 2>&1; test ! -e $WORK/carolshop/ops" |
| 304 | check "only owners change groups (exit 7)" code 7 as bob "$VCS" -C bobshop group ops @bob @carol |
| 305 |
| 306 | # Approvals: a land waits until someone else approves it. |
| 307 | check "alice requires an approval" as laptop "$VCS" -C shop settings approvals=1 |
| 308 | printf 'reviewed\n' > shop/reviewed.txt |
| 309 | check "alice's land waits (exit 5)" code 5 as laptop "$VCS" -C shop land -m "Needs review" |
| 310 | REVIEW=$(as laptop "$VCS" --json -C shop log --changes | jq_ '[r["change"]["id"] for r in d["changes"] if r["change"].get("title") == "Needs review"][0]') |
| 311 | check "alice can't approve her own (exit 7)" code 7 as laptop "$VCS" -C shop approve "$REVIEW" |
| 312 | check "bob approves, and it lands" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop approve $REVIEW | grep -q landed" |
| 313 | check "main has it" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sync >/dev/null 2>&1; test -e $WORK/bobshop/reviewed.txt" |
| 314 | check "approvals off again" as laptop "$VCS" -C shop settings approvals=0 |
| 315 |
| 316 | # MCP, for agents without a workspace (Upstream's host). |
| 317 | if [ "$TARGET" = worker ]; then |
| 318 | rpc() { curl -s -X POST -H "authorization: Bearer $ALICE" -H 'content-type: application/json' -d "$1" "http://$HOST/mcp"; } |
| 319 | check "mcp initialize" sh -c "$(declare -f rpc); ALICE=$ALICE HOST=$HOST; rpc '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}' | grep -q protocolVersion" |
| 320 | check "mcp lists the tools" sh -c "$(declare -f rpc); ALICE=$ALICE HOST=$HOST; rpc '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\"}' | grep -q onset_land" |
| 321 | check "mcp lands edits" sh -c "$(declare -f rpc); ALICE=$ALICE HOST=$HOST; rpc '{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"onset_land\",\"arguments\":{\"project\":\"acme/shop\",\"title\":\"From MCP\",\"files\":{\"mcp/hi.txt\":\"hi from mcp\\n\"}}}}' | grep -q landed" |
| 322 | check "mcp reads it back" sh -c "$(declare -f rpc); ALICE=$ALICE HOST=$HOST; rpc '{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\",\"params\":{\"name\":\"onset_read\",\"arguments\":{\"project\":\"acme/shop\",\"path\":\"mcp/hi.txt\"}}}' | grep -q 'hi from mcp'" |
| 323 | check "mcp lists a folder (by the tool's name from before Onset had its name)" sh -c "$(declare -f rpc); ALICE=$ALICE HOST=$HOST; rpc '{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"tools/call\",\"params\":{\"name\":\"vcs_list\",\"arguments\":{\"project\":\"acme/shop\",\"path\":\"mcp\"}}}' | grep -q 'hi.txt'" |
| 324 | check "mcp log has it" sh -c "$(declare -f rpc); ALICE=$ALICE HOST=$HOST; rpc '{\"jsonrpc\":\"2.0\",\"id\":6,\"method\":\"tools/call\",\"params\":{\"name\":\"onset_log\",\"arguments\":{\"project\":\"acme/shop\",\"limit\":3}}}' | grep -q 'From MCP'" |
| 325 | check "mcp needs a token" sh -c "curl -s -X POST -H 'content-type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"tools/call\",\"params\":{\"name\":\"vcs_log\",\"arguments\":{\"project\":\"acme/shop\"}}}' http://$HOST/mcp | grep -q 'isError'" |
| 326 | check "rest reads a file" sh -c "curl -s -H 'authorization: Bearer $ALICE' 'http://$HOST/v1/projects/acme/shop/file?path=mcp/hi.txt' | grep -q base64" |
| 327 | fi |
| 328 |
| 329 | # A bigger tree on a new machine: the hosted service sends it from a |
| 330 | # snapshot once it has made one, with what's newer on top, and never sends |
| 331 | # someone a snapshot of files they can't see. |
| 332 | fresh() { # n, token, command... |
| 333 | local n=$1 token=$2 |
| 334 | shift 2 |
| 335 | ONSET_HOME="$WORK/fresh$n" "$VCS" login "$HOST" --token "$token" >/dev/null 2>&1 |
| 336 | ONSET_HOME="$WORK/fresh$n" ONSET_NAME=Fresh ONSET_EMAIL=fresh@acme.test "$@" |
| 337 | } |
| 338 | tree_of() { curl -s -H "authorization: Bearer $1" "http://$HOST/v1/projects/acme/shop" | jq_ 'd["main"]["tree"]'; } |
| 339 | from_snapshot() { curl -s -o /dev/null -D - -H "authorization: Bearer $1" "http://$HOST/v1/projects/acme/shop/trees/$(tree_of "$1")" | grep -qi '^vcs-snapshot:'; } |
| 340 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 341 | mkdir -p shop/many && for i in $(seq 1 300); do printf 'file %s\n' "$i" > "shop/many/f$i.txt"; done |
| 342 | snap_get() { # n, token, folder: a new machine's copy has many/ as it is |
| 343 | fresh "$1" "$2" "$VCS" get "$HOST/acme/shop" "$WORK/$3" >/dev/null 2>&1 && diff -r "$WORK/shop/many" "$WORK/$3/many" |
| 344 | } |
| 345 | check "land 300 files" as laptop "$VCS" -C shop land -m "Many files" |
| 346 | check "a new machine gets them all" snap_get 1 "$ALICE" snap1 |
| 347 | check "billing too, for alice" test -e snap1/billing/rates.txt |
| 348 | check "billing's bytes are there to find" grep -rq 'rate 12' "$WORK/fresh1" |
| 349 | if [ "$TARGET" = worker ]; then |
| 350 | for _ in $(seq 50); do from_snapshot "$ALICE" && break; sleep 0.2; done |
| 351 | check "the host made a snapshot" from_snapshot "$ALICE" |
| 352 | check "bob's download doesn't come from alice's snapshot" sh -c "! curl -s -o /dev/null -D - -H 'authorization: Bearer $BOB' http://$HOST/v1/projects/acme/shop/trees/$(tree_of "$BOB") | grep -qi '^vcs-snapshot:'" |
| 353 | fi |
| 354 | printf 'changed\n' > shop/many/f7.txt && printf 'new\n' > shop/many/new.txt && rm shop/many/f9.txt |
| 355 | check "land changes past it" as laptop "$VCS" -C shop land -m "Past the snapshot" |
| 356 | check "the next machine gets the snapshot and what's newer" snap_get 2 "$ALICE" snap2 |
| 357 | check "billing too" test -e snap2/billing/rates.txt |
| 358 | [ "$TARGET" = worker ] && check "that came from the snapshot" from_snapshot "$ALICE" |
| 359 | check "bob's new machine gets it without billing" snap_get 3 "$BOB" snap3 |
| 360 | check "no billing folder" test ! -e snap3/billing |
| 361 | check "and none of billing's bytes" sh -c "! grep -rq 'rate 12' '$WORK/fresh3'" |
| 362 |
| 363 | # Renames on the host: an edit follows a moved file when it lands, and log follows the move. |
| 364 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 365 | mkdir -p shop/notes && for i in $(seq 1 30); do printf 'line %s\n' "$i"; done > shop/notes/a.txt |
| 366 | check "land a file to move" as laptop "$VCS" -C shop land -m "Notes to move" |
| 367 | check "a second workspace on it" as cloud "$VCS" get "$HOST/acme/shop" movetwo |
| 368 | mkdir -p shop/kept && mv shop/notes/a.txt shop/kept/a.md |
| 369 | check "land the move" as laptop "$VCS" -C shop land -m "Move them" |
| 370 | perl -pi -e 's/^line 9$/line nine/' movetwo/notes/a.txt |
| 371 | check "an edit to the old name lands" as cloud "$VCS" -C movetwo land -m "Edit before the move" |
| 372 | check "it followed the move" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop sync >/dev/null 2>&1; grep -q 'line nine' $WORK/shop/kept/a.md" |
| 373 | check "log follows the move on the host" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop log kept/a.md | grep -q 'from notes/a.txt'" |
| 374 |
| 375 | # Signed lands (Upstream's host keeps keys): on key, then lands are signed |
| 376 | # and on verify checks them; a project can take signed lands only. |
| 377 | if [ "$TARGET" = worker ]; then |
| 378 | check "vcs key" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop key | grep -q 'to @alice'" |
| 379 | printf 'signed\n' > shop/signed.txt |
| 380 | check "a signed land" as laptop "$VCS" -C shop land -m "Signed work" |
| 381 | check "verify says who signed it" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop verify | grep -q 'by @alice'" |
| 382 | check "an older unsigned version says so (exit 1)" code 1 as laptop "$VCS" -C shop verify 1 |
| 383 | check "signed lands only" as laptop "$VCS" -C shop settings signed=required |
| 384 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 385 | printf 'unsigned\n' > bobshop/unsigned.txt |
| 386 | check "bob's unsigned land is refused" sh -c "! ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop land -m Unsigned >/dev/null 2>&1" |
| 387 | check "back to optional" as laptop "$VCS" -C shop settings signed=optional |
| 388 | check "and bob's lands" as bob "$VCS" -C bobshop land -m "Unsigned" |
| 389 | fi |
| 390 |
| 391 | # Release lines on the host: alice starts 1.x, bob gets a workspace on it and |
| 392 | # lands a fix there, and a fix that landed on main comes over. |
| 393 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 394 | check "alice starts line 1.x" as laptop "$VCS" -C shop line 1.x |
| 395 | check "a reader can't start one" sh -c "! ONSET_HOME=$WORK/carol $VCS -C $WORK/carolshop line 2.x >/dev/null 2>&1" |
| 396 | check "bob gets a workspace on it" as bob "$VCS" get "$HOST/acme/shop" bob1x --line 1.x |
| 397 | printf 'fix on 1.x\n' > bob1x/line-fix.txt |
| 398 | check "bob lands a fix on it" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bob1x land -m 'Fix on 1.x' | grep -q 'as 1.x/1'" |
| 399 | check "alice sees 1.x/1" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop log --line 1.x | grep -q 'Fix on 1.x'" |
| 400 | check "main doesn't have it" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop sync >/dev/null 2>&1; test ! -e $WORK/shop/line-fix.txt" |
| 401 | printf 'main fix\n' > shop/main-fix.txt |
| 402 | as laptop "$VCS" -C shop land -m "Main fix" >/dev/null 2>&1 |
| 403 | MAINFIX=$(as laptop "$VCS" --json -C shop log -n 1 | jq_ 'd["versions"][0]["change"]') |
| 404 | check "alice brings main's fix over" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop land $MAINFIX --to 1.x | grep -q '1.x/2'" |
| 405 | check "bob's line has both" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bob1x sync >/dev/null 2>&1; test -f $WORK/bob1x/main-fix.txt -a -f $WORK/bob1x/line-fix.txt" |
| 406 | check "and nothing private" test ! -e bob1x/billing |
| 407 |
| 408 | # Purge: alice (an owner) takes a key (small, kept beside the index) and a |
| 409 | # big file (in a pack, which is written again without it) out of the |
| 410 | # history; bob's machine takes them out when it next syncs; bob (a member, |
| 411 | # not an owner) can't. |
| 412 | printf 'not-a-real-key-hosted\n' > shop/leaked.pem |
| 413 | as laptop "$VCS" -C shop land -m "Oops" --allow-secrets >/dev/null 2>&1 |
| 414 | head -c 900000 /dev/urandom > shop/big.bin |
| 415 | BIG=$( (printf '\001'; cat shop/big.bin) | sha256sum | cut -c1-64) |
| 416 | as laptop "$VCS" -C shop land -m "A big file" >/dev/null 2>&1 |
| 417 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 418 | check "bob has the key" test -f bobshop/leaked.pem |
| 419 | check "bob can't purge" sh -c "! ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop purge leaked.pem --yes >/dev/null 2>&1" |
| 420 | check "alice sees what it would do" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop purge leaked.pem | grep -q would" |
| 421 | check "alice purges them" as laptop "$VCS" -C shop purge leaked.pem big.bin --yes |
| 422 | check "gone from alice's workspace" test ! -e shop/leaked.pem -a ! -e shop/big.bin |
| 423 | LEAKED=$(printf '\001not-a-real-key-hosted\n' | sha256sum | cut -c1-64) |
| 424 | host_lacks() { # object id (hex): the host says it's missing (deleting it runs just after the purge) |
| 425 | for _ in $(seq 50); do |
| 426 | [ "$(printf '%s' "$1" | xxd -r -p | curl -s -X POST -H "authorization: Bearer $ALICE" --data-binary @- "http://$HOST/v1/projects/acme/shop/objects/missing" | xxd -p | tr -d '\n')" = "$1" ] && return 0 |
| 427 | sleep 0.2 |
| 428 | done |
| 429 | return 1 |
| 430 | } |
| 431 | check "the host deleted the key" host_lacks "$LEAKED" |
| 432 | check "and the big file" host_lacks "$BIG" |
| 433 | check "bob's next sync takes it out" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sync >/dev/null 2>&1; test ! -e $WORK/bobshop/leaked.pem -a ! -e $WORK/bobshop/big.bin" |
| 434 | check "no version has it" sh -c "! ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop show leaked.pem --from main >/dev/null 2>&1" |
| 435 | check "bob lands after it" sh -c "printf 'after\n' > $WORK/bobshop/after-purge.txt && ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop land -m 'After the purge' >/dev/null" |
| 436 | PURGED="$WORK/purged-box" |
| 437 | check "a new machine gets the project whole" sh -c "ONSET_HOME=$PURGED ONSET_NAME=Alice ONSET_EMAIL=alice@acme.test $VCS login $HOST --token $ALICE >/dev/null && cd $WORK && ONSET_HOME=$PURGED ONSET_NAME=Alice ONSET_EMAIL=alice@acme.test $VCS get $HOST/acme/shop purgedshop >/dev/null && test -f $WORK/purgedshop/after-purge.txt -a ! -e $WORK/purgedshop/leaked.pem" |
| 438 | check "the host's copy checks out" as laptop "$VCS" -C shop check |
| 439 |
| 440 | # Encrypted at rest (Upstream's host): packs written before encryption was |
| 441 | # on are written again encrypted, and a new pack has no plain bytes in R2. |
| 442 | if [ "$TARGET" = worker ]; then |
| 443 | unencrypted() { curl -s -X POST -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/shop/check" | jq_ 'd["unencrypted"]'; } |
| 444 | all_encrypted() { |
| 445 | for _ in $(seq 100); do |
| 446 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 447 | [ "$(unencrypted)" = 0 ] && return 0 |
| 448 | sleep 0.3 |
| 449 | done |
| 450 | return 1 |
| 451 | } |
| 452 | check "the old packs are encrypted as the project's used" all_encrypted |
| 453 | { printf 'SEALED-MARKER-9d2b\n'; head -c 700000 /dev/urandom; } > shop/sealed.bin |
| 454 | check "a land after it" as laptop "$VCS" -C shop land -m "Written encrypted" |
| 455 | check "R2 has the plain pack's bytes (kept for rollbacks)" grep -rqa 'PLAIN-MARKER-6c1e' "$WORK/wrangler" |
| 456 | check "but not the new pack's" sh -c "! grep -rqa 'SEALED-MARKER-9d2b' '$WORK/wrangler'" |
| 457 | ENC="$WORK/encrypted-box" |
| 458 | check "a new machine reads it all" sh -c "ONSET_HOME=$ENC ONSET_NAME=Alice ONSET_EMAIL=alice@acme.test $VCS login $HOST --token $ALICE >/dev/null && cd $WORK && ONSET_HOME=$ENC ONSET_NAME=Alice ONSET_EMAIL=alice@acme.test $VCS get $HOST/acme/shop encshop >/dev/null && head -c 18 $WORK/encshop/plain.bin | grep -q PLAIN-MARKER && head -c 19 $WORK/encshop/sealed.bin | grep -q SEALED-MARKER" |
| 459 | check "and the host's copy checks out" as laptop "$VCS" -C shop check |
| 460 | fi |
| 461 |
| 462 | # Lazy workspaces (Linux, FUSE): every file shows at once at its size, and |
| 463 | # each comes from the host the first time it's opened; work lands as from |
| 464 | # any workspace, and the mount comes back with the next command. |
| 465 | if [ "$(uname -s)" = Linux ] && [ -e /dev/fuse ] && command -v fusermount3 >/dev/null; then |
| 466 | LZ="$WORK/lazy-box" |
| 467 | lz() { ONSET_HOME="$LZ" ONSET_NAME="Alice" ONSET_EMAIL="alice@acme.test" "$@"; } |
| 468 | lz "$VCS" login "$HOST" --token "$ALICE" >/dev/null |
| 469 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 470 | check "get a lazy workspace" lz "$VCS" get "$HOST/acme/shop" lazyshop --lazy |
| 471 | check "the files are all there" test "$(cd lazyshop && find . -name .onset -prune -o -type f -print | grep -v '/\.env' | sort)" = "$(cd shop && find . -name .onset -prune -o -type f -print | grep -v '/\.env' | sort)" |
| 472 | check "at their sizes" test "$(stat -c %s lazyshop/shared.txt)" = "$(stat -c %s shop/shared.txt)" |
| 473 | FETCHED=$(find "$LZ/objects" -type f | wc -l) |
| 474 | check "a file comes when it's opened" cmp -s lazyshop/shared.txt shop/shared.txt |
| 475 | check "and only that file" test "$(find "$LZ/objects" -type f | wc -l)" = "$((FETCHED + 1))" |
| 476 | check "nothing looks changed" sh -c "ONSET_HOME=$LZ $VCS -C $WORK/lazyshop status | grep -q 'no changes yet'" |
| 477 | printf 'from a lazy workspace\n' >> lazyshop/shared.txt |
| 478 | mkdir -p lazyshop/lazy && printf 'new\n' > lazyshop/lazy/new.txt |
| 479 | mv lazyshop/feature.txt lazyshop/feature-moved.txt |
| 480 | check "status shows the work" sh -c "ONSET_HOME=$LZ $VCS -C $WORK/lazyshop status | grep -q 'R feature.txt -> feature-moved.txt'" |
| 481 | check "it lands" lz "$VCS" -C lazyshop land -m "From a lazy workspace" |
| 482 | check "others get it" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop sync >/dev/null 2>&1; grep -q 'from a lazy workspace' $WORK/shop/shared.txt && test -f $WORK/shop/lazy/new.txt -a -f $WORK/shop/feature-moved.txt" |
| 483 | printf 'from the laptop\n' > shop/to-lazy.txt |
| 484 | as laptop "$VCS" -C shop land -m "For the lazy one" >/dev/null 2>&1 |
| 485 | check "a sync brings others' work in" sh -c "ONSET_HOME=$LZ $VCS -C $WORK/lazyshop sync >/dev/null && grep -q 'from the laptop' $WORK/lazyshop/to-lazy.txt" |
| 486 | check "the mount stops" lz "$VCS" mount lazyshop --stop |
| 487 | check "leaving an empty folder" test -z "$(ls lazyshop)" |
| 488 | check "and the next command starts it" sh -c "ONSET_HOME=$LZ $VCS -C $WORK/lazyshop status >/dev/null && grep -q 'from a lazy workspace' $WORK/lazyshop/shared.txt" |
| 489 | lz "$VCS" mount lazyshop --stop >/dev/null 2>&1 |
| 490 | fi |
| 491 |
| 492 | # Agent sessions on the host: kept with the change, seen by its owner on any |
| 493 | # of their machines and by people the change is shared with, and deleted from |
| 494 | # the host when its owner deletes it. |
| 495 | mkdir -p "$WORK/claude-a/projects/-x" |
| 496 | HSES="$WORK/claude-a/projects/-x/host-1.jsonl" |
| 497 | printf '%s\n' '{"type":"user","message":{"role":"user","content":"add the session file"}}' '{"type":"assistant","message":{"content":[{"type":"text","text":"Adding it now."}]}}' > "$HSES" |
| 498 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 499 | printf 'with a session\n' > shop/session.txt |
| 500 | check "alice's agent lands with its session" sh -c "ONSET_HOME=$WORK/laptop CLAUDE_CONFIG_DIR=$WORK/claude-a CLAUDE_CODE_SESSION_ID=host-1 $VCS -C $WORK/shop land -m 'With a session' | grep -q 'Claude Code, 2 messages'" |
| 501 | SC=$(as laptop "$VCS" --json -C shop log -n 1 | jq_ 'd["versions"][0]["change"]') |
| 502 | check "alice's other machine sees it" sh -c "ONSET_HOME=$WORK/cloud $VCS -C $WORK/cloudshop sessions show $SC | grep -q 'agent: Adding it now.'" |
| 503 | check "bob doesn't" sh -c "! ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sessions show $SC 2>/dev/null | grep -q 'Adding it now.'" |
| 504 | check "alice shares the change with bob" as laptop "$VCS" -C shop access "$SC" @bob |
| 505 | check "then bob does" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sessions show $SC | grep -q 'Adding it now.'" |
| 506 | SESSION_TEXT=$( (printf '\001'; cat "$HSES") | sha256sum | cut -c1-64) |
| 507 | check "alice deletes it" as laptop "$VCS" -C shop sessions delete "$SC" |
| 508 | check "the host deleted its text" host_lacks "$SESSION_TEXT" |
| 509 | check "and nobody sees it" sh -c "! ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sessions show $SC 2>/dev/null | grep -q 'Adding it now.'" |
| 510 |
| 511 | # A project imported from a SHA-256 git repository publishes like any other; |
| 512 | # its git view (SHA-1) gets new commits, and says so. |
| 513 | git init -q --object-format=sha256 "$WORK/sha256src" |
| 514 | (cd "$WORK/sha256src" && printf 'sha\n' > s.txt && git add -A && git -c user.name=A -c user.email=a@example.com commit -qm "From SHA-256") |
| 515 | check "import a SHA-256 repository" as laptop "$VCS" import "$WORK/sha256src" "$WORK/sha256proj" |
| 516 | check "publish it" sh -c "cd $WORK/sha256proj && ONSET_HOME=$WORK/laptop ONSET_NAME=Alice ONSET_EMAIL=alice@acme.test $VCS publish $HOST/acme/sha256 2>&1 | grep -q 'SHA-1'" |
| 517 | check "it's on the host" test "$(curl -s -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/sha256" | jq_ 'd["main"]["n"]')" = 1 |
| 518 |
| 519 | # Sealed folders: encrypted on the machines of the people they're for. The |
| 520 | # host keeps them and can't read them; a machine that lacks the key is |
| 521 | # brought in by the next reader's machine, and merges happen on a reader's. |
| 522 | if [ "$TARGET" = worker ]; then |
| 523 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 524 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 525 | check "alice seals vault/ for bob" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop access vault @bob --sealed | grep -q '@bob has no machine key yet'" |
| 526 | mkdir -p shop/vault && printf 'payroll: 4100\n\nrates\n\nbonus: 0\n' > shop/vault/pay.txt |
| 527 | check "it lands" as laptop "$VCS" -C shop land -m "Payroll" |
| 528 | check "alice reads it" grep -q 'payroll: 4100' shop/vault/pay.txt |
| 529 | PLAIN=$( (printf '\001'; cat shop/vault/pay.txt) | sha256sum | cut -c1-64) |
| 530 | check "the host never had it plain" host_lacks "$PLAIN" |
| 531 | check "the host won't read it" sh -c "curl -s -H 'authorization: Bearer $ALICE' 'http://$HOST/v1/projects/acme/shop/file?path=vault/pay.txt' | grep -q sealed" |
| 532 | check "or edit it" sh -c "curl -s -X POST -H 'authorization: Bearer $ALICE' -H 'content-type: application/json' -d '{\"title\":\"x\",\"files\":{\"vault/pay.txt\":\"y\"}}' 'http://$HOST/v1/projects/acme/shop/edits' | grep -q sealed" |
| 533 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 534 | check "bob's machine has no key yet" test ! -e bobshop/vault/pay.txt |
| 535 | check "and waits for one" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop access vault | grep -q 'waiting'" |
| 536 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 537 | check "alice's next command brings bob in" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop access vault | grep -q '@bob: [0-9a-f]'" |
| 538 | check "bob reads it" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sync >/dev/null 2>&1; grep -q 'payroll: 4100' $WORK/bobshop/vault/pay.txt" |
| 539 | check "carol never gets it" sh -c "ONSET_HOME=$WORK/carol $VCS -C $WORK/carolshop sync >/dev/null 2>&1; test ! -e $WORK/carolshop/vault" |
| 540 | # Both change it: the host can't merge it, bob's machine does. |
| 541 | printf 'payroll: 4100\n\nrates\n\nbonus: 300\n' > shop/vault/pay.txt |
| 542 | as laptop "$VCS" -C shop land -m "Bonus" >/dev/null 2>&1 |
| 543 | printf 'payroll: 4500\n\nrates\n\nbonus: 0\n' > bobshop/vault/pay.txt |
| 544 | check "bob's sync merges it" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sync >/dev/null 2>&1; grep -q 'payroll: 4500' $WORK/bobshop/vault/pay.txt && grep -q 'bonus: 300' $WORK/bobshop/vault/pay.txt && ! grep -q '<<<<<<<' $WORK/bobshop/vault/pay.txt" |
| 545 | check "and lands" as bob "$VCS" -C bobshop land -m "Raise" |
| 546 | check "alice gets both" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop sync >/dev/null 2>&1; grep -q 'payroll: 4500' $WORK/shop/vault/pay.txt && grep -q 'bonus: 300' $WORK/shop/vault/pay.txt" |
| 547 | # Alice's other machine is brought in the same way. |
| 548 | as cloud "$VCS" -C cloudshop sync >/dev/null 2>&1 |
| 549 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 550 | check "a reader's machine brings alice's other one in" sh -c "ONSET_HOME=$WORK/cloud $VCS -C $WORK/cloudshop sync >/dev/null 2>&1; grep -q 'payroll: 4500' $WORK/cloudshop/vault/pay.txt" |
| 551 | check "vcs key shows a machine's fingerprint" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop key | grep -q 'machine'" |
| 552 | # Unsealed: plain again from the next land. |
| 553 | check "alice unseals it" as laptop "$VCS" -C shop access vault @bob |
| 554 | check "a land keeps it plain" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop land -m Unseal 2>&1 | grep -q main" |
| 555 | check "the host reads it now" sh -c "curl -s -H 'authorization: Bearer $ALICE' 'http://$HOST/v1/projects/acme/shop/file?path=vault/pay.txt' | grep -q base64" |
| 556 | fi |
| 557 |
| 558 | # A project has a storage limit (60 MB in this test; 100 GiB for real). |
| 559 | if [ "$TARGET" = worker ]; then |
| 560 | head -c 70000000 /dev/urandom > shop/huge.bin |
| 561 | check "a land past the limit is refused" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop land -m Huge 2>&1 | grep -q limit" |
| 562 | rm -f shop/huge.bin |
| 563 | fi |
| 564 |
| 565 | # A new machine starts with recent history (here the last 3 versions) and |
| 566 | # fetches older versions when a command asks for one. |
| 567 | NEWBOX="$WORK/newbox" |
| 568 | nb() { ONSET_HOME="$NEWBOX" ONSET_NAME="Alice" ONSET_EMAIL="alice@acme.test" ONSET_HISTORY_KEPT=3 "$@"; } |
| 569 | nb "$VCS" login "$HOST" --token "$ALICE" >/dev/null |
| 570 | check "a new machine gets the project" nb "$VCS" get "$HOST/acme/shop" newshop |
| 571 | LATEST=$(nb "$VCS" --json -C newshop log -n 1 | python3 -c 'import json,sys; print(json.load(sys.stdin)["versions"][0]["n"])') |
| 572 | check "with only recent history" test "$(python3 -c 'import sqlite3, sys; print(sqlite3.connect(sys.argv[1]).execute("SELECT count(*) FROM versions").fetchone()[0])' "$NEWBOX/vcs.db")" = 3 |
| 573 | check "an old version when asked" sh -c "ONSET_HOME=$NEWBOX $VCS -C $WORK/newshop log 2 | grep -q 'main 2'" |
| 574 | check "a longer log fetches what it shows" test "$(nb "$VCS" --json -C newshop log -n 10 | python3 -c 'import json,sys; print(len(json.load(sys.stdin)["versions"]))')" = 10 |
| 575 | check "diffs of old versions" nb "$VCS" -C newshop diff --from 1 --to 2 --stat |
| 576 | check "and it lands like any machine" sh -c "printf 'new box\n' > $WORK/newshop/newbox.txt && ONSET_HOME=$NEWBOX $VCS -C $WORK/newshop land -m 'From a new machine' | grep -q 'main $((LATEST + 1))'" |
| 577 |
| 578 | # Checks: the host's versions and what main needs, and this machine's objects. |
| 579 | check "vcs check passes" as laptop "$VCS" -C shop check |
| 580 | check "a reader can't check the host's copy" sh -c "! ONSET_HOME=$WORK/carol $VCS -C $WORK/carolshop check >/dev/null 2>&1" |
| 581 | OBJECT=$(find "$WORK/laptop/objects" -type f | head -1) |
| 582 | # Its last bytes gone, so it can't decode to what its ID says. |
| 583 | python3 -c 'import sys; p = sys.argv[1]; d = open(p, "rb").read(); open(p, "wb").write(d[:-4])' "$OBJECT" |
| 584 | check "damage on this machine is found (exit 1)" code 1 as laptop "$VCS" -C shop check |
| 585 |
| 586 | [ "$fail" != 0 ] && [ -f "$WORK/server.log" ] && { echo "server log:"; tail -n 60 "$WORK/server.log"; } |
| 587 | echo "hosted: $pass passed, $fail failed" |
| 588 | [ "$fail" = 0 ] |
| 589 |