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 | # (ONSET_FRONT_AT=0 sends every landing on the Worker through its project's front.) |
| 9 | set -uo pipefail |
| 10 |
| 11 | ROOT=$(cd "$(dirname "$0")/.." && pwd) |
| 12 | TESTS=$ROOT/tests |
| 13 | BIN=${1:-$ROOT/target/debug} |
| 14 | TARGET=${ONSET_TARGET:-server} |
| 15 | # Windows (WINDOWS=1, or Git Bash there) has no executable bit on disk; Onset keeps it in the tree. |
| 16 | case "${WINDOWS:-$(uname -s)}" in MINGW* | MSYS* | CYGWIN* | 1) WINDOWS=1 ;; *) WINDOWS= ;; esac |
| 17 | BIN_EXE= |
| 18 | [ -n "$WINDOWS" ] && [ -x "$BIN/on.exe" ] && BIN_EXE=.exe |
| 19 | VCS=$BIN/on$BIN_EXE |
| 20 | SERVER=$BIN/onset-server$BIN_EXE |
| 21 | WORK=$(mktemp -d) |
| 22 | export ONSET_SAVER=0 |
| 23 | # Workspaces' .git views are tested in local.sh; here they'd only race the suite. |
| 24 | export ONSET_GIT=0 |
| 25 | # Run from inside an agent, its own session would be kept with the tests' lands. |
| 26 | unset CLAUDE_CODE_SESSION_ID CLAUDECODE CLAUDE_CONFIG_DIR ONSET_SESSION_FILE ONSET_SESSIONS VCS_SESSION_FILE VCS_SESSIONS VCS_HOME VCS_TOKEN |
| 27 | export CODEX_HOME="$WORK/no-codex" |
| 28 | PORT=$((20000 + RANDOM % 20000)) |
| 29 | HOST="127.0.0.1:$PORT" |
| 30 | SERVER_PID= |
| 31 | stop_server() { |
| 32 | [ -z "$SERVER_PID" ] && return |
| 33 | if [ "$TARGET" = worker ]; then kill -- "-$SERVER_PID" 2>/dev/null; else kill "$SERVER_PID" 2>/dev/null; fi |
| 34 | wait "$SERVER_PID" 2>/dev/null |
| 35 | SERVER_PID= |
| 36 | } |
| 37 | cleanup() { |
| 38 | stop_server |
| 39 | # ONSET_KEEP_LOG=<file>: the server's log, for a failed run. |
| 40 | [ -n "${ONSET_KEEP_LOG:-}" ] && cp "$WORK/server.log" "$ONSET_KEEP_LOG" 2>/dev/null |
| 41 | # Lazy workspaces' mounts first (FUSE on Linux, NFS on macOS; on Windows, the lazy workspace's server). |
| 42 | grep -o " $WORK/[^ ]*" /proc/self/mountinfo 2>/dev/null | while read -r m; do fusermount3 -u "$m" 2>/dev/null; done |
| 43 | if [ "$(uname -s)" = Darwin ]; then |
| 44 | real=$(cd "$WORK" 2>/dev/null && pwd -P) |
| 45 | [ -n "$real" ] && /sbin/mount | sed -n "s|^.* on \($real/[^ ]*\) (.*|\1|p" | while read -r m; do /sbin/umount -f "$m" 2>/dev/null; done |
| 46 | fi |
| 47 | [ -d "$WORK/lazyshop/.onset" ] && ONSET_HOME="$WORK/lazy-box" "$VCS" mount "$WORK/lazyshop" --stop >/dev/null 2>&1 |
| 48 | rm -rf "$WORK" |
| 49 | } |
| 50 | trap cleanup EXIT |
| 51 | cd "$WORK" |
| 52 |
| 53 | if [ "$TARGET" = worker ]; then |
| 54 | APP=$ROOT/../apps/onset |
| 55 | ALICE=vcs_test_alice_0123456789 |
| 56 | BOB=vcs_test_bob_0123456789 |
| 57 | CAROL=vcs_test_carol_0123456789 |
| 58 | # Packs start plain; the restart for the offline checks turns encryption |
| 59 | # on, and the packs from before are encrypted as the project's used. |
| 60 | DATA_KEY=off |
| 61 | 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\"}}" |
| 62 | start_server() { |
| 63 | # Its own process group, so stopping it stops workerd too, and its own |
| 64 | # name, so a dev process left from another run never stands in for it. |
| 65 | 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))" \ |
| 66 | --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" ${ONSET_FRONT_AT:+--var "FRONT_AT:$ONSET_FRONT_AT"} >"$WORK/server.log" 2>&1 & |
| 67 | SERVER_PID=$! |
| 68 | for _ in $(seq 300); do curl -s "http://$HOST/.well-known/onset.json" 2>/dev/null | grep -q api && return; sleep 0.2; done |
| 69 | cat "$WORK/server.log"; echo "worker didn't start"; exit 1 |
| 70 | } |
| 71 | add_member() { # project, handle, role |
| 72 | 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 |
| 73 | } |
| 74 | else |
| 75 | start_server() { |
| 76 | "$SERVER" serve --data "$WORK/server" --listen "$HOST" --threads 32 2>"$WORK/server.log" & |
| 77 | SERVER_PID=$! |
| 78 | for _ in $(seq 50); do curl -s "http://$HOST/.well-known/onset.json" >/dev/null && return; sleep 0.1; done |
| 79 | echo "server didn't start"; exit 1 |
| 80 | } |
| 81 | add_member() { |
| 82 | "$SERVER" member add "$1" "$2" --role "$3" --data "$WORK/server" |
| 83 | } |
| 84 | ALICE=$("$SERVER" user add alice --name Alice --email alice@acme.test --data "$WORK/server") |
| 85 | BOB=$("$SERVER" user add bob --name Bob --email bob@acme.test --data "$WORK/server") |
| 86 | CAROL=$("$SERVER" user add carol --name Carol --email carol@acme.test --data "$WORK/server") |
| 87 | fi |
| 88 |
| 89 | pass=0 |
| 90 | fail=0 |
| 91 | check() { |
| 92 | local name=$1 |
| 93 | shift |
| 94 | if "$@" >/dev/null 2>&1; then pass=$((pass + 1)); else fail=$((fail + 1)); echo "FAIL: $name"; fi |
| 95 | } |
| 96 | code() { |
| 97 | local want=$1 |
| 98 | shift |
| 99 | "$@" >/dev/null 2>&1 |
| 100 | [ "$?" = "$want" ] |
| 101 | } |
| 102 | jq_() { python3 -c "import json,sys; d=json.load(sys.stdin); print($1)"; } |
| 103 |
| 104 | # People and machines: each has a ONSET_HOME. |
| 105 | as() { # who, command... |
| 106 | local who=$1 |
| 107 | shift |
| 108 | case $who in |
| 109 | laptop) ONSET_HOME="$WORK/laptop" ONSET_NAME="Alice" ONSET_EMAIL="alice@acme.test" "$@" ;; |
| 110 | cloud) ONSET_HOME="$WORK/cloud" ONSET_NAME="Alice" ONSET_EMAIL="alice@acme.test" "$@" ;; |
| 111 | bob) ONSET_HOME="$WORK/bob" ONSET_NAME="Bob" ONSET_EMAIL="bob@acme.test" "$@" ;; |
| 112 | carol) ONSET_HOME="$WORK/carol" ONSET_NAME="Carol" ONSET_EMAIL="carol@acme.test" "$@" ;; |
| 113 | esac |
| 114 | } |
| 115 |
| 116 | start_server |
| 117 |
| 118 | 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" |
| 119 | check "laptop login" as laptop "$VCS" login "$HOST" --token "$ALICE" |
| 120 | check "cloud login" as cloud "$VCS" login "$HOST" --token "$ALICE" |
| 121 | check "bob login" as bob "$VCS" login "$HOST" --token "$BOB" |
| 122 | check "carol login" as carol "$VCS" login "$HOST" --token "$CAROL" |
| 123 |
| 124 | # Signing in through the browser (a stand-in sign-in page approves it). |
| 125 | LPORT=$((PORT + 3)) |
| 126 | python3 "$TESTS/login-server.py" "$LPORT" "$ALICE" & |
| 127 | LOGIN_PID=$! |
| 128 | for _ in $(seq 100); do curl -s -o /dev/null -X POST "http://127.0.0.1:$LPORT/login" && break; sleep 0.1; done |
| 129 | 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'" |
| 130 | kill "$LOGIN_PID" 2>/dev/null |
| 131 | check "no browser login without a sign-in page" sh -c "ONSET_HOME=$WORK/browser2 '$VCS' login '$HOST' 2>&1 | grep -q -- '--token'" |
| 132 |
| 133 | # Alice starts locally and publishes. |
| 134 | mkdir shop && cd shop |
| 135 | printf 'line 1\nline 2\nline 3\n' > shared.txt |
| 136 | printf 'SECRET=alice\n' > .env |
| 137 | check "init" as laptop "$VCS" init |
| 138 | check "first version" as laptop "$VCS" land -m "First version" |
| 139 | printf 'line 1\nline 2\nline 3\nline 4\n' > shared.txt |
| 140 | check "second version" as laptop "$VCS" land -m "Second version" |
| 141 | check "publish" as laptop "$VCS" publish "$HOST/acme/shop" |
| 142 | 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 |
| 143 | # A project begun on a machine with no name set (no git config either): once |
| 144 | # published, its open change is the account's, not the system user's. |
| 145 | NONAME="$WORK/noname" |
| 146 | mkdir -p "$NONAME/home" "$WORK/nonameshop" |
| 147 | check "a project begun without a name lands as the account once published" sh -c "cd $WORK/nonameshop && echo a > a.txt && export HOME=$NONAME/home ONSET_HOME=$NONAME && $VCS login $HOST --token $ALICE >/dev/null && $VCS init >/dev/null && $VCS land -m first >/dev/null && $VCS publish $HOST/acme/noname >/dev/null && $VCS --json status | grep -q '\"author\":\"Alice <alice@acme.test>\"'" |
| 148 | add_member acme/shop bob member |
| 149 | add_member acme/shop carol reader |
| 150 |
| 151 | # Bob gets it. |
| 152 | cd "$WORK" |
| 153 | check "bob gets" as bob "$VCS" get "$HOST/acme/shop" bobshop |
| 154 | check "bob has the files" test "$(tail -1 bobshop/shared.txt)" = "line 4" |
| 155 | check "bob doesn't get alice's .env" test ! -e bobshop/.env |
| 156 |
| 157 | # Parallel landings from two people. |
| 158 | printf 'alice\n' > shop/alice.txt |
| 159 | printf 'bob\n' > bobshop/bob.txt |
| 160 | check "alice lands" as laptop "$VCS" -C shop land -m "Alice's file" |
| 161 | check "bob lands without syncing" as bob "$VCS" -C bobshop land -m "Bob's file" |
| 162 | check "main is 4" test "$(as bob "$VCS" --json -C bobshop log | jq_ 'd["versions"][0]["n"]')" = 4 |
| 163 |
| 164 | # A conflict between them. |
| 165 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 166 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 167 | perl -pi -e 's/^line 2$/line two (alice)/' shop/shared.txt |
| 168 | perl -pi -e 's/^line 2$/line two (bob)/' bobshop/shared.txt |
| 169 | check "alice lands line 2" as laptop "$VCS" -C shop land -m "Alice's line 2" |
| 170 | check "bob conflicts (exit 3)" code 3 as bob "$VCS" -C bobshop land -m "Bob's line 2" |
| 171 | check "bob syncs (exit 3)" code 3 as bob "$VCS" -C bobshop sync |
| 172 | check "markers" grep -q '<<<<<<< main' bobshop/shared.txt |
| 173 | perl -ni -e 'print unless /^(<<<<<<< |=======$|>>>>>>> )/ or /line two \(alice\)/' bobshop/shared.txt |
| 174 | check "bob lands after fixing" as bob "$VCS" -C bobshop land |
| 175 |
| 176 | # A reader's change waits for someone who can land it. |
| 177 | check "carol gets" as carol "$VCS" get "$HOST/acme/shop" carolshop |
| 178 | printf 'fix from carol\n' > carolshop/carol.txt |
| 179 | check "carol's land waits (exit 5)" code 5 as carol "$VCS" -C carolshop land -m "Carol's fix" |
| 180 | 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]') |
| 181 | check "alice sees the waiting change" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop log --changes | grep -q waiting" |
| 182 | as laptop "$VCS" -C shop land "$CAROL_CHANGE" >"$WORK/carol-land.out" 2>&1; cat "$WORK/carol-land.out" >&2 |
| 183 | check "alice lands carol's change" grep -q landed "$WORK/carol-land.out" |
| 184 | check "carol is the author" test "$(as laptop "$VCS" --json -C shop log -n 1 | jq_ 'd["versions"][0]["author"].split(" <")[0]')" = Carol |
| 185 |
| 186 | # Alice moves from her laptop to a cloud machine mid-change. |
| 187 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 188 | printf 'started on the laptop\n' > shop/feature.txt |
| 189 | check "describe on laptop" as laptop "$VCS" -C shop describe "Feature" |
| 190 | FEATURE=$(as laptop "$VCS" --json -C shop status | jq_ 'd["change"]["id"]') |
| 191 | check "cloud picks it up" as cloud "$VCS" get "$HOST/acme/shop" cloudshop --change "$FEATURE" |
| 192 | check "the edit came along" test "$(cat cloudshop/feature.txt)" = "started on the laptop" |
| 193 | check ".env came along" test "$(cat cloudshop/.env)" = "SECRET=alice" |
| 194 | printf 'continued in the cloud\n' >> cloudshop/feature.txt |
| 195 | check "cloud saves" as cloud "$VCS" -C cloudshop status |
| 196 | printf 'late laptop edit\n' > shop/late.txt |
| 197 | OUT=$(as laptop "$VCS" --json -C shop status 2>/dev/null) |
| 198 | check "laptop's late edit is rescued into a new change" sh -c "echo '$OUT' | grep -q 'Rescued from'" |
| 199 | check "cloud lands the feature" as cloud "$VCS" -C cloudshop land |
| 200 | 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 |
| 201 |
| 202 | # Sharing a change. |
| 203 | printf 'draft\n' > bobshop/draft.txt |
| 204 | check "bob describes" as bob "$VCS" -C bobshop describe "Draft for Alice" |
| 205 | DRAFT=$(as bob "$VCS" --json -C bobshop status | jq_ 'd["change"]["id"]') |
| 206 | check "bob shares it" as bob "$VCS" -C bobshop access "$DRAFT" @alice |
| 207 | check "alice sees it" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop log --changes | grep -q 'Draft for Alice'" |
| 208 |
| 209 | # Private paths: only Alice sees billing/. (First drop the rescued change, |
| 210 | # which still has the takeover's conflict in it.) |
| 211 | as laptop "$VCS" -C shop drop >/dev/null 2>&1 |
| 212 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 213 | mkdir -p shop/billing && printf 'rate 10\n' > shop/billing/rates.txt |
| 214 | check "alice lands billing" as laptop "$VCS" -C shop land -m "Billing rates" |
| 215 | 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" |
| 216 | check "alice makes billing/ hers" as laptop "$VCS" -C shop access billing @alice |
| 217 | 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" |
| 218 | printf 'bob was here\n' >> bobshop/README.md 2>/dev/null || printf 'bob was here\n' > bobshop/README.md |
| 219 | check "bob lands outside billing" as bob "$VCS" -C bobshop land -m "Bob's readme" |
| 220 | 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" |
| 221 | mkdir -p bobshop/billing && printf 'sneaky\n' > bobshop/billing/new.txt |
| 222 | check "bob can't write into billing (exit 7)" code 7 as bob "$VCS" -C bobshop land -m "Sneaky" |
| 223 | rm -rf bobshop/billing |
| 224 | as bob "$VCS" -C bobshop drop >/dev/null 2>&1 |
| 225 | printf 'rate 12\n' > shop/billing/rates.txt |
| 226 | check "alice changes billing" as laptop "$VCS" -C shop land -m "New billing rates" |
| 227 | 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'" |
| 228 | 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" |
| 229 | 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" |
| 230 | # Naming the full tree in a change of his own doesn't get bob in either. |
| 231 | FULL=$(curl -s -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/shop" | jq_ 'd["main"]["tree"]') |
| 232 | CLAIM="{\"change\":{\"id\":\"bcdfghjkmnpq\",\"base_tree\":\"$FULL\",\"head_tree\":\"$FULL\"},\"workspace\":\"w1\"}" |
| 233 | 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" |
| 234 | 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" |
| 235 |
| 236 | # Undo a hosted land. |
| 237 | as cloud "$VCS" -C cloudshop sync >/dev/null 2>&1 |
| 238 | printf 'oops\n' > cloudshop/oops.txt |
| 239 | check "cloud lands oops" as cloud "$VCS" -C cloudshop land -m "Oops" |
| 240 | check "cloud undoes it" as cloud "$VCS" -C cloudshop undo |
| 241 | check "main took it back" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop log -n 1 | grep -q 'Take back'" |
| 242 | check "the undone change lands again" as cloud "$VCS" -C cloudshop land -m "Oops, kept" |
| 243 | check "and it's on main again" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sync >/dev/null 2>&1; test -f $WORK/bobshop/oops.txt" |
| 244 |
| 245 | # A land whose answer was lost (a dropped connection, a client that gave up): |
| 246 | # landing again answers as the first time, and never lands it twice, even |
| 247 | # after a later version took its edits back out. |
| 248 | main_n() { curl -s -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/shop" | jq_ 'd["main"]["n"]'; } |
| 249 | as cloud "$VCS" -C cloudshop sync >/dev/null 2>&1 |
| 250 | printf 'retry\n' > cloudshop/retry.txt |
| 251 | as cloud "$VCS" -C cloudshop describe "Retry me" >/dev/null 2>&1 |
| 252 | rm -rf "$WORK/cloud-before" "$WORK/cloudshop-before" |
| 253 | cp -a "$WORK/cloud" "$WORK/cloud-before" && cp -a cloudshop/.onset "$WORK/cloudshop-before" |
| 254 | check "a land goes through" as cloud "$VCS" -C cloudshop land |
| 255 | LANDED=$(main_n) |
| 256 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 257 | rm -f bobshop/retry.txt |
| 258 | check "bob takes its file out again" as bob "$VCS" -C bobshop land -m "No retry file" |
| 259 | rm -rf "$WORK/cloud" cloudshop/.onset && mv "$WORK/cloud-before" "$WORK/cloud" && mv "$WORK/cloudshop-before" cloudshop/.onset |
| 260 | check "landing again answers as the first time" sh -c "ONSET_HOME=$WORK/cloud ONSET_NAME=Alice ONSET_EMAIL=alice@acme.test $VCS -C $WORK/cloudshop land | grep -q 'as main $LANDED'" |
| 261 | check "main didn't move" test "$(main_n)" = "$((LANDED + 1))" |
| 262 | check "bob's version still holds" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sync >/dev/null 2>&1; test ! -e $WORK/bobshop/retry.txt" |
| 263 | as cloud "$VCS" -C cloudshop sync >/dev/null 2>&1 |
| 264 | printf 'oops\n' > bobshop/oops-again.txt |
| 265 | cp bobshop/oops-again.txt cloudshop/oops-again.txt |
| 266 | check "bob lands a file" as bob "$VCS" -C bobshop land -m "Oops again" |
| 267 | check "the same edits from someone else are on main already (exit 1)" code 1 as cloud "$VCS" -C cloudshop land -m "Same file" |
| 268 | as cloud "$VCS" -C cloudshop drop >/dev/null 2>&1 |
| 269 |
| 270 | # Offline: saves stay local, land says offline, later it all goes up. |
| 271 | if [ "$TARGET" = worker ]; then |
| 272 | # A pack written plain, before encryption is on. |
| 273 | { printf 'PLAIN-MARKER-6c1e\n'; head -c 700000 /dev/urandom; } > shop/plain.bin |
| 274 | as laptop "$VCS" -C shop land -m "Written plain" >/dev/null 2>&1 |
| 275 | fi |
| 276 | stop_server |
| 277 | printf 'written offline\n' > cloudshop/offline.txt |
| 278 | check "status works offline" as cloud "$VCS" -C cloudshop status |
| 279 | check "land says offline (exit 6)" code 6 as cloud "$VCS" -C cloudshop land -m "Offline work" |
| 280 | DATA_KEY=test-data-key-2026 |
| 281 | start_server |
| 282 | check "land works once back" as cloud "$VCS" -C cloudshop land -m "Offline work" |
| 283 |
| 284 | # 32 agents land at once on one main, none refused. |
| 285 | BEFORE=$(as bob "$VCS" --json -C bobshop log -n 1 | jq_ 'd["versions"][0]["n"]') |
| 286 | for i in $(seq 1 32); do |
| 287 | as bob "$VCS" get "$HOST/acme/shop" "agent$i" >/dev/null 2>&1 |
| 288 | printf 'agent %s\n' "$i" > "agent$i/agent-$i.txt" |
| 289 | done |
| 290 | pids=() |
| 291 | for i in $(seq 1 32); do |
| 292 | (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") & |
| 293 | pids+=($!) |
| 294 | done |
| 295 | wait "${pids[@]}" |
| 296 | OK=$(grep -l '^0$' "$WORK"/land*.code | wc -l | tr -d " ") |
| 297 | check "all 32 landed" test "$OK" = 32 |
| 298 | for i in $(seq 1 32); do |
| 299 | [ "$(cat "$WORK/land$i.code")" = 0 ] || { echo "agent $i:"; cat "$WORK/land$i.out"; } |
| 300 | done |
| 301 | AFTER=$(as bob "$VCS" --json -C bobshop log -n 1 | jq_ 'd["versions"][0]["n"]') |
| 302 | check "main moved by 32" test "$((AFTER - BEFORE))" = 32 |
| 303 | # An agent that landed in the middle still sees every version, in order. |
| 304 | 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'" |
| 305 |
| 306 | # Landing without a workspace: file edits in one call. |
| 307 | edits() { # token, json |
| 308 | curl -s -X POST -H "authorization: Bearer $1" -H 'content-type: application/json' -d "$2" "http://$HOST/v1/projects/acme/shop/edits" |
| 309 | } |
| 310 | 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\"'" |
| 311 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 312 | check "edits reach workspaces" test "$(cat bobshop/api/hello.txt 2>/dev/null)" = hello |
| 313 | [ -z "$WINDOWS" ] && check "edits keep the executable bit" test -x bobshop/api/run.sh |
| 314 | check "edits to a file on main need the version they were made on" sh -c "$(declare -f edits); HOST=$HOST; edits $ALICE '{\"title\":\"No base\",\"files\":{\"api/hello.txt\":null}}' | grep -q 'send base'" |
| 315 | HELLO_AT=$(main_n) |
| 316 | check "edits can delete" sh -c "$(declare -f edits); HOST=$HOST; edits $ALICE '{\"title\":\"Remove hello\",\"base\":$HELLO_AT,\"files\":{\"api/hello.txt\":null}}' | grep -q '\"outcome\":\"landed\"'" |
| 317 | AFTER_HELLO=$(main_n) |
| 318 | check "the same edits again answer as the first time" sh -c "$(declare -f edits); HOST=$HOST; edits $ALICE '{\"title\":\"Remove hello\",\"base\":$HELLO_AT,\"files\":{\"api/hello.txt\":null}}' | grep -q '\"n\":$AFTER_HELLO'" |
| 319 | check "and land nothing" test "$(main_n)" = "$AFTER_HELLO" |
| 320 | # Edits made on an older version merge with what landed since, file by file. |
| 321 | printf 'one\ntwo\nthree\n' > bobshop/merge-me.txt |
| 322 | as bob "$VCS" -C bobshop land -m "Merge me" >/dev/null 2>&1 |
| 323 | READ_AT=$(main_n) |
| 324 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 325 | printf 'one\ntwo\nthree, by bob\n' > bobshop/merge-me.txt |
| 326 | as bob "$VCS" -C bobshop land -m "Bob's third line" >/dev/null 2>&1 |
| 327 | check "edits on an older version keep what landed since" sh -c "$(declare -f edits); HOST=$HOST; edits $ALICE '{\"title\":\"First line\",\"base\":$READ_AT,\"files\":{\"merge-me.txt\":\"ONE\\ntwo\\nthree\\n\"}}' | grep -q '\"outcome\":\"landed\"'" |
| 328 | check "both edits are there" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sync >/dev/null 2>&1; test \"\$(head -1 $WORK/bobshop/merge-me.txt)\" = ONE && test \"\$(tail -1 $WORK/bobshop/merge-me.txt)\" = 'three, by bob'" |
| 329 | # Every version of a file reads as it was, in a folder landed on over and |
| 330 | # over (Upstream's host keeps each folder as a delta on the one before, a |
| 331 | # chain at most 32 deep), and still after a restart, with the chains moved |
| 332 | # to storage. |
| 333 | mkdir -p bobshop/rounds && for i in $(seq 1 60); do printf 'file %s\n' "$i" > "bobshop/rounds/f$i.txt"; done |
| 334 | as bob "$VCS" -C bobshop land -m "Rounds" >/dev/null 2>&1 |
| 335 | FIRST=$(main_n) |
| 336 | at=$FIRST |
| 337 | for r in $(seq 1 40); do at=$(edits "$ALICE" "{\"title\":\"Round $r\",\"base\":$at,\"files\":{\"rounds/f7.txt\":\"round $r\\n\"}}" | jq_ 'd["version"]["n"]'); done |
| 338 | read_at() { curl -s -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/shop/file?path=rounds/f7.txt&version=$1" | jq_ '__import__("base64").b64decode(d["base64"]).decode().strip()'; } |
| 339 | every_round() { |
| 340 | [ "$(read_at "$FIRST")" = "file 7" ] || return 1 |
| 341 | for r in 1 2 16 31 32 33 34 39 40; do [ "$(read_at $((FIRST + r)))" = "round $r" ] || return 1; done |
| 342 | } |
| 343 | check "every version of a file reads as it was" every_round |
| 344 | deltas_kept() { curl -s -X POST -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/shop/check" | jq_ 'd["deltas"]'; } |
| 345 | [ "$TARGET" = worker ] && check "the host kept them as deltas" test "$(deltas_kept)" -gt 40 |
| 346 | # A CLI's lands send the folders they change as deltas on the ones they |
| 347 | # replaced (the self-hosted server makes them whole as they come in). |
| 348 | [ "$TARGET" = worker ] && KEPT=$(deltas_kept) |
| 349 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 350 | for r in 1 2 3; do |
| 351 | printf 'cli %s\n' "$r" > bobshop/rounds/f9.txt |
| 352 | as bob "$VCS" -C bobshop land -m "CLI round $r" >/dev/null 2>&1 |
| 353 | done |
| 354 | check "a CLI's lands go in" test "$(cat bobshop/rounds/f9.txt)" = "cli 3" -a "$(main_n)" -ge $((at + 3)) |
| 355 | [ "$TARGET" = worker ] && check "as deltas on what they replaced" test "$(deltas_kept)" -ge $((KEPT + 3)) |
| 356 | stop_server |
| 357 | start_server |
| 358 | check "and after a restart" every_round |
| 359 | check "a new machine gets that folder whole" sh -c "ONSET_HOME=$WORK/carol $VCS get $HOST/acme/shop $WORK/roundscheck >/dev/null 2>&1 && test \"\$(cat $WORK/roundscheck/rounds/f7.txt)\" = 'round 40' && test \"\$(cat $WORK/roundscheck/rounds/f9.txt)\" = 'cli 3' && test \$(ls $WORK/roundscheck/rounds | wc -l) -eq 60" |
| 360 | 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\"'" |
| 361 | check "bad paths refused in edits" sh -c "$(declare -f edits); HOST=$HOST; edits $ALICE '{\"title\":\"Escape\",\"files\":{\"../x\":\"x\"}}' | grep -q bad_usage" |
| 362 | 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" |
| 363 | # A nested git checkout lands as files, never its .git. |
| 364 | 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 |
| 365 | check "a nested git checkout lands" as bob "$VCS" -C bobshop land -m "Vendored lib" |
| 366 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 367 | check "its files reach workspaces" test -f shop/vendor/lib/lib.txt |
| 368 | check "its .git doesn't" test ! -e shop/vendor/lib/.git |
| 369 | 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 |
| 370 | 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 |
| 371 | 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 |
| 372 | 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" |
| 373 | 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" |
| 374 | 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" |
| 375 | 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" |
| 376 | check "tag on the host" as laptop "$VCS" -C shop tag v1.0 2 |
| 377 | check "bob sees the tag" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop tag | grep -q 'v1.0'" |
| 378 | check "show at a tag on the host" test "$(as bob "$VCS" -C bobshop show shared.txt --from v1.0 | tail -1)" = "line 4" |
| 379 | check "a reader can't tag (exit 7)" code 7 as carol "$VCS" -C carolshop tag nope |
| 380 | 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\"'" |
| 381 |
| 382 | # Groups: a folder for a group. |
| 383 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 384 | check "alice makes a group" as laptop "$VCS" -C shop group ops @bob |
| 385 | check "the group lists bob" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop group | grep -q '@ops.*@bob'" |
| 386 | mkdir -p shop/ops && printf 'runbook\n' > shop/ops/runbook.txt |
| 387 | check "alice lands ops/" as laptop "$VCS" -C shop land -m "Runbook" |
| 388 | check "ops/ is the group's" as laptop "$VCS" -C shop access ops @ops |
| 389 | 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" |
| 390 | 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" |
| 391 | check "only owners change groups (exit 7)" code 7 as bob "$VCS" -C bobshop group ops @bob @carol |
| 392 |
| 393 | # Approvals: a land waits until someone else approves it. |
| 394 | check "alice requires an approval" as laptop "$VCS" -C shop settings approvals=1 |
| 395 | printf 'reviewed\n' > shop/reviewed.txt |
| 396 | check "alice's land waits (exit 5)" code 5 as laptop "$VCS" -C shop land -m "Needs review" |
| 397 | 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]') |
| 398 | check "alice can't approve her own (exit 7)" code 7 as laptop "$VCS" -C shop approve "$REVIEW" |
| 399 | check "bob approves, and it lands" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop approve $REVIEW | grep -q landed" |
| 400 | 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" |
| 401 | check "approvals off again" as laptop "$VCS" -C shop settings approvals=0 |
| 402 |
| 403 | # MCP, for agents without a workspace (Upstream's host). |
| 404 | if [ "$TARGET" = worker ]; then |
| 405 | rpc() { curl -s -X POST -H "authorization: Bearer $ALICE" -H 'content-type: application/json' -d "$1" "http://$HOST/mcp"; } |
| 406 | check "mcp initialize" sh -c "$(declare -f rpc); ALICE=$ALICE HOST=$HOST; rpc '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}' | grep -q protocolVersion" |
| 407 | 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" |
| 408 | 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" |
| 409 | 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'" |
| 410 | 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'" |
| 411 | 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'" |
| 412 | 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'" |
| 413 | 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" |
| 414 | fi |
| 415 |
| 416 | # A bigger tree on a new machine: the hosted service sends it from a |
| 417 | # snapshot once it has made one, with what's newer on top, and never sends |
| 418 | # someone a snapshot of files they can't see. |
| 419 | fresh() { # n, token, command... |
| 420 | local n=$1 token=$2 |
| 421 | shift 2 |
| 422 | ONSET_HOME="$WORK/fresh$n" "$VCS" login "$HOST" --token "$token" >/dev/null 2>&1 |
| 423 | ONSET_HOME="$WORK/fresh$n" ONSET_NAME=Fresh ONSET_EMAIL=fresh@acme.test "$@" |
| 424 | } |
| 425 | tree_of() { curl -s -H "authorization: Bearer $1" "http://$HOST/v1/projects/acme/shop" | jq_ 'd["main"]["tree"]'; } |
| 426 | 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:'; } |
| 427 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 428 | mkdir -p shop/many && for i in $(seq 1 300); do printf 'file %s\n' "$i" > "shop/many/f$i.txt"; done |
| 429 | snap_get() { # n, token, folder: a new machine's copy has many/ as it is |
| 430 | fresh "$1" "$2" "$VCS" get "$HOST/acme/shop" "$WORK/$3" >/dev/null 2>&1 && diff -r "$WORK/shop/many" "$WORK/$3/many" |
| 431 | } |
| 432 | check "land 300 files" as laptop "$VCS" -C shop land -m "Many files" |
| 433 | check "a new machine gets them all" snap_get 1 "$ALICE" snap1 |
| 434 | check "billing too, for alice" test -e snap1/billing/rates.txt |
| 435 | check "billing's bytes are there to find" grep -rq 'rate 12' "$WORK/fresh1" |
| 436 | if [ "$TARGET" = worker ]; then |
| 437 | for _ in $(seq 50); do from_snapshot "$ALICE" && break; sleep 0.2; done |
| 438 | check "the host made a snapshot" from_snapshot "$ALICE" |
| 439 | 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:'" |
| 440 | fi |
| 441 | printf 'changed\n' > shop/many/f7.txt && printf 'new\n' > shop/many/new.txt && rm shop/many/f9.txt |
| 442 | check "land changes past it" as laptop "$VCS" -C shop land -m "Past the snapshot" |
| 443 | check "the next machine gets the snapshot and what's newer" snap_get 2 "$ALICE" snap2 |
| 444 | check "billing too" test -e snap2/billing/rates.txt |
| 445 | [ "$TARGET" = worker ] && check "that came from the snapshot" from_snapshot "$ALICE" |
| 446 | check "bob's new machine gets it without billing" snap_get 3 "$BOB" snap3 |
| 447 | check "no billing folder" test ! -e snap3/billing |
| 448 | check "and none of billing's bytes" sh -c "! grep -rq 'rate 12' '$WORK/fresh3'" |
| 449 |
| 450 | # Renames on the host: an edit follows a moved file when it lands, and log follows the move. |
| 451 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 452 | mkdir -p shop/notes && for i in $(seq 1 30); do printf 'line %s\n' "$i"; done > shop/notes/a.txt |
| 453 | check "land a file to move" as laptop "$VCS" -C shop land -m "Notes to move" |
| 454 | check "a second workspace on it" as cloud "$VCS" get "$HOST/acme/shop" movetwo |
| 455 | mkdir -p shop/kept && mv shop/notes/a.txt shop/kept/a.md |
| 456 | check "land the move" as laptop "$VCS" -C shop land -m "Move them" |
| 457 | perl -pi -e 's/^line 9$/line nine/' movetwo/notes/a.txt |
| 458 | check "an edit to the old name lands" as cloud "$VCS" -C movetwo land -m "Edit before the move" |
| 459 | 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" |
| 460 | 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'" |
| 461 |
| 462 | # Signed lands (Upstream's host keeps keys): on key, then lands are signed |
| 463 | # and on verify checks them; a project can take signed lands only. |
| 464 | if [ "$TARGET" = worker ]; then |
| 465 | check "vcs key" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop key | grep -q 'to @alice'" |
| 466 | printf 'signed\n' > shop/signed.txt |
| 467 | check "a signed land" as laptop "$VCS" -C shop land -m "Signed work" |
| 468 | check "verify says who signed it" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop verify | grep -q 'by @alice'" |
| 469 | check "an older unsigned version says so (exit 1)" code 1 as laptop "$VCS" -C shop verify 1 |
| 470 | check "signed lands only" as laptop "$VCS" -C shop settings signed=required |
| 471 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 472 | printf 'unsigned\n' > bobshop/unsigned.txt |
| 473 | check "bob's unsigned land is refused" sh -c "! ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop land -m Unsigned >/dev/null 2>&1" |
| 474 | check "back to optional" as laptop "$VCS" -C shop settings signed=optional |
| 475 | check "and bob's lands" as bob "$VCS" -C bobshop land -m "Unsigned" |
| 476 | fi |
| 477 |
| 478 | # Release lines on the host: alice starts 1.x, bob gets a workspace on it and |
| 479 | # lands a fix there, and a fix that landed on main comes over. |
| 480 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 481 | check "alice starts line 1.x" as laptop "$VCS" -C shop line 1.x |
| 482 | check "a reader can't start one" sh -c "! ONSET_HOME=$WORK/carol $VCS -C $WORK/carolshop line 2.x >/dev/null 2>&1" |
| 483 | check "bob gets a workspace on it" as bob "$VCS" get "$HOST/acme/shop" bob1x --line 1.x |
| 484 | printf 'fix on 1.x\n' > bob1x/line-fix.txt |
| 485 | 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'" |
| 486 | 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'" |
| 487 | 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" |
| 488 | printf 'main fix\n' > shop/main-fix.txt |
| 489 | as laptop "$VCS" -C shop land -m "Main fix" >/dev/null 2>&1 |
| 490 | MAINFIX=$(as laptop "$VCS" --json -C shop log -n 1 | jq_ 'd["versions"][0]["change"]') |
| 491 | 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'" |
| 492 | 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" |
| 493 | check "and nothing private" test ! -e bob1x/billing |
| 494 |
| 495 | # Purge: alice (an owner) takes a key (small, kept beside the index) and a |
| 496 | # big file (in a pack, which is written again without it) out of the |
| 497 | # history; bob's machine takes them out when it next syncs; bob (a member, |
| 498 | # not an owner) can't. |
| 499 | printf 'not-a-real-key-hosted\n' > shop/leaked.pem |
| 500 | as laptop "$VCS" -C shop land -m "Oops" --allow-secrets >/dev/null 2>&1 |
| 501 | head -c 900000 /dev/urandom > shop/big.bin |
| 502 | BIG=$( (printf '\001'; cat shop/big.bin) | sha256sum | cut -c1-64) |
| 503 | as laptop "$VCS" -C shop land -m "A big file" >/dev/null 2>&1 |
| 504 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 505 | check "bob has the key" test -f bobshop/leaked.pem |
| 506 | check "bob can't purge" sh -c "! ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop purge leaked.pem --yes >/dev/null 2>&1" |
| 507 | check "alice sees what it would do" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop purge leaked.pem | grep -q would" |
| 508 | check "alice purges them" as laptop "$VCS" -C shop purge leaked.pem big.bin --yes |
| 509 | check "gone from alice's workspace" test ! -e shop/leaked.pem -a ! -e shop/big.bin |
| 510 | LEAKED=$(printf '\001not-a-real-key-hosted\n' | sha256sum | cut -c1-64) |
| 511 | host_lacks() { # object id (hex): the host says it's missing (deleting it runs just after the purge) |
| 512 | for _ in $(seq 50); do |
| 513 | [ "$(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 |
| 514 | sleep 0.2 |
| 515 | done |
| 516 | return 1 |
| 517 | } |
| 518 | check "the host deleted the key" host_lacks "$LEAKED" |
| 519 | check "and the big file" host_lacks "$BIG" |
| 520 | 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" |
| 521 | check "no version has it" sh -c "! ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop show leaked.pem --from main >/dev/null 2>&1" |
| 522 | 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" |
| 523 | PURGED="$WORK/purged-box" |
| 524 | 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" |
| 525 | check "the host's copy checks out" as laptop "$VCS" -C shop check |
| 526 |
| 527 | # Encrypted at rest (Upstream's host): packs written before encryption was |
| 528 | # on are written again encrypted, and a new pack has no plain bytes in R2. |
| 529 | if [ "$TARGET" = worker ]; then |
| 530 | unencrypted() { curl -s -X POST -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/shop/check" | jq_ 'd["unencrypted"]'; } |
| 531 | all_encrypted() { |
| 532 | for _ in $(seq 100); do |
| 533 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 534 | [ "$(unencrypted)" = 0 ] && return 0 |
| 535 | sleep 0.3 |
| 536 | done |
| 537 | return 1 |
| 538 | } |
| 539 | check "the old packs are encrypted as the project's used" all_encrypted |
| 540 | { printf 'SEALED-MARKER-9d2b\n'; head -c 700000 /dev/urandom; } > shop/sealed.bin |
| 541 | check "a land after it" as laptop "$VCS" -C shop land -m "Written encrypted" |
| 542 | check "R2 has the plain pack's bytes (kept for rollbacks)" grep -rqa 'PLAIN-MARKER-6c1e' "$WORK/wrangler" |
| 543 | check "but not the new pack's" sh -c "! grep -rqa 'SEALED-MARKER-9d2b' '$WORK/wrangler'" |
| 544 | ENC="$WORK/encrypted-box" |
| 545 | 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" |
| 546 | check "and the host's copy checks out" as laptop "$VCS" -C shop check |
| 547 | fi |
| 548 |
| 549 | # Lazy workspaces (Linux with FUSE, macOS with its NFS client, Windows |
| 550 | # where the folder is a Cloud Files sync root of placeholders): every file |
| 551 | # shows at once at its size, and each comes from the host the first time |
| 552 | # it's opened; work lands as from any workspace, and the mount comes back |
| 553 | # with the next command. |
| 554 | LAZY= |
| 555 | [ "$(uname -s)" = Linux ] && [ -e /dev/fuse ] && command -v fusermount3 >/dev/null && LAZY=1 |
| 556 | [ "$(uname -s)" = Darwin ] && [ -x /sbin/mount_nfs ] && LAZY=1 |
| 557 | [ -n "$WINDOWS" ] && LAZY=1 |
| 558 | if [ -n "$LAZY" ]; then |
| 559 | size_of() { stat -c %s "$1" 2>/dev/null || stat -f %z "$1"; } |
| 560 | objects() { echo $(($(find "$LZ/objects" -type f | wc -l))); } |
| 561 | LZ="$WORK/lazy-box" |
| 562 | lz() { ONSET_HOME="$LZ" ONSET_NAME="Alice" ONSET_EMAIL="alice@acme.test" "$@"; } |
| 563 | lz "$VCS" login "$HOST" --token "$ALICE" >/dev/null |
| 564 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 565 | check "get a lazy workspace" lz "$VCS" get "$HOST/acme/shop" lazyshop --lazy |
| 566 | [ -z "$WINDOWS" ] && check "it's a mount" sh -c "mount | grep -qF \" on $(cd lazyshop && pwd -P) \"" |
| 567 | check "the files are all there" test "$(cd lazyshop && find . -name .onset -prune -o -name .git -prune -o -type f -print | grep -v '/\.env' | sort)" = "$(cd shop && find . -name .onset -prune -o -name .git -prune -o -type f -print | grep -v '/\.env' | sort)" |
| 568 | check "at their sizes" test "$(size_of lazyshop/shared.txt)" = "$(size_of shop/shared.txt)" |
| 569 | FETCHED=$(objects) |
| 570 | check "a file comes when it's opened" cmp -s lazyshop/shared.txt shop/shared.txt |
| 571 | check "and only that file" test "$(objects)" = "$((FETCHED + 1))" |
| 572 | check "nothing looks changed" sh -c "ONSET_HOME=$LZ $VCS -C $WORK/lazyshop status | grep -q 'no changes yet'" |
| 573 | check "and looking fetched nothing" test "$(objects)" = "$((FETCHED + 1))" |
| 574 | printf 'from a lazy workspace\n' >> lazyshop/shared.txt |
| 575 | mkdir -p lazyshop/lazy && printf 'new\n' > lazyshop/lazy/new.txt |
| 576 | if [ -n "$WINDOWS" ]; then |
| 577 | # Git Bash's mv can't move a file Windows hasn't fetched yet (OneDrive's |
| 578 | # neither): it takes the file for a volume mount point. Windows' own move can. |
| 579 | cmd //c "move $(cygpath -w "$WORK/lazyshop/feature.txt") $(cygpath -w "$WORK/lazyshop/feature-moved.txt")" >/dev/null |
| 580 | else |
| 581 | mv lazyshop/feature.txt lazyshop/feature-moved.txt |
| 582 | fi |
| 583 | check "status shows the work" sh -c "ONSET_HOME=$LZ $VCS -C $WORK/lazyshop status | grep -q 'R feature.txt -> feature-moved.txt'" |
| 584 | check "it lands" lz "$VCS" -C lazyshop land -m "From a lazy workspace" |
| 585 | 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" |
| 586 | printf 'from the laptop\n' > shop/to-lazy.txt |
| 587 | as laptop "$VCS" -C shop land -m "For the lazy one" >/dev/null 2>&1 |
| 588 | 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" |
| 589 | check "the mount stops" lz "$VCS" mount lazyshop --stop |
| 590 | if [ -n "$WINDOWS" ]; then |
| 591 | check "leaving every file listed" test -f lazyshop/many/f1.txt |
| 592 | else |
| 593 | check "leaving an empty folder" test -z "$(ls lazyshop)" |
| 594 | fi |
| 595 | check "a file not fetched yet waits for it" sh -c "! cat $WORK/lazyshop/many/f1.txt" |
| 596 | 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 && grep -qx 'file 1' $WORK/lazyshop/many/f1.txt" |
| 597 | if [ -z "$WINDOWS" ]; then |
| 598 | # The process serving the mount dies (a crash): the next command takes |
| 599 | # the dead mount down and starts another. |
| 600 | pkill -9 -f "mount --serve .*lazyshop" || true |
| 601 | sleep 1 |
| 602 | before=$fail |
| 603 | check "after its process dies, the next command starts it again" sh -c "ONSET_HOME=$LZ $VCS -C $WORK/lazyshop status >$WORK/crash.out 2>&1 && grep -qx 'file 1' $WORK/lazyshop/many/f1.txt" |
| 604 | if [ "$fail" != "$before" ]; then |
| 605 | cat "$WORK/crash.out" |
| 606 | mount | grep lazyshop || true |
| 607 | fi |
| 608 | fi |
| 609 | lz "$VCS" mount lazyshop --stop >/dev/null 2>&1 |
| 610 | fi |
| 611 |
| 612 | # Agent sessions on the host: kept with the change, seen by its owner on any |
| 613 | # of their machines and by people the change is shared with, and deleted from |
| 614 | # the host when its owner deletes it. |
| 615 | mkdir -p "$WORK/claude-a/projects/-x" |
| 616 | HSES="$WORK/claude-a/projects/-x/host-1.jsonl" |
| 617 | printf '%s\n' '{"type":"user","message":{"role":"user","content":"add the session file"}}' '{"type":"assistant","message":{"content":[{"type":"text","text":"Adding it now."}]}}' > "$HSES" |
| 618 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 619 | printf 'with a session\n' > shop/session.txt |
| 620 | 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'" |
| 621 | SC=$(as laptop "$VCS" --json -C shop log -n 1 | jq_ 'd["versions"][0]["change"]') |
| 622 | 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.'" |
| 623 | 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.'" |
| 624 | check "alice shares the change with bob" as laptop "$VCS" -C shop access "$SC" @bob |
| 625 | check "then bob does" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sessions show $SC | grep -q 'Adding it now.'" |
| 626 | SESSION_TEXT=$( (printf '\001'; cat "$HSES") | sha256sum | cut -c1-64) |
| 627 | # Upstream's host keeps machine keys, so it only ever had the sealed text. |
| 628 | [ "$TARGET" = worker ] && check "the host never had its text (sealed)" host_lacks "$SESSION_TEXT" |
| 629 | check "alice deletes it" as laptop "$VCS" -C shop sessions delete "$SC" |
| 630 | check "the host deleted its text" host_lacks "$SESSION_TEXT" |
| 631 | 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.'" |
| 632 |
| 633 | # A project imported from a SHA-256 git repository publishes like any other; |
| 634 | # its git view (SHA-1) gets new commits, and says so. |
| 635 | git init -q --object-format=sha256 "$WORK/sha256src" |
| 636 | (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") |
| 637 | check "import a SHA-256 repository" as laptop "$VCS" import "$WORK/sha256src" "$WORK/sha256proj" |
| 638 | 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'" |
| 639 | check "it's on the host" test "$(curl -s -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/sha256" | jq_ 'd["main"]["n"]')" = 1 |
| 640 |
| 641 | # Sealed folders: encrypted on the machines of the people they're for. The |
| 642 | # host keeps them and can't read them; a machine that lacks the key is |
| 643 | # brought in by the next reader's machine, and merges happen on a reader's. |
| 644 | if [ "$TARGET" = worker ]; then |
| 645 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 646 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 647 | # Bob's machine put its key on his account when it got the project, so it's in from the start. |
| 648 | check "alice seals vault/ for bob" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop access vault @bob --sealed | grep -q '@bob: [0-9a-f]'" |
| 649 | mkdir -p shop/vault && printf 'payroll: 4100\n\nrates\n\nbonus: 0\n' > shop/vault/pay.txt |
| 650 | check "it lands" as laptop "$VCS" -C shop land -m "Payroll" |
| 651 | check "alice reads it" grep -q 'payroll: 4100' shop/vault/pay.txt |
| 652 | PLAIN=$( (printf '\001'; cat shop/vault/pay.txt) | sha256sum | cut -c1-64) |
| 653 | check "the host never had it plain" host_lacks "$PLAIN" |
| 654 | 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" |
| 655 | 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" |
| 656 | 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" |
| 657 | # A machine of bob's that's new since waits for the key, and a reader's next command brings it in. |
| 658 | ONSET_HOME="$WORK/bob2" "$VCS" login "$HOST" --token "$BOB" >/dev/null 2>&1 |
| 659 | ONSET_HOME="$WORK/bob2" ONSET_NAME="Bob" ONSET_EMAIL="bob@acme.test" "$VCS" get "$HOST/acme/shop" bob2shop >/dev/null 2>&1 |
| 660 | check "bob's new machine has no key yet" test ! -e bob2shop/vault/pay.txt |
| 661 | check "and waits for one" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop access vault | grep -q 'waiting'" |
| 662 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 663 | check "alice's next command brings it in" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop access vault | grep -v waiting | grep -q '@bob: [0-9a-f]*, [0-9a-f]'" |
| 664 | check "bob's new machine reads it" sh -c "ONSET_HOME=$WORK/bob2 ONSET_NAME=Bob ONSET_EMAIL=bob@acme.test $VCS -C $WORK/bob2shop sync >/dev/null 2>&1; grep -q 'payroll: 4100' $WORK/bob2shop/vault/pay.txt" |
| 665 | 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" |
| 666 | # Both change it: the host can't merge it, bob's machine does. |
| 667 | printf 'payroll: 4100\n\nrates\n\nbonus: 300\n' > shop/vault/pay.txt |
| 668 | as laptop "$VCS" -C shop land -m "Bonus" >/dev/null 2>&1 |
| 669 | printf 'payroll: 4500\n\nrates\n\nbonus: 0\n' > bobshop/vault/pay.txt |
| 670 | 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" |
| 671 | check "and lands" as bob "$VCS" -C bobshop land -m "Raise" |
| 672 | 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" |
| 673 | # Alice's other machine is brought in the same way. |
| 674 | as cloud "$VCS" -C cloudshop sync >/dev/null 2>&1 |
| 675 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 676 | 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" |
| 677 | check "vcs key shows a machine's fingerprint" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop key | grep -q 'machine'" |
| 678 | # Unsealed: plain again from the next land. |
| 679 | check "alice unseals it" as laptop "$VCS" -C shop access vault @bob |
| 680 | check "a land keeps it plain" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop land -m Unseal 2>&1 | grep -q main" |
| 681 | 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" |
| 682 | fi |
| 683 |
| 684 | # A project has a storage limit (60 MB in this test; 100 GiB for real). |
| 685 | if [ "$TARGET" = worker ]; then |
| 686 | head -c 70000000 /dev/urandom > shop/huge.bin |
| 687 | 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" |
| 688 | rm -f shop/huge.bin |
| 689 | fi |
| 690 |
| 691 | # A new machine starts with recent history (here the last 3 versions) and |
| 692 | # fetches older versions when a command asks for one. |
| 693 | NEWBOX="$WORK/newbox" |
| 694 | nb() { ONSET_HOME="$NEWBOX" ONSET_NAME="Alice" ONSET_EMAIL="alice@acme.test" ONSET_HISTORY_KEPT=3 "$@"; } |
| 695 | nb "$VCS" login "$HOST" --token "$ALICE" >/dev/null |
| 696 | check "a new machine gets the project" nb "$VCS" get "$HOST/acme/shop" newshop |
| 697 | LATEST=$(nb "$VCS" --json -C newshop log -n 1 | python3 -c 'import json,sys; print(json.load(sys.stdin)["versions"][0]["n"])') |
| 698 | 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 |
| 699 | check "an old version when asked" sh -c "ONSET_HOME=$NEWBOX $VCS -C $WORK/newshop log 2 | grep -q 'main 2'" |
| 700 | 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 |
| 701 | check "diffs of old versions" nb "$VCS" -C newshop diff --from 1 --to 2 --stat |
| 702 | 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))'" |
| 703 |
| 704 | # on watch on a host: bob's land breaks a check; alice's watcher takes it back |
| 705 | # and bob's change is open again, for him. |
| 706 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 707 | printf 'test ! -e broken-by-bob\n' > shop/check.sh |
| 708 | as laptop "$VCS" -C shop land -m "A check" >/dev/null 2>&1 |
| 709 | WGOOD=$(main_n) |
| 710 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 711 | printf 'x\n' > bobshop/broken-by-bob |
| 712 | check "bob's land breaks the check" as bob "$VCS" -C bobshop land -m "Breaks the check" |
| 713 | WBROKE=$(main_n) |
| 714 | WCHANGE=$(as bob "$VCS" --json -C bobshop log "$WBROKE" | jq_ 'd["change"]') |
| 715 | check "alice's watch takes it back" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop watch --run 'sh check.sh' --from $WGOOD --once | grep -q 'main $WBROKE (\"Breaks the check\"'" |
| 716 | check "bob's change is open again for him" sh -c "ONSET_HOME=$WORK/bob $VCS --json -C $WORK/bobshop status $WCHANGE | grep -q '\"state\": *\"open\"'" |
| 717 | check "and main passes again" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sync >/dev/null 2>&1; test ! -e $WORK/bobshop/broken-by-bob" |
| 718 |
| 719 | # Checks: the host's versions and what main needs, and this machine's objects. |
| 720 | check "vcs check passes" as laptop "$VCS" -C shop check |
| 721 | 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" |
| 722 | # A file of main's, damaged on this machine (its last bytes gone, as a power cut can leave it). |
| 723 | README_ID=$( (printf '\001'; cat shop/README.md) | sha256sum | cut -c1-64) |
| 724 | OBJECT="$WORK/laptop/objects/${README_ID:0:2}/${README_ID:2}" |
| 725 | python3 -c 'import sys; p = sys.argv[1]; d = open(p, "rb").read(); open(p, "wb").write(d[:-4])' "$OBJECT" |
| 726 | check "damage on this machine is found and repaired from the host" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop check | grep -q 'repaired *1 of them'" |
| 727 | check "and it checks out after" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop check | grep -q 'each matching its ID'" |
| 728 | check "the file reads as it was" sh -c "test \"\$(ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop show README.md)\" = \"\$(cat $WORK/shop/README.md)\"" |
| 729 |
| 730 | [ "$fail" != 0 ] && [ -f "$WORK/server.log" ] && { echo "server log:"; tail -n 60 "$WORK/server.log"; } |
| 731 | echo "hosted: $pass passed, $fail failed" |
| 732 | [ "$fail" = 0 ] |
| 733 |