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 vcs-server by default; with VCS_TARGET=worker, |
| 5 | # against the hosted service (apps/vcs) under `wrangler dev`, in its test |
| 6 | # sign-in mode. |
| 7 | # Usage: [VCS_TARGET=worker] tests/hosted.sh [path/to/target/debug] |
| 8 | set -uo pipefail |
| 9 |
| 10 | ROOT=$(cd "$(dirname "$0")/.." && pwd) |
| 11 | TESTS=$ROOT/tests |
| 12 | BIN=${1:-$ROOT/target/debug} |
| 13 | TARGET=${VCS_TARGET:-server} |
| 14 | # Windows (WINDOWS=1, or Git Bash there) has no executable bit on disk; vcs keeps it in the tree. |
| 15 | case "${WINDOWS:-$(uname -s)}" in MINGW* | MSYS* | CYGWIN* | 1) WINDOWS=1 ;; *) WINDOWS= ;; esac |
| 16 | BIN_EXE= |
| 17 | [ -n "$WINDOWS" ] && [ -x "$BIN/vcs.exe" ] && BIN_EXE=.exe |
| 18 | VCS=$BIN/vcs$BIN_EXE |
| 19 | SERVER=$BIN/vcs-server$BIN_EXE |
| 20 | WORK=$(mktemp -d) |
| 21 | export VCS_SAVER=0 |
| 22 | PORT=$((20000 + RANDOM % 20000)) |
| 23 | HOST="127.0.0.1:$PORT" |
| 24 | SERVER_PID= |
| 25 | stop_server() { |
| 26 | [ -z "$SERVER_PID" ] && return |
| 27 | if [ "$TARGET" = worker ]; then kill -- "-$SERVER_PID" 2>/dev/null; else kill "$SERVER_PID" 2>/dev/null; fi |
| 28 | wait "$SERVER_PID" 2>/dev/null |
| 29 | SERVER_PID= |
| 30 | } |
| 31 | cleanup() { |
| 32 | stop_server |
| 33 | rm -rf "$WORK" |
| 34 | } |
| 35 | trap cleanup EXIT |
| 36 | cd "$WORK" |
| 37 |
| 38 | if [ "$TARGET" = worker ]; then |
| 39 | APP=$ROOT/../apps/vcs |
| 40 | ALICE=vcs_test_alice_0123456789 |
| 41 | BOB=vcs_test_bob_0123456789 |
| 42 | CAROL=vcs_test_carol_0123456789 |
| 43 | 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\"}}" |
| 44 | start_server() { |
| 45 | # Its own process group, so stopping it stops workerd too, and its own |
| 46 | # name, so a dev process left from another run never stands in for it. |
| 47 | 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))" \ |
| 48 | --persist-to "$WORK/wrangler" --var AUTH_MODE:test --var "TEST_USERS:$USERS" --var COMPACT_AT:4096 --var MAX_PROJECT_BYTES:60000000 >"$WORK/server.log" 2>&1 & |
| 49 | SERVER_PID=$! |
| 50 | for _ in $(seq 300); do curl -s "http://$HOST/.well-known/vcs.json" 2>/dev/null | grep -q api && return; sleep 0.2; done |
| 51 | cat "$WORK/server.log"; echo "worker didn't start"; exit 1 |
| 52 | } |
| 53 | add_member() { # project, handle, role |
| 54 | 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 |
| 55 | } |
| 56 | else |
| 57 | start_server() { |
| 58 | "$SERVER" serve --data "$WORK/server" --listen "$HOST" --threads 32 2>"$WORK/server.log" & |
| 59 | SERVER_PID=$! |
| 60 | for _ in $(seq 50); do curl -s "http://$HOST/.well-known/vcs.json" >/dev/null && return; sleep 0.1; done |
| 61 | echo "server didn't start"; exit 1 |
| 62 | } |
| 63 | add_member() { |
| 64 | "$SERVER" member add "$1" "$2" --role "$3" --data "$WORK/server" |
| 65 | } |
| 66 | ALICE=$("$SERVER" user add alice --name Alice --email alice@acme.test --data "$WORK/server") |
| 67 | BOB=$("$SERVER" user add bob --name Bob --email bob@acme.test --data "$WORK/server") |
| 68 | CAROL=$("$SERVER" user add carol --name Carol --email carol@acme.test --data "$WORK/server") |
| 69 | fi |
| 70 |
| 71 | pass=0 |
| 72 | fail=0 |
| 73 | check() { |
| 74 | local name=$1 |
| 75 | shift |
| 76 | if "$@" >/dev/null 2>&1; then pass=$((pass + 1)); else fail=$((fail + 1)); echo "FAIL: $name"; fi |
| 77 | } |
| 78 | code() { |
| 79 | local want=$1 |
| 80 | shift |
| 81 | "$@" >/dev/null 2>&1 |
| 82 | [ "$?" = "$want" ] |
| 83 | } |
| 84 | jq_() { python3 -c "import json,sys; d=json.load(sys.stdin); print($1)"; } |
| 85 |
| 86 | # People and machines: each has a VCS_HOME. |
| 87 | as() { # who, command... |
| 88 | local who=$1 |
| 89 | shift |
| 90 | case $who in |
| 91 | laptop) VCS_HOME="$WORK/laptop" VCS_NAME="Alice" VCS_EMAIL="alice@acme.test" "$@" ;; |
| 92 | cloud) VCS_HOME="$WORK/cloud" VCS_NAME="Alice" VCS_EMAIL="alice@acme.test" "$@" ;; |
| 93 | bob) VCS_HOME="$WORK/bob" VCS_NAME="Bob" VCS_EMAIL="bob@acme.test" "$@" ;; |
| 94 | carol) VCS_HOME="$WORK/carol" VCS_NAME="Carol" VCS_EMAIL="carol@acme.test" "$@" ;; |
| 95 | esac |
| 96 | } |
| 97 |
| 98 | start_server |
| 99 |
| 100 | check "laptop login" as laptop "$VCS" login "$HOST" --token "$ALICE" |
| 101 | check "cloud login" as cloud "$VCS" login "$HOST" --token "$ALICE" |
| 102 | check "bob login" as bob "$VCS" login "$HOST" --token "$BOB" |
| 103 | check "carol login" as carol "$VCS" login "$HOST" --token "$CAROL" |
| 104 |
| 105 | # Signing in through the browser (a stand-in sign-in page approves it). |
| 106 | LPORT=$((PORT + 3)) |
| 107 | python3 "$TESTS/login-server.py" "$LPORT" "$ALICE" & |
| 108 | LOGIN_PID=$! |
| 109 | for _ in $(seq 100); do curl -s -o /dev/null -X POST "http://127.0.0.1:$LPORT/login" && break; sleep 0.1; done |
| 110 | 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'" |
| 111 | kill "$LOGIN_PID" 2>/dev/null |
| 112 | check "no browser login without a sign-in page" sh -c "VCS_HOME=$WORK/browser2 '$VCS' login '$HOST' 2>&1 | grep -q -- '--token'" |
| 113 |
| 114 | # Alice starts locally and publishes. |
| 115 | mkdir shop && cd shop |
| 116 | printf 'line 1\nline 2\nline 3\n' > shared.txt |
| 117 | printf 'SECRET=alice\n' > .env |
| 118 | check "init" as laptop "$VCS" init |
| 119 | check "first version" as laptop "$VCS" land -m "First version" |
| 120 | printf 'line 1\nline 2\nline 3\nline 4\n' > shared.txt |
| 121 | check "second version" as laptop "$VCS" land -m "Second version" |
| 122 | check "publish" as laptop "$VCS" publish "$HOST/acme/shop" |
| 123 | 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 |
| 124 | add_member acme/shop bob member |
| 125 | add_member acme/shop carol reader |
| 126 |
| 127 | # Bob gets it. |
| 128 | cd "$WORK" |
| 129 | check "bob gets" as bob "$VCS" get "$HOST/acme/shop" bobshop |
| 130 | check "bob has the files" test "$(tail -1 bobshop/shared.txt)" = "line 4" |
| 131 | check "bob doesn't get alice's .env" test ! -e bobshop/.env |
| 132 |
| 133 | # Parallel landings from two people. |
| 134 | printf 'alice\n' > shop/alice.txt |
| 135 | printf 'bob\n' > bobshop/bob.txt |
| 136 | check "alice lands" as laptop "$VCS" -C shop land -m "Alice's file" |
| 137 | check "bob lands without syncing" as bob "$VCS" -C bobshop land -m "Bob's file" |
| 138 | check "main is 4" test "$(as bob "$VCS" --json -C bobshop log | jq_ 'd["versions"][0]["n"]')" = 4 |
| 139 |
| 140 | # A conflict between them. |
| 141 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 142 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 143 | perl -pi -e 's/^line 2$/line two (alice)/' shop/shared.txt |
| 144 | perl -pi -e 's/^line 2$/line two (bob)/' bobshop/shared.txt |
| 145 | check "alice lands line 2" as laptop "$VCS" -C shop land -m "Alice's line 2" |
| 146 | check "bob conflicts (exit 3)" code 3 as bob "$VCS" -C bobshop land -m "Bob's line 2" |
| 147 | check "bob syncs (exit 3)" code 3 as bob "$VCS" -C bobshop sync |
| 148 | check "markers" grep -q '<<<<<<< main' bobshop/shared.txt |
| 149 | perl -ni -e 'print unless /^(<<<<<<< |=======$|>>>>>>> )/ or /line two \(alice\)/' bobshop/shared.txt |
| 150 | check "bob lands after fixing" as bob "$VCS" -C bobshop land |
| 151 |
| 152 | # A reader's change waits for someone who can land it. |
| 153 | check "carol gets" as carol "$VCS" get "$HOST/acme/shop" carolshop |
| 154 | printf 'fix from carol\n' > carolshop/carol.txt |
| 155 | check "carol's land waits (exit 5)" code 5 as carol "$VCS" -C carolshop land -m "Carol's fix" |
| 156 | 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]') |
| 157 | check "alice sees the waiting change" sh -c "VCS_HOME=$WORK/laptop $VCS -C $WORK/shop log --changes | grep -q waiting" |
| 158 | as laptop "$VCS" -C shop land "$CAROL_CHANGE" >"$WORK/carol-land.out" 2>&1; cat "$WORK/carol-land.out" >&2 |
| 159 | check "alice lands carol's change" grep -q landed "$WORK/carol-land.out" |
| 160 | check "carol is the author" test "$(as laptop "$VCS" --json -C shop log -n 1 | jq_ 'd["versions"][0]["author"].split(" <")[0]')" = Carol |
| 161 |
| 162 | # Alice moves from her laptop to a cloud machine mid-change. |
| 163 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 164 | printf 'started on the laptop\n' > shop/feature.txt |
| 165 | check "describe on laptop" as laptop "$VCS" -C shop describe "Feature" |
| 166 | FEATURE=$(as laptop "$VCS" --json -C shop status | jq_ 'd["change"]["id"]') |
| 167 | check "cloud picks it up" as cloud "$VCS" get "$HOST/acme/shop" cloudshop --change "$FEATURE" |
| 168 | check "the edit came along" test "$(cat cloudshop/feature.txt)" = "started on the laptop" |
| 169 | check ".env came along" test "$(cat cloudshop/.env)" = "SECRET=alice" |
| 170 | printf 'continued in the cloud\n' >> cloudshop/feature.txt |
| 171 | check "cloud saves" as cloud "$VCS" -C cloudshop status |
| 172 | printf 'late laptop edit\n' > shop/late.txt |
| 173 | OUT=$(as laptop "$VCS" --json -C shop status 2>/dev/null) |
| 174 | check "laptop's late edit is rescued into a new change" sh -c "echo '$OUT' | grep -q 'Rescued from'" |
| 175 | check "cloud lands the feature" as cloud "$VCS" -C cloudshop land |
| 176 | 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 |
| 177 |
| 178 | # Sharing a change. |
| 179 | printf 'draft\n' > bobshop/draft.txt |
| 180 | check "bob describes" as bob "$VCS" -C bobshop describe "Draft for Alice" |
| 181 | DRAFT=$(as bob "$VCS" --json -C bobshop status | jq_ 'd["change"]["id"]') |
| 182 | check "bob shares it" as bob "$VCS" -C bobshop access "$DRAFT" @alice |
| 183 | check "alice sees it" sh -c "VCS_HOME=$WORK/laptop $VCS -C $WORK/shop log --changes | grep -q 'Draft for Alice'" |
| 184 |
| 185 | # Private paths: only Alice sees billing/. (First drop the rescued change, |
| 186 | # which still has the takeover's conflict in it.) |
| 187 | as laptop "$VCS" -C shop drop >/dev/null 2>&1 |
| 188 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 189 | mkdir -p shop/billing && printf 'rate 10\n' > shop/billing/rates.txt |
| 190 | check "alice lands billing" as laptop "$VCS" -C shop land -m "Billing rates" |
| 191 | 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" |
| 192 | check "alice makes billing/ hers" as laptop "$VCS" -C shop access billing @alice |
| 193 | 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" |
| 194 | printf 'bob was here\n' >> bobshop/README.md 2>/dev/null || printf 'bob was here\n' > bobshop/README.md |
| 195 | check "bob lands outside billing" as bob "$VCS" -C bobshop land -m "Bob's readme" |
| 196 | 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" |
| 197 | mkdir -p bobshop/billing && printf 'sneaky\n' > bobshop/billing/new.txt |
| 198 | check "bob can't write into billing (exit 7)" code 7 as bob "$VCS" -C bobshop land -m "Sneaky" |
| 199 | rm -rf bobshop/billing |
| 200 | as bob "$VCS" -C bobshop drop >/dev/null 2>&1 |
| 201 | printf 'rate 12\n' > shop/billing/rates.txt |
| 202 | check "alice changes billing" as laptop "$VCS" -C shop land -m "New billing rates" |
| 203 | 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'" |
| 204 | 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" |
| 205 | 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" |
| 206 | # Naming the full tree in a change of his own doesn't get bob in either. |
| 207 | FULL=$(curl -s -H "authorization: Bearer $ALICE" "http://$HOST/v1/projects/acme/shop" | jq_ 'd["main"]["tree"]') |
| 208 | CLAIM="{\"change\":{\"id\":\"bcdfghjkmnpq\",\"base_tree\":\"$FULL\",\"head_tree\":\"$FULL\"},\"workspace\":\"w1\"}" |
| 209 | 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" |
| 210 | 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" |
| 211 |
| 212 | # Undo a hosted land. |
| 213 | as cloud "$VCS" -C cloudshop sync >/dev/null 2>&1 |
| 214 | printf 'oops\n' > cloudshop/oops.txt |
| 215 | check "cloud lands oops" as cloud "$VCS" -C cloudshop land -m "Oops" |
| 216 | check "cloud undoes it" as cloud "$VCS" -C cloudshop undo |
| 217 | check "main took it back" sh -c "VCS_HOME=$WORK/bob $VCS -C $WORK/bobshop log -n 1 | grep -q 'Take back'" |
| 218 |
| 219 | # Offline: saves stay local, land says offline, later it all goes up. |
| 220 | stop_server |
| 221 | printf 'written offline\n' > cloudshop/offline.txt |
| 222 | check "status works offline" as cloud "$VCS" -C cloudshop status |
| 223 | check "land says offline (exit 6)" code 6 as cloud "$VCS" -C cloudshop land -m "Offline work" |
| 224 | start_server |
| 225 | check "land works once back" as cloud "$VCS" -C cloudshop land -m "Offline work" |
| 226 |
| 227 | # 32 agents land at once on one main, none refused. |
| 228 | BEFORE=$(as bob "$VCS" --json -C bobshop log -n 1 | jq_ 'd["versions"][0]["n"]') |
| 229 | for i in $(seq 1 32); do |
| 230 | as bob "$VCS" get "$HOST/acme/shop" "agent$i" >/dev/null 2>&1 |
| 231 | printf 'agent %s\n' "$i" > "agent$i/agent-$i.txt" |
| 232 | done |
| 233 | pids=() |
| 234 | for i in $(seq 1 32); do |
| 235 | (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") & |
| 236 | pids+=($!) |
| 237 | done |
| 238 | wait "${pids[@]}" |
| 239 | OK=$(grep -l '^0$' "$WORK"/land*.code | wc -l | tr -d " ") |
| 240 | check "all 32 landed" test "$OK" = 32 |
| 241 | for i in $(seq 1 32); do |
| 242 | [ "$(cat "$WORK/land$i.code")" = 0 ] || { echo "agent $i:"; cat "$WORK/land$i.out"; } |
| 243 | done |
| 244 | AFTER=$(as bob "$VCS" --json -C bobshop log -n 1 | jq_ 'd["versions"][0]["n"]') |
| 245 | check "main moved by 32" test "$((AFTER - BEFORE))" = 32 |
| 246 | # An agent that landed in the middle still sees every version, in order. |
| 247 | 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'" |
| 248 |
| 249 | # Landing without a workspace: file edits in one call. |
| 250 | edits() { # token, json |
| 251 | curl -s -X POST -H "authorization: Bearer $1" -H 'content-type: application/json' -d "$2" "http://$HOST/v1/projects/acme/shop/edits" |
| 252 | } |
| 253 | 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\"'" |
| 254 | as bob "$VCS" -C bobshop sync >/dev/null 2>&1 |
| 255 | check "edits reach workspaces" test "$(cat bobshop/api/hello.txt 2>/dev/null)" = hello |
| 256 | [ -z "$WINDOWS" ] && check "edits keep the executable bit" test -x bobshop/api/run.sh |
| 257 | 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\"'" |
| 258 | 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\"'" |
| 259 | check "bad paths refused in edits" sh -c "$(declare -f edits); HOST=$HOST; edits $ALICE '{\"title\":\"Escape\",\"files\":{\"../x\":\"x\"}}' | grep -q bad_usage" |
| 260 | 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" |
| 261 | # A nested git checkout lands as files, never its .git. |
| 262 | 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 |
| 263 | check "a nested git checkout lands" as bob "$VCS" -C bobshop land -m "Vendored lib" |
| 264 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 265 | check "its files reach workspaces" test -f shop/vendor/lib/lib.txt |
| 266 | check "its .git doesn't" test ! -e shop/vendor/lib/.git |
| 267 | 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\"'" |
| 268 |
| 269 | # Groups: a folder for a group. |
| 270 | as laptop "$VCS" -C shop sync >/dev/null 2>&1 |
| 271 | check "alice makes a group" as laptop "$VCS" -C shop group ops @bob |
| 272 | check "the group lists bob" sh -c "VCS_HOME=$WORK/laptop $VCS -C $WORK/shop group | grep -q '@ops.*@bob'" |
| 273 | mkdir -p shop/ops && printf 'runbook\n' > shop/ops/runbook.txt |
| 274 | check "alice lands ops/" as laptop "$VCS" -C shop land -m "Runbook" |
| 275 | check "ops/ is the group's" as laptop "$VCS" -C shop access ops @ops |
| 276 | 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" |
| 277 | 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" |
| 278 | check "only owners change groups (exit 7)" code 7 as bob "$VCS" -C bobshop group ops @bob @carol |
| 279 |
| 280 | # Approvals: a land waits until someone else approves it. |
| 281 | check "alice requires an approval" as laptop "$VCS" -C shop settings approvals=1 |
| 282 | printf 'reviewed\n' > shop/reviewed.txt |
| 283 | check "alice's land waits (exit 5)" code 5 as laptop "$VCS" -C shop land -m "Needs review" |
| 284 | 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]') |
| 285 | check "alice can't approve her own (exit 7)" code 7 as laptop "$VCS" -C shop approve "$REVIEW" |
| 286 | check "bob approves, and it lands" sh -c "VCS_HOME=$WORK/bob $VCS -C $WORK/bobshop approve $REVIEW | grep -q landed" |
| 287 | 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" |
| 288 | check "approvals off again" as laptop "$VCS" -C shop settings approvals=0 |
| 289 |
| 290 | # MCP, for agents without a workspace (Upstream's host). |
| 291 | if [ "$TARGET" = worker ]; then |
| 292 | rpc() { curl -s -X POST -H "authorization: Bearer $ALICE" -H 'content-type: application/json' -d "$1" "http://$HOST/mcp"; } |
| 293 | check "mcp initialize" sh -c "$(declare -f rpc); ALICE=$ALICE HOST=$HOST; rpc '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}' | grep -q protocolVersion" |
| 294 | 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" |
| 295 | 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" |
| 296 | 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'" |
| 297 | 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'" |
| 298 | 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'" |
| 299 | 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'" |
| 300 | 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" |
| 301 | fi |
| 302 |
| 303 | # A project has a storage limit (60 MB in this test; 100 GiB for real). |
| 304 | if [ "$TARGET" = worker ]; then |
| 305 | head -c 70000000 /dev/urandom > shop/huge.bin |
| 306 | 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" |
| 307 | rm -f shop/huge.bin |
| 308 | fi |
| 309 |
| 310 | # A new machine starts with recent history (here the last 3 versions) and |
| 311 | # fetches older versions when a command asks for one. |
| 312 | NEWBOX="$WORK/newbox" |
| 313 | nb() { VCS_HOME="$NEWBOX" VCS_NAME="Alice" VCS_EMAIL="alice@acme.test" VCS_HISTORY_KEPT=3 "$@"; } |
| 314 | nb "$VCS" login "$HOST" --token "$ALICE" >/dev/null |
| 315 | check "a new machine gets the project" nb "$VCS" get "$HOST/acme/shop" newshop |
| 316 | LATEST=$(nb "$VCS" --json -C newshop log -n 1 | python3 -c 'import json,sys; print(json.load(sys.stdin)["versions"][0]["n"])') |
| 317 | 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 |
| 318 | check "an old version when asked" sh -c "VCS_HOME=$NEWBOX $VCS -C $WORK/newshop log 2 | grep -q 'main 2'" |
| 319 | 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 |
| 320 | check "diffs of old versions" nb "$VCS" -C newshop diff --from 1 --to 2 --stat |
| 321 | 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))'" |
| 322 |
| 323 | # Checks: the host's versions and what main needs, and this machine's objects. |
| 324 | check "vcs check passes" as laptop "$VCS" -C shop check |
| 325 | 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" |
| 326 | OBJECT=$(find "$WORK/laptop/objects" -type f | head -1) |
| 327 | # Its last bytes gone, so it can't decode to what its ID says. |
| 328 | python3 -c 'import sys; p = sys.argv[1]; d = open(p, "rb").read(); open(p, "wb").write(d[:-4])' "$OBJECT" |
| 329 | check "damage on this machine is found (exit 1)" code 1 as laptop "$VCS" -C shop check |
| 330 |
| 331 | [ "$fail" != 0 ] && [ -f "$WORK/server.log" ] && { echo "server log:"; tail -n 60 "$WORK/server.log"; } |
| 332 | echo "hosted: $pass passed, $fail failed" |
| 333 | [ "$fail" = 0 ] |
| 334 |