#!/usr/bin/env bash # End-to-end checks of hosted projects: two people, a reader, two machines, # takeovers, waiting changes, offline work and 32 agents landing at once. # Against the self-hosted onset-server by default; with ONSET_TARGET=worker, # against the hosted service (apps/onset) under `wrangler dev`, in its test # sign-in mode. # Usage: [ONSET_TARGET=worker] tests/hosted.sh [path/to/target/debug] set -uo pipefail ROOT=$(cd "$(dirname "$0")/.." && pwd) TESTS=$ROOT/tests BIN=${1:-$ROOT/target/debug} TARGET=${ONSET_TARGET:-server} # Windows (WINDOWS=1, or Git Bash there) has no executable bit on disk; Onset keeps it in the tree. case "${WINDOWS:-$(uname -s)}" in MINGW* | MSYS* | CYGWIN* | 1) WINDOWS=1 ;; *) WINDOWS= ;; esac BIN_EXE= [ -n "$WINDOWS" ] && [ -x "$BIN/on.exe" ] && BIN_EXE=.exe VCS=$BIN/on$BIN_EXE SERVER=$BIN/onset-server$BIN_EXE WORK=$(mktemp -d) export ONSET_SAVER=0 # Run from inside an agent, its own session would be kept with the tests' lands. unset CLAUDE_CODE_SESSION_ID CLAUDECODE CLAUDE_CONFIG_DIR ONSET_SESSION_FILE ONSET_SESSIONS VCS_SESSION_FILE VCS_SESSIONS VCS_HOME VCS_TOKEN export CODEX_HOME="$WORK/no-codex" PORT=$((20000 + RANDOM % 20000)) HOST="127.0.0.1:$PORT" SERVER_PID= stop_server() { [ -z "$SERVER_PID" ] && return if [ "$TARGET" = worker ]; then kill -- "-$SERVER_PID" 2>/dev/null; else kill "$SERVER_PID" 2>/dev/null; fi wait "$SERVER_PID" 2>/dev/null SERVER_PID= } cleanup() { stop_server # Lazy workspaces' mounts first. grep -o " $WORK/[^ ]*" /proc/self/mountinfo 2>/dev/null | while read -r m; do fusermount3 -u "$m" 2>/dev/null; done rm -rf "$WORK" } trap cleanup EXIT cd "$WORK" if [ "$TARGET" = worker ]; then APP=$ROOT/../apps/onset ALICE=vcs_test_alice_0123456789 BOB=vcs_test_bob_0123456789 CAROL=vcs_test_carol_0123456789 # Packs start plain; the restart for the offline checks turns encryption # on, and the packs from before are encrypted as the project's used. DATA_KEY=off 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\"}}" start_server() { # Its own process group, so stopping it stops workerd too, and its own # name, so a dev process left from another run never stands in for it. 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))" \ --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 & SERVER_PID=$! for _ in $(seq 300); do curl -s "http://$HOST/.well-known/onset.json" 2>/dev/null | grep -q api && return; sleep 0.2; done cat "$WORK/server.log"; echo "worker didn't start"; exit 1 } add_member() { # project, handle, role 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 } else start_server() { "$SERVER" serve --data "$WORK/server" --listen "$HOST" --threads 32 2>"$WORK/server.log" & SERVER_PID=$! for _ in $(seq 50); do curl -s "http://$HOST/.well-known/onset.json" >/dev/null && return; sleep 0.1; done echo "server didn't start"; exit 1 } add_member() { "$SERVER" member add "$1" "$2" --role "$3" --data "$WORK/server" } ALICE=$("$SERVER" user add alice --name Alice --email alice@acme.test --data "$WORK/server") BOB=$("$SERVER" user add bob --name Bob --email bob@acme.test --data "$WORK/server") CAROL=$("$SERVER" user add carol --name Carol --email carol@acme.test --data "$WORK/server") fi pass=0 fail=0 check() { local name=$1 shift if "$@" >/dev/null 2>&1; then pass=$((pass + 1)); else fail=$((fail + 1)); echo "FAIL: $name"; fi } code() { local want=$1 shift "$@" >/dev/null 2>&1 [ "$?" = "$want" ] } jq_() { python3 -c "import json,sys; d=json.load(sys.stdin); print($1)"; } # People and machines: each has a ONSET_HOME. as() { # who, command... local who=$1 shift case $who in laptop) ONSET_HOME="$WORK/laptop" ONSET_NAME="Alice" ONSET_EMAIL="alice@acme.test" "$@" ;; cloud) ONSET_HOME="$WORK/cloud" ONSET_NAME="Alice" ONSET_EMAIL="alice@acme.test" "$@" ;; bob) ONSET_HOME="$WORK/bob" ONSET_NAME="Bob" ONSET_EMAIL="bob@acme.test" "$@" ;; carol) ONSET_HOME="$WORK/carol" ONSET_NAME="Carol" ONSET_EMAIL="carol@acme.test" "$@" ;; esac } start_server 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" check "laptop login" as laptop "$VCS" login "$HOST" --token "$ALICE" check "cloud login" as cloud "$VCS" login "$HOST" --token "$ALICE" check "bob login" as bob "$VCS" login "$HOST" --token "$BOB" check "carol login" as carol "$VCS" login "$HOST" --token "$CAROL" # Signing in through the browser (a stand-in sign-in page approves it). LPORT=$((PORT + 3)) python3 "$TESTS/login-server.py" "$LPORT" "$ALICE" & LOGIN_PID=$! for _ in $(seq 100); do curl -s -o /dev/null -X POST "http://127.0.0.1:$LPORT/login" && break; sleep 0.1; done 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'" kill "$LOGIN_PID" 2>/dev/null check "no browser login without a sign-in page" sh -c "ONSET_HOME=$WORK/browser2 '$VCS' login '$HOST' 2>&1 | grep -q -- '--token'" # Alice starts locally and publishes. mkdir shop && cd shop printf 'line 1\nline 2\nline 3\n' > shared.txt printf 'SECRET=alice\n' > .env check "init" as laptop "$VCS" init check "first version" as laptop "$VCS" land -m "First version" printf 'line 1\nline 2\nline 3\nline 4\n' > shared.txt check "second version" as laptop "$VCS" land -m "Second version" check "publish" as laptop "$VCS" publish "$HOST/acme/shop" 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 add_member acme/shop bob member add_member acme/shop carol reader # Bob gets it. cd "$WORK" check "bob gets" as bob "$VCS" get "$HOST/acme/shop" bobshop check "bob has the files" test "$(tail -1 bobshop/shared.txt)" = "line 4" check "bob doesn't get alice's .env" test ! -e bobshop/.env # Parallel landings from two people. printf 'alice\n' > shop/alice.txt printf 'bob\n' > bobshop/bob.txt check "alice lands" as laptop "$VCS" -C shop land -m "Alice's file" check "bob lands without syncing" as bob "$VCS" -C bobshop land -m "Bob's file" check "main is 4" test "$(as bob "$VCS" --json -C bobshop log | jq_ 'd["versions"][0]["n"]')" = 4 # A conflict between them. as laptop "$VCS" -C shop sync >/dev/null 2>&1 as bob "$VCS" -C bobshop sync >/dev/null 2>&1 perl -pi -e 's/^line 2$/line two (alice)/' shop/shared.txt perl -pi -e 's/^line 2$/line two (bob)/' bobshop/shared.txt check "alice lands line 2" as laptop "$VCS" -C shop land -m "Alice's line 2" check "bob conflicts (exit 3)" code 3 as bob "$VCS" -C bobshop land -m "Bob's line 2" check "bob syncs (exit 3)" code 3 as bob "$VCS" -C bobshop sync check "markers" grep -q '<<<<<<< main' bobshop/shared.txt perl -ni -e 'print unless /^(<<<<<<< |=======$|>>>>>>> )/ or /line two \(alice\)/' bobshop/shared.txt check "bob lands after fixing" as bob "$VCS" -C bobshop land # A reader's change waits for someone who can land it. check "carol gets" as carol "$VCS" get "$HOST/acme/shop" carolshop printf 'fix from carol\n' > carolshop/carol.txt check "carol's land waits (exit 5)" code 5 as carol "$VCS" -C carolshop land -m "Carol's fix" 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]') check "alice sees the waiting change" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop log --changes | grep -q waiting" as laptop "$VCS" -C shop land "$CAROL_CHANGE" >"$WORK/carol-land.out" 2>&1; cat "$WORK/carol-land.out" >&2 check "alice lands carol's change" grep -q landed "$WORK/carol-land.out" check "carol is the author" test "$(as laptop "$VCS" --json -C shop log -n 1 | jq_ 'd["versions"][0]["author"].split(" <")[0]')" = Carol # Alice moves from her laptop to a cloud machine mid-change. as laptop "$VCS" -C shop sync >/dev/null 2>&1 printf 'started on the laptop\n' > shop/feature.txt check "describe on laptop" as laptop "$VCS" -C shop describe "Feature" FEATURE=$(as laptop "$VCS" --json -C shop status | jq_ 'd["change"]["id"]') check "cloud picks it up" as cloud "$VCS" get "$HOST/acme/shop" cloudshop --change "$FEATURE" check "the edit came along" test "$(cat cloudshop/feature.txt)" = "started on the laptop" check ".env came along" test "$(cat cloudshop/.env)" = "SECRET=alice" printf 'continued in the cloud\n' >> cloudshop/feature.txt check "cloud saves" as cloud "$VCS" -C cloudshop status printf 'late laptop edit\n' > shop/late.txt OUT=$(as laptop "$VCS" --json -C shop status 2>/dev/null) check "laptop's late edit is rescued into a new change" sh -c "echo '$OUT' | grep -q 'Rescued from'" check "cloud lands the feature" as cloud "$VCS" -C cloudshop land 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 # Sharing a change. printf 'draft\n' > bobshop/draft.txt check "bob describes" as bob "$VCS" -C bobshop describe "Draft for Alice" DRAFT=$(as bob "$VCS" --json -C bobshop status | jq_ 'd["change"]["id"]') check "bob shares it" as bob "$VCS" -C bobshop access "$DRAFT" @alice check "alice sees it" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop log --changes | grep -q 'Draft for Alice'" # Private paths: only Alice sees billing/. (First drop the rescued change, # which still has the takeover's conflict in it.) as laptop "$VCS" -C shop drop >/dev/null 2>&1 as laptop "$VCS" -C shop sync >/dev/null 2>&1 mkdir -p shop/billing && printf 'rate 10\n' > shop/billing/rates.txt check "alice lands billing" as laptop "$VCS" -C shop land -m "Billing rates" 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" check "alice makes billing/ hers" as laptop "$VCS" -C shop access billing @alice 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" printf 'bob was here\n' >> bobshop/README.md 2>/dev/null || printf 'bob was here\n' > bobshop/README.md check "bob lands outside billing" as bob "$VCS" -C bobshop land -m "Bob's readme" 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" mkdir -p bobshop/billing && printf 'sneaky\n' > bobshop/billing/new.txt check "bob can't write into billing (exit 7)" code 7 as bob "$VCS" -C bobshop land -m "Sneaky" rm -rf bobshop/billing as bob "$VCS" -C bobshop drop >/dev/null 2>&1 printf 'rate 12\n' > shop/billing/rates.txt check "alice changes billing" as laptop "$VCS" -C shop land -m "New billing rates" 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'" 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" 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" # Naming the full tree in a change of his own doesn't get bob in either. FULL=$(curl -s -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/shop" | jq_ 'd["main"]["tree"]') CLAIM="{\"change\":{\"id\":\"bcdfghjkmnpq\",\"base_tree\":\"$FULL\",\"head_tree\":\"$FULL\"},\"workspace\":\"w1\"}" 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" 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" # Undo a hosted land. as cloud "$VCS" -C cloudshop sync >/dev/null 2>&1 printf 'oops\n' > cloudshop/oops.txt check "cloud lands oops" as cloud "$VCS" -C cloudshop land -m "Oops" check "cloud undoes it" as cloud "$VCS" -C cloudshop undo check "main took it back" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop log -n 1 | grep -q 'Take back'" check "the undone change lands again" as cloud "$VCS" -C cloudshop land -m "Oops, kept" 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" # A land whose answer was lost (a dropped connection, a client that gave up): # landing again answers as the first time, and never lands it twice, even # after a later version took its edits back out. main_n() { curl -s -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/shop" | jq_ 'd["main"]["n"]'; } as cloud "$VCS" -C cloudshop sync >/dev/null 2>&1 printf 'retry\n' > cloudshop/retry.txt as cloud "$VCS" -C cloudshop describe "Retry me" >/dev/null 2>&1 rm -rf "$WORK/cloud-before" "$WORK/cloudshop-before" cp -a "$WORK/cloud" "$WORK/cloud-before" && cp -a cloudshop/.onset "$WORK/cloudshop-before" check "a land goes through" as cloud "$VCS" -C cloudshop land LANDED=$(main_n) as bob "$VCS" -C bobshop sync >/dev/null 2>&1 rm -f bobshop/retry.txt check "bob takes its file out again" as bob "$VCS" -C bobshop land -m "No retry file" rm -rf "$WORK/cloud" cloudshop/.onset && mv "$WORK/cloud-before" "$WORK/cloud" && mv "$WORK/cloudshop-before" cloudshop/.onset 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'" check "main didn't move" test "$(main_n)" = "$((LANDED + 1))" 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" as cloud "$VCS" -C cloudshop sync >/dev/null 2>&1 printf 'oops\n' > bobshop/oops-again.txt cp bobshop/oops-again.txt cloudshop/oops-again.txt check "bob lands a file" as bob "$VCS" -C bobshop land -m "Oops again" check "the same edits from someone else are on main already (exit 1)" code 1 as cloud "$VCS" -C cloudshop land -m "Same file" as cloud "$VCS" -C cloudshop drop >/dev/null 2>&1 # Offline: saves stay local, land says offline, later it all goes up. if [ "$TARGET" = worker ]; then # A pack written plain, before encryption is on. { printf 'PLAIN-MARKER-6c1e\n'; head -c 700000 /dev/urandom; } > shop/plain.bin as laptop "$VCS" -C shop land -m "Written plain" >/dev/null 2>&1 fi stop_server printf 'written offline\n' > cloudshop/offline.txt check "status works offline" as cloud "$VCS" -C cloudshop status check "land says offline (exit 6)" code 6 as cloud "$VCS" -C cloudshop land -m "Offline work" DATA_KEY=test-data-key-2026 start_server check "land works once back" as cloud "$VCS" -C cloudshop land -m "Offline work" # 32 agents land at once on one main, none refused. BEFORE=$(as bob "$VCS" --json -C bobshop log -n 1 | jq_ 'd["versions"][0]["n"]') for i in $(seq 1 32); do as bob "$VCS" get "$HOST/acme/shop" "agent$i" >/dev/null 2>&1 printf 'agent %s\n' "$i" > "agent$i/agent-$i.txt" done pids=() for i in $(seq 1 32); do (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") & pids+=($!) done wait "${pids[@]}" OK=$(grep -l '^0$' "$WORK"/land*.code | wc -l | tr -d " ") check "all 32 landed" test "$OK" = 32 for i in $(seq 1 32); do [ "$(cat "$WORK/land$i.code")" = 0 ] || { echo "agent $i:"; cat "$WORK/land$i.out"; } done AFTER=$(as bob "$VCS" --json -C bobshop log -n 1 | jq_ 'd["versions"][0]["n"]') check "main moved by 32" test "$((AFTER - BEFORE))" = 32 # An agent that landed in the middle still sees every version, in order. 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'" # Landing without a workspace: file edits in one call. edits() { # token, json curl -s -X POST -H "authorization: Bearer $1" -H 'content-type: application/json' -d "$2" "http://$HOST/v1/projects/acme/shop/edits" } 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\"'" as bob "$VCS" -C bobshop sync >/dev/null 2>&1 check "edits reach workspaces" test "$(cat bobshop/api/hello.txt 2>/dev/null)" = hello [ -z "$WINDOWS" ] && check "edits keep the executable bit" test -x bobshop/api/run.sh 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'" HELLO_AT=$(main_n) 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\"'" AFTER_HELLO=$(main_n) 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'" check "and land nothing" test "$(main_n)" = "$AFTER_HELLO" # Edits made on an older version merge with what landed since, file by file. printf 'one\ntwo\nthree\n' > bobshop/merge-me.txt as bob "$VCS" -C bobshop land -m "Merge me" >/dev/null 2>&1 READ_AT=$(main_n) as bob "$VCS" -C bobshop sync >/dev/null 2>&1 printf 'one\ntwo\nthree, by bob\n' > bobshop/merge-me.txt as bob "$VCS" -C bobshop land -m "Bob's third line" >/dev/null 2>&1 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\"'" 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'" 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\"'" check "bad paths refused in edits" sh -c "$(declare -f edits); HOST=$HOST; edits $ALICE '{\"title\":\"Escape\",\"files\":{\"../x\":\"x\"}}' | grep -q bad_usage" 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" # A nested git checkout lands as files, never its .git. 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 check "a nested git checkout lands" as bob "$VCS" -C bobshop land -m "Vendored lib" as laptop "$VCS" -C shop sync >/dev/null 2>&1 check "its files reach workspaces" test -f shop/vendor/lib/lib.txt check "its .git doesn't" test ! -e shop/vendor/lib/.git 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 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 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 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" 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" 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" 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" check "tag on the host" as laptop "$VCS" -C shop tag v1.0 2 check "bob sees the tag" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop tag | grep -q 'v1.0'" check "show at a tag on the host" test "$(as bob "$VCS" -C bobshop show shared.txt --from v1.0 | tail -1)" = "line 4" check "a reader can't tag (exit 7)" code 7 as carol "$VCS" -C carolshop tag nope 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\"'" # Groups: a folder for a group. as laptop "$VCS" -C shop sync >/dev/null 2>&1 check "alice makes a group" as laptop "$VCS" -C shop group ops @bob check "the group lists bob" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop group | grep -q '@ops.*@bob'" mkdir -p shop/ops && printf 'runbook\n' > shop/ops/runbook.txt check "alice lands ops/" as laptop "$VCS" -C shop land -m "Runbook" check "ops/ is the group's" as laptop "$VCS" -C shop access ops @ops 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" 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" check "only owners change groups (exit 7)" code 7 as bob "$VCS" -C bobshop group ops @bob @carol # Approvals: a land waits until someone else approves it. check "alice requires an approval" as laptop "$VCS" -C shop settings approvals=1 printf 'reviewed\n' > shop/reviewed.txt check "alice's land waits (exit 5)" code 5 as laptop "$VCS" -C shop land -m "Needs review" 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]') check "alice can't approve her own (exit 7)" code 7 as laptop "$VCS" -C shop approve "$REVIEW" check "bob approves, and it lands" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop approve $REVIEW | grep -q landed" 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" check "approvals off again" as laptop "$VCS" -C shop settings approvals=0 # MCP, for agents without a workspace (Upstream's host). if [ "$TARGET" = worker ]; then rpc() { curl -s -X POST -H "authorization: Bearer $ALICE" -H 'content-type: application/json' -d "$1" "http://$HOST/mcp"; } check "mcp initialize" sh -c "$(declare -f rpc); ALICE=$ALICE HOST=$HOST; rpc '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}' | grep -q protocolVersion" 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" 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" 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'" 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'" 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'" 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'" 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" fi # A bigger tree on a new machine: the hosted service sends it from a # snapshot once it has made one, with what's newer on top, and never sends # someone a snapshot of files they can't see. fresh() { # n, token, command... local n=$1 token=$2 shift 2 ONSET_HOME="$WORK/fresh$n" "$VCS" login "$HOST" --token "$token" >/dev/null 2>&1 ONSET_HOME="$WORK/fresh$n" ONSET_NAME=Fresh ONSET_EMAIL=fresh@acme.test "$@" } tree_of() { curl -s -H "authorization: Bearer $1" "http://$HOST/v1/projects/acme/shop" | jq_ 'd["main"]["tree"]'; } 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:'; } as laptop "$VCS" -C shop sync >/dev/null 2>&1 mkdir -p shop/many && for i in $(seq 1 300); do printf 'file %s\n' "$i" > "shop/many/f$i.txt"; done snap_get() { # n, token, folder: a new machine's copy has many/ as it is fresh "$1" "$2" "$VCS" get "$HOST/acme/shop" "$WORK/$3" >/dev/null 2>&1 && diff -r "$WORK/shop/many" "$WORK/$3/many" } check "land 300 files" as laptop "$VCS" -C shop land -m "Many files" check "a new machine gets them all" snap_get 1 "$ALICE" snap1 check "billing too, for alice" test -e snap1/billing/rates.txt check "billing's bytes are there to find" grep -rq 'rate 12' "$WORK/fresh1" if [ "$TARGET" = worker ]; then for _ in $(seq 50); do from_snapshot "$ALICE" && break; sleep 0.2; done check "the host made a snapshot" from_snapshot "$ALICE" 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:'" fi printf 'changed\n' > shop/many/f7.txt && printf 'new\n' > shop/many/new.txt && rm shop/many/f9.txt check "land changes past it" as laptop "$VCS" -C shop land -m "Past the snapshot" check "the next machine gets the snapshot and what's newer" snap_get 2 "$ALICE" snap2 check "billing too" test -e snap2/billing/rates.txt [ "$TARGET" = worker ] && check "that came from the snapshot" from_snapshot "$ALICE" check "bob's new machine gets it without billing" snap_get 3 "$BOB" snap3 check "no billing folder" test ! -e snap3/billing check "and none of billing's bytes" sh -c "! grep -rq 'rate 12' '$WORK/fresh3'" # Renames on the host: an edit follows a moved file when it lands, and log follows the move. as laptop "$VCS" -C shop sync >/dev/null 2>&1 mkdir -p shop/notes && for i in $(seq 1 30); do printf 'line %s\n' "$i"; done > shop/notes/a.txt check "land a file to move" as laptop "$VCS" -C shop land -m "Notes to move" check "a second workspace on it" as cloud "$VCS" get "$HOST/acme/shop" movetwo mkdir -p shop/kept && mv shop/notes/a.txt shop/kept/a.md check "land the move" as laptop "$VCS" -C shop land -m "Move them" perl -pi -e 's/^line 9$/line nine/' movetwo/notes/a.txt check "an edit to the old name lands" as cloud "$VCS" -C movetwo land -m "Edit before the move" 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" 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'" # Signed lands (Upstream's host keeps keys): on key, then lands are signed # and on verify checks them; a project can take signed lands only. if [ "$TARGET" = worker ]; then check "vcs key" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop key | grep -q 'to @alice'" printf 'signed\n' > shop/signed.txt check "a signed land" as laptop "$VCS" -C shop land -m "Signed work" check "verify says who signed it" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop verify | grep -q 'by @alice'" check "an older unsigned version says so (exit 1)" code 1 as laptop "$VCS" -C shop verify 1 check "signed lands only" as laptop "$VCS" -C shop settings signed=required as bob "$VCS" -C bobshop sync >/dev/null 2>&1 printf 'unsigned\n' > bobshop/unsigned.txt check "bob's unsigned land is refused" sh -c "! ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop land -m Unsigned >/dev/null 2>&1" check "back to optional" as laptop "$VCS" -C shop settings signed=optional check "and bob's lands" as bob "$VCS" -C bobshop land -m "Unsigned" fi # Release lines on the host: alice starts 1.x, bob gets a workspace on it and # lands a fix there, and a fix that landed on main comes over. as laptop "$VCS" -C shop sync >/dev/null 2>&1 check "alice starts line 1.x" as laptop "$VCS" -C shop line 1.x check "a reader can't start one" sh -c "! ONSET_HOME=$WORK/carol $VCS -C $WORK/carolshop line 2.x >/dev/null 2>&1" check "bob gets a workspace on it" as bob "$VCS" get "$HOST/acme/shop" bob1x --line 1.x printf 'fix on 1.x\n' > bob1x/line-fix.txt 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'" 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'" 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" printf 'main fix\n' > shop/main-fix.txt as laptop "$VCS" -C shop land -m "Main fix" >/dev/null 2>&1 MAINFIX=$(as laptop "$VCS" --json -C shop log -n 1 | jq_ 'd["versions"][0]["change"]') 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'" 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" check "and nothing private" test ! -e bob1x/billing # Purge: alice (an owner) takes a key (small, kept beside the index) and a # big file (in a pack, which is written again without it) out of the # history; bob's machine takes them out when it next syncs; bob (a member, # not an owner) can't. printf 'not-a-real-key-hosted\n' > shop/leaked.pem as laptop "$VCS" -C shop land -m "Oops" --allow-secrets >/dev/null 2>&1 head -c 900000 /dev/urandom > shop/big.bin BIG=$( (printf '\001'; cat shop/big.bin) | sha256sum | cut -c1-64) as laptop "$VCS" -C shop land -m "A big file" >/dev/null 2>&1 as bob "$VCS" -C bobshop sync >/dev/null 2>&1 check "bob has the key" test -f bobshop/leaked.pem check "bob can't purge" sh -c "! ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop purge leaked.pem --yes >/dev/null 2>&1" check "alice sees what it would do" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop purge leaked.pem | grep -q would" check "alice purges them" as laptop "$VCS" -C shop purge leaked.pem big.bin --yes check "gone from alice's workspace" test ! -e shop/leaked.pem -a ! -e shop/big.bin LEAKED=$(printf '\001not-a-real-key-hosted\n' | sha256sum | cut -c1-64) host_lacks() { # object id (hex): the host says it's missing (deleting it runs just after the purge) for _ in $(seq 50); do [ "$(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 sleep 0.2 done return 1 } check "the host deleted the key" host_lacks "$LEAKED" check "and the big file" host_lacks "$BIG" 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" check "no version has it" sh -c "! ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop show leaked.pem --from main >/dev/null 2>&1" 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" PURGED="$WORK/purged-box" 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" check "the host's copy checks out" as laptop "$VCS" -C shop check # Encrypted at rest (Upstream's host): packs written before encryption was # on are written again encrypted, and a new pack has no plain bytes in R2. if [ "$TARGET" = worker ]; then unencrypted() { curl -s -X POST -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/shop/check" | jq_ 'd["unencrypted"]'; } all_encrypted() { for _ in $(seq 100); do as laptop "$VCS" -C shop sync >/dev/null 2>&1 [ "$(unencrypted)" = 0 ] && return 0 sleep 0.3 done return 1 } check "the old packs are encrypted as the project's used" all_encrypted { printf 'SEALED-MARKER-9d2b\n'; head -c 700000 /dev/urandom; } > shop/sealed.bin check "a land after it" as laptop "$VCS" -C shop land -m "Written encrypted" check "R2 has the plain pack's bytes (kept for rollbacks)" grep -rqa 'PLAIN-MARKER-6c1e' "$WORK/wrangler" check "but not the new pack's" sh -c "! grep -rqa 'SEALED-MARKER-9d2b' '$WORK/wrangler'" ENC="$WORK/encrypted-box" 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" check "and the host's copy checks out" as laptop "$VCS" -C shop check fi # Lazy workspaces (Linux, FUSE): every file shows at once at its size, and # each comes from the host the first time it's opened; work lands as from # any workspace, and the mount comes back with the next command. if [ "$(uname -s)" = Linux ] && [ -e /dev/fuse ] && command -v fusermount3 >/dev/null; then LZ="$WORK/lazy-box" lz() { ONSET_HOME="$LZ" ONSET_NAME="Alice" ONSET_EMAIL="alice@acme.test" "$@"; } lz "$VCS" login "$HOST" --token "$ALICE" >/dev/null as laptop "$VCS" -C shop sync >/dev/null 2>&1 check "get a lazy workspace" lz "$VCS" get "$HOST/acme/shop" lazyshop --lazy 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)" check "at their sizes" test "$(stat -c %s lazyshop/shared.txt)" = "$(stat -c %s shop/shared.txt)" FETCHED=$(find "$LZ/objects" -type f | wc -l) check "a file comes when it's opened" cmp -s lazyshop/shared.txt shop/shared.txt check "and only that file" test "$(find "$LZ/objects" -type f | wc -l)" = "$((FETCHED + 1))" check "nothing looks changed" sh -c "ONSET_HOME=$LZ $VCS -C $WORK/lazyshop status | grep -q 'no changes yet'" printf 'from a lazy workspace\n' >> lazyshop/shared.txt mkdir -p lazyshop/lazy && printf 'new\n' > lazyshop/lazy/new.txt mv lazyshop/feature.txt lazyshop/feature-moved.txt check "status shows the work" sh -c "ONSET_HOME=$LZ $VCS -C $WORK/lazyshop status | grep -q 'R feature.txt -> feature-moved.txt'" check "it lands" lz "$VCS" -C lazyshop land -m "From a lazy workspace" 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" printf 'from the laptop\n' > shop/to-lazy.txt as laptop "$VCS" -C shop land -m "For the lazy one" >/dev/null 2>&1 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" check "the mount stops" lz "$VCS" mount lazyshop --stop check "leaving an empty folder" test -z "$(ls lazyshop)" 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" lz "$VCS" mount lazyshop --stop >/dev/null 2>&1 fi # Agent sessions on the host: kept with the change, seen by its owner on any # of their machines and by people the change is shared with, and deleted from # the host when its owner deletes it. mkdir -p "$WORK/claude-a/projects/-x" HSES="$WORK/claude-a/projects/-x/host-1.jsonl" printf '%s\n' '{"type":"user","message":{"role":"user","content":"add the session file"}}' '{"type":"assistant","message":{"content":[{"type":"text","text":"Adding it now."}]}}' > "$HSES" as laptop "$VCS" -C shop sync >/dev/null 2>&1 printf 'with a session\n' > shop/session.txt 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'" SC=$(as laptop "$VCS" --json -C shop log -n 1 | jq_ 'd["versions"][0]["change"]') 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.'" 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.'" check "alice shares the change with bob" as laptop "$VCS" -C shop access "$SC" @bob check "then bob does" sh -c "ONSET_HOME=$WORK/bob $VCS -C $WORK/bobshop sessions show $SC | grep -q 'Adding it now.'" SESSION_TEXT=$( (printf '\001'; cat "$HSES") | sha256sum | cut -c1-64) # Upstream's host keeps machine keys, so it only ever had the sealed text. [ "$TARGET" = worker ] && check "the host never had its text (sealed)" host_lacks "$SESSION_TEXT" check "alice deletes it" as laptop "$VCS" -C shop sessions delete "$SC" check "the host deleted its text" host_lacks "$SESSION_TEXT" 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.'" # A project imported from a SHA-256 git repository publishes like any other; # its git view (SHA-1) gets new commits, and says so. git init -q --object-format=sha256 "$WORK/sha256src" (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") check "import a SHA-256 repository" as laptop "$VCS" import "$WORK/sha256src" "$WORK/sha256proj" 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'" check "it's on the host" test "$(curl -s -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/sha256" | jq_ 'd["main"]["n"]')" = 1 # Sealed folders: encrypted on the machines of the people they're for. The # host keeps them and can't read them; a machine that lacks the key is # brought in by the next reader's machine, and merges happen on a reader's. if [ "$TARGET" = worker ]; then as laptop "$VCS" -C shop sync >/dev/null 2>&1 as bob "$VCS" -C bobshop sync >/dev/null 2>&1 # Bob's machine put its key on his account when it got the project, so it's in from the start. 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]'" mkdir -p shop/vault && printf 'payroll: 4100\n\nrates\n\nbonus: 0\n' > shop/vault/pay.txt check "it lands" as laptop "$VCS" -C shop land -m "Payroll" check "alice reads it" grep -q 'payroll: 4100' shop/vault/pay.txt PLAIN=$( (printf '\001'; cat shop/vault/pay.txt) | sha256sum | cut -c1-64) check "the host never had it plain" host_lacks "$PLAIN" 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" 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" 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" # A machine of bob's that's new since waits for the key, and a reader's next command brings it in. ONSET_HOME="$WORK/bob2" "$VCS" login "$HOST" --token "$BOB" >/dev/null 2>&1 ONSET_HOME="$WORK/bob2" ONSET_NAME="Bob" ONSET_EMAIL="bob@acme.test" "$VCS" get "$HOST/acme/shop" bob2shop >/dev/null 2>&1 check "bob's new machine has no key yet" test ! -e bob2shop/vault/pay.txt check "and waits for one" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop access vault | grep -q 'waiting'" as laptop "$VCS" -C shop sync >/dev/null 2>&1 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]'" 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" 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" # Both change it: the host can't merge it, bob's machine does. printf 'payroll: 4100\n\nrates\n\nbonus: 300\n' > shop/vault/pay.txt as laptop "$VCS" -C shop land -m "Bonus" >/dev/null 2>&1 printf 'payroll: 4500\n\nrates\n\nbonus: 0\n' > bobshop/vault/pay.txt 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" check "and lands" as bob "$VCS" -C bobshop land -m "Raise" 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" # Alice's other machine is brought in the same way. as cloud "$VCS" -C cloudshop sync >/dev/null 2>&1 as bob "$VCS" -C bobshop sync >/dev/null 2>&1 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" check "vcs key shows a machine's fingerprint" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop key | grep -q 'machine'" # Unsealed: plain again from the next land. check "alice unseals it" as laptop "$VCS" -C shop access vault @bob check "a land keeps it plain" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop land -m Unseal 2>&1 | grep -q main" 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" fi # A project has a storage limit (60 MB in this test; 100 GiB for real). if [ "$TARGET" = worker ]; then head -c 70000000 /dev/urandom > shop/huge.bin 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" rm -f shop/huge.bin fi # A new machine starts with recent history (here the last 3 versions) and # fetches older versions when a command asks for one. NEWBOX="$WORK/newbox" nb() { ONSET_HOME="$NEWBOX" ONSET_NAME="Alice" ONSET_EMAIL="alice@acme.test" ONSET_HISTORY_KEPT=3 "$@"; } nb "$VCS" login "$HOST" --token "$ALICE" >/dev/null check "a new machine gets the project" nb "$VCS" get "$HOST/acme/shop" newshop LATEST=$(nb "$VCS" --json -C newshop log -n 1 | python3 -c 'import json,sys; print(json.load(sys.stdin)["versions"][0]["n"])') 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 check "an old version when asked" sh -c "ONSET_HOME=$NEWBOX $VCS -C $WORK/newshop log 2 | grep -q 'main 2'" 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 check "diffs of old versions" nb "$VCS" -C newshop diff --from 1 --to 2 --stat 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))'" # on watch on a host: bob's land breaks a check; alice's watcher takes it back # and bob's change is open again, for him. as laptop "$VCS" -C shop sync >/dev/null 2>&1 printf 'test ! -e broken-by-bob\n' > shop/check.sh as laptop "$VCS" -C shop land -m "A check" >/dev/null 2>&1 WGOOD=$(main_n) as bob "$VCS" -C bobshop sync >/dev/null 2>&1 printf 'x\n' > bobshop/broken-by-bob check "bob's land breaks the check" as bob "$VCS" -C bobshop land -m "Breaks the check" WBROKE=$(main_n) WCHANGE=$(as bob "$VCS" --json -C bobshop log "$WBROKE" | jq_ 'd["change"]') 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\"'" 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\"'" 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" # Checks: the host's versions and what main needs, and this machine's objects. check "vcs check passes" as laptop "$VCS" -C shop check 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" # A file of main's, damaged on this machine (its last bytes gone, as a power cut can leave it). README_ID=$( (printf '\001'; cat shop/README.md) | sha256sum | cut -c1-64) OBJECT="$WORK/laptop/objects/${README_ID:0:2}/${README_ID:2}" python3 -c 'import sys; p = sys.argv[1]; d = open(p, "rb").read(); open(p, "wb").write(d[:-4])' "$OBJECT" 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'" check "and it checks out after" sh -c "ONSET_HOME=$WORK/laptop $VCS -C $WORK/shop check | grep -q 'each matching its ID'" 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)\"" [ "$fail" != 0 ] && [ -f "$WORK/server.log" ] && { echo "server log:"; tail -n 60 "$WORK/server.log"; } echo "hosted: $pass passed, $fail failed" [ "$fail" = 0 ]