#!/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 vcs-server by default; with VCS_TARGET=worker, # against the hosted service (apps/vcs) under `wrangler dev`, in its test # sign-in mode. # Usage: [VCS_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=${VCS_TARGET:-server} # Windows (WINDOWS=1, or Git Bash there) has no executable bit on disk; vcs keeps it in the tree. case "${WINDOWS:-$(uname -s)}" in MINGW* | MSYS* | CYGWIN* | 1) WINDOWS=1 ;; *) WINDOWS= ;; esac BIN_EXE= [ -n "$WINDOWS" ] && [ -x "$BIN/vcs.exe" ] && BIN_EXE=.exe VCS=$BIN/vcs$BIN_EXE SERVER=$BIN/vcs-server$BIN_EXE WORK=$(mktemp -d) export VCS_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 VCS_SESSION_FILE VCS_SESSIONS 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/vcs 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/vcs.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/vcs.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 VCS_HOME. as() { # who, command... local who=$1 shift case $who in laptop) VCS_HOME="$WORK/laptop" VCS_NAME="Alice" VCS_EMAIL="alice@acme.test" "$@" ;; cloud) VCS_HOME="$WORK/cloud" VCS_NAME="Alice" VCS_EMAIL="alice@acme.test" "$@" ;; bob) VCS_HOME="$WORK/bob" VCS_NAME="Bob" VCS_EMAIL="bob@acme.test" "$@" ;; carol) VCS_HOME="$WORK/carol" VCS_NAME="Carol" VCS_EMAIL="carol@acme.test" "$@" ;; esac } start_server 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 "VCS_HOME=$WORK/browser VCS_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 "VCS_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 "VCS_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 "VCS_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 "VCS_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 "VCS_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 "VCS_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 "VCS_HOME=$WORK/bob $VCS -C $WORK/bobshop log -n 1 | grep -q 'a private change'" check "carol never gets billing" sh -c "VCS_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 "VCS_HOME=$WORK/bob $VCS -C $WORK/bobshop log -n 1 | grep -q 'Take back'" # 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 (VCS_HOME="$WORK/bob" VCS_NAME="Bob" VCS_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 "VCS_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 can delete" sh -c "$(declare -f edits); HOST=$HOST; edits $ALICE '{\"title\":\"Remove hello\",\"files\":{\"api/hello.txt\":null}}' | grep -q '\"outcome\":\"landed\"'" 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 "! VCS_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 "! VCS_HOME=$WORK/bob $VCS -C $WORK/bobshop show billing/rates.txt >/dev/null 2>&1" check "no blame of a private file" sh -c "! VCS_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 "VCS_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 "VCS_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 "VCS_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 "VCS_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 "VCS_HOME=$WORK/bob $VCS -C $WORK/bobshop approve $REVIEW | grep -q landed" check "main has it" sh -c "VCS_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 vcs_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\":\"vcs_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\":\"vcs_read\",\"arguments\":{\"project\":\"acme/shop\",\"path\":\"mcp/hi.txt\"}}}' | grep -q 'hi from mcp'" check "mcp lists a folder" 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\":\"vcs_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 VCS_HOME="$WORK/fresh$n" "$VCS" login "$HOST" --token "$token" >/dev/null 2>&1 VCS_HOME="$WORK/fresh$n" VCS_NAME=Fresh VCS_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 "VCS_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 "VCS_HOME=$WORK/laptop $VCS -C $WORK/shop log kept/a.md | grep -q 'from notes/a.txt'" # Signed lands (Upstream's host keeps keys): vcs key, then lands are signed # and vcs verify checks them; a project can take signed lands only. if [ "$TARGET" = worker ]; then check "vcs key" sh -c "VCS_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 "VCS_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 "! VCS_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 "! VCS_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 "VCS_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 "VCS_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 "VCS_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 "VCS_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 "VCS_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 "! VCS_HOME=$WORK/bob $VCS -C $WORK/bobshop purge leaked.pem --yes >/dev/null 2>&1" check "alice sees what it would do" sh -c "VCS_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 "VCS_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 "! VCS_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 && VCS_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 "VCS_HOME=$PURGED VCS_NAME=Alice VCS_EMAIL=alice@acme.test $VCS login $HOST --token $ALICE >/dev/null && cd $WORK && VCS_HOME=$PURGED VCS_NAME=Alice VCS_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 "VCS_HOME=$ENC VCS_NAME=Alice VCS_EMAIL=alice@acme.test $VCS login $HOST --token $ALICE >/dev/null && cd $WORK && VCS_HOME=$ENC VCS_NAME=Alice VCS_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() { VCS_HOME="$LZ" VCS_NAME="Alice" VCS_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 .vcs -prune -o -type f -print | grep -v '/\.env' | sort)" = "$(cd shop && find . -name .vcs -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 "VCS_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 "VCS_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 "VCS_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 "VCS_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 "VCS_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 "VCS_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 "VCS_HOME=$WORK/cloud $VCS -C $WORK/cloudshop sessions show $SC | grep -q 'agent: Adding it now.'" check "bob doesn't" sh -c "! VCS_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 "VCS_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) 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 "! VCS_HOME=$WORK/bob $VCS -C $WORK/bobshop sessions show $SC 2>/dev/null | grep -q 'Adding it now.'" # 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 "VCS_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() { VCS_HOME="$NEWBOX" VCS_NAME="Alice" VCS_EMAIL="alice@acme.test" VCS_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 "VCS_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 && VCS_HOME=$NEWBOX $VCS -C $WORK/newshop land -m 'From a new machine' | grep -q 'main $((LATEST + 1))'" # 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 "! VCS_HOME=$WORK/carol $VCS -C $WORK/carolshop check >/dev/null 2>&1" OBJECT=$(find "$WORK/laptop/objects" -type f | head -1) # Its last bytes gone, so it can't decode to what its ID says. 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 (exit 1)" code 1 as laptop "$VCS" -C shop check [ "$fail" != 0 ] && [ -f "$WORK/server.log" ] && { echo "server log:"; tail -n 60 "$WORK/server.log"; } echo "hosted: $pass passed, $fail failed" [ "$fail" = 0 ]