marsh

Workspaces acceptance scenarios

Black-box scenarios for workspaces, with section references written as sN. Harness: tests/acceptance/workspaces_uat.py, run by make regress ONLY=workspaces (or ONLY=W01-plain-bash) against ~/.marsh-dev (--prefix). There are no unit tests or mocks. Callers are host bash, marsh -c sessions, the host CLI, and registered fixture Kit commands. Inside Kit jobs the caller is the shipped marsh shim, driven by the fixture's argv-only pipeline SEP ARGV [SEP ARGV]... mode. That mode turns %%% into ::: one level at a time, because a job argument cannot be the literal :::. An older fixture without pipeline cannot run W17–W21. Use a current immutable fixture reference for release qualification; a local source fixture is an authoring option.

The harness observes only stdout/stderr bytes, exit statuses, PIPESTATUS, the user's tree, and the user's .git: index bytes and mtime, HEAD, refs, packed-refs, object list, config, and hooks. It also reads out/ artifacts, public JSON (splits, status, jobs show), and stock sbx exec <owned vm> docker inspect. Each run is isolated in its own MARSH_HOME and control home. Cleanup removes only VMs in its own ownership map whose stable IDs it has seen (harness.py IsolatedScope). Every subprocess has a timeout and is killed by process group. A preflight checks marsh --help, which needs no VM, then splits --json, split --help, and join --help. If the CLI is missing, it marks every scenario blocked before any VM work. Without the preflight, an unknown marsh split runs as a Brush script and boots a shell VM. Use --no-preflight to skip it, --only ID (repeatable) to select scenarios, and --fail-fast to stop at the first failure. Evidence goes to <evidence>/workspaces.json.

IDSpec claimSetup / actionPass criteria
W01 plain-bashs2 primary CLI; spool; argv verbatim; SPLIT_*; releasehost bash: printf 'in\0put\n' | marsh split -b sh=cat ::: k fixture streams 'a b' '$x' '' '*' | marsh join -- sh -c 'copy out/'PIPESTATUS 0 23 0; exact out/{sh,k}/{stdout,stderr,status} bytes; SPLIT_ID/DIR/MANIFEST/OBJECTS, with SPLIT_DIR=<root>/.marsh/split/<id>/out; manifest v2 with placement shell-vm and kit:*; split removed (CMD 0); user state unchanged; Kit container verified deleted
W02 lease-kept-joinables6 lease; s2 two-stepsession: h=$(marsh split ... </dev/null); sleep 68 s; marsh join -- CMD <<<"$h"splits --json state kept after the lease; join works and CMD sees A\ta.txt; consumed(0) removes
W03 snapshot-fidelitys1 snapshot S/H/I; s8 gitfile; ignoresmain repo with staged, unstaged, deleted, untracked, and ignored edits; -b g='git status; git diff --cached; rev-parse; ls -A'fork status, cached diff, and HEAD equal the user's; ignored secret.log and .marsh absent; .git is a file; user index bytes and mtime, refs, objects, and config untouched
W04 snapshot-consistencys8 M2 frozen basebranch waits on a gate; the user then edits u.txt, adds late.txt, and stagesbranch reads one and no late.txt; no diff.patch or files; the user's edits and index survive
W05 patch-roundtrips5 layout; s8 patch writer (literal binary, renames as D+A)shell branch: binary append, chmod +x, symlink retarget, file to symlink, delete, add binary, add in new dir, no-EOL, non-ASCII path with space; Kit chmod and Kit symlink; join -- git apply each diff.patchuser tree equals the expected bytes, exec bits, and link targets; files sets exact (M, T, D, A); user .git/index unchanged; the rendering shows binary file changed: PATH (N bytes), never GIT binary patch
W06 non-gits1 (S without H/I); s8 confinementplain dir with shell edits, a Kit write, a Kit absolute write; join -- git applypatches apply, including binary; escape file absent; no .git created; no .marsh/.gitignore
W07 status-pipefails2 declaration order; pipefail-b a='sleep 2; exit 3' -b b='exit 5' with join -- true / join -- exit 4, with and without pipefailp1=3 3 0, p2=4 3 4, p3=0 3 0; join -- true tolerates CMD not reading stdin
W08 join-after-failures2 CMD runs after failure; s2 sugar-b bad='...; exit 4' then join -- CMD; Brush printf in | split { a: cat; b: exit 3 } | join | {...}CMD gets the rendering with statuses and out/bad/{status,stderr}; CMD 0 removes the split; sugar PIPESTATUS 0 3 N 0; $SPLIT_DIR/a/stdout; SPLIT_ID unset afterwards
W09 retentions2 release; s6; s5 status countsfour splits: bare/ok, bare/failed, --keep, -- falsefirst one removed; the other three are kept in splits --json; status --json kept count ≥ 3; splits rm removes each
W10 environments2 env; s2 no ttyhost: credential-shaped names (*_KEY*, *TOKEN, *PASSWORD, *_PAT, *SECRET, *CREDENTIALS, AWS_*) never reach a branch; exported, unexported, MARSH_*, DOCKER_HOST, SBX_*; session: function, unexported var, set -fonly the exported value arrives; /dev/tty fails; no function or set option inherited
W11 kit-confinements8 mounts (NoWriteToUserTree, SnapshotImmutable, AdminReadOnly); s4 no sockets; s5 lineageKit branches write absolute <proj>/x, ../../../../x, .git/x, .git/objects/x, ../store.git/x; plus ok, authority, identityescapes fail and nothing appears; ok = A\tinside.txt; no sockets or authority env; cwd = fork; each receipt's mounts ⊇ the s8 table (admin dir read-write, its config/HEAD/info/packed-refs and the gitfile read-only) and no other user-tree path; store and user objects read-only; receipts carry lineage; containers deleted
W12 admin-tampers8 capture verification; s11 bullet 4Kit writes ../.admin/lk/COMMIT_EDITMSG (must succeed: Git state stays writable) and writes ../.admin/L/{config,HEAD,info/exclude} (fsmonitor and hooksPath to real host scripts) and replaces the .git gitfile; join --keep; host git -C <fork> status and commiteach branch is rejected: or failed with EROFS/EBUSY/EACCES (not ENOENT); fork config has no core.fsmonitor/hooksPath; marker files absent. A control repo first proves host git runs fsmonitor
W13 nested-dotgits8 no nested .git at any depth (case-insensitive APFS)Kit writes src/.git and src/.GiT gitfiles that point at a repo with an fsmonitor markerboth rejected: and fork removed; sibling branch exited 0; marker absent
W14 hostile-user-repos8 metadata read as data, no host Git execution; s11 bullet 5user repo config: fsmonitor, required clean/smudge filter, diff.external, hooksPath, include.path; dirty filesplit and join succeed; no marker from any of them
W15 label-and-argv-refusals2 labels; s2 registered CMD; s7 all-or-nothing../x, a/b, A+a, duplicate, out, Base, .admin, ::: ../y, ::: k /bin/sh -c, ::: k bash -ceach exits 2 with a diagnostic; marker absent; no new split dir; no Kit job
W16 caps-and-pools4 fan-out cap 16; pool of 8 (BudgetBound)17 branches; then 9 × sleep 6 with join -- CMD17 gives 2 and nothing runs; 9 gives exactly one failed: capacity and 8 exited 0, both visible in the rendering, and CMD runs
W17 nested-from-kits2 shim; s3 creator-only join; s1 sibling forks; s5 lineagesession: ::: p fixture pipeline | marsh split %%% c ... | tee /dev/stderr | marsh join --keep; then the session joins the child handlechild created and kept; the ancestor's join exits nonzero and its CMD never runs; child lineage names the parent; child out/ is a sibling under .marsh/split/; c.txt absent from the user tree; receipts verified deleted
W18 depth-caps4 depth 3d1 -> d2 -> d3 nested through pipeline; d3 attempts the depth-4 splitroot exits 2 and out/d1/status = exited 2; "depth" visible; exactly 3 Kit jobs (d4 never runs)
W19 interrupt-subtrees2 signals; s7 cancelhost split: shell sleep 15; touch late + Kit hold + nested Kit hold; SIGINT to the process groupexits 130 within 25 s with no handle; a later join refuses and its CMD does not run; late never appears; all 3 containers verified deleted; the confirmed-cancelled split directories are removed
W20 creator-exit-cascades4 revoke on exit; s7p: pipeline | --exit-after 20 marsh split %%% c fixture hold 600p = exited 7; child split cancelled or done; both containers deleted; finishes in under 120 s
W21 daemon-restarts6 restart; NoReplay; s7 quarantineshell + Kit hold running; authenticated daemon shutdown; next command restarts the daemonclient exits nonzero; split uncertain, forks flagged untrusted and retained; no Kit job is created after the restart; workers reset fixture succeeds
W22 git-clean-replaceds6 (dev, ino) check; fd-based removalbranch gated; user runs git clean -xfd; recreates <id>/w/sentinelsplit nonzero; workspace replaced reported; sentinel survives join and splits rm
W23 non-creator-joins2/s3 creator onlysession A creates; session B runs join -- touch mB nonzero; marker absent; split not released
W25 planted-symlinkss13 descriptor-only host I/Osplit running; an ordinary Kit job tries project-symlink out/a/stdout -> <host file>; the host plants out/a/{stdout,status} symlinksthe Kit job fails (.marsh is read-only to it); the host file is unchanged after capture and splits rm; tampered is recorded
W26 nested-repositorys8 nested .git pruned at snapshotuser tree holds a nested repository vendor/libsplit exits 0; the fork lists no .git there; files = A\ta.txt; the user's nested repository is intact
W27 sugar-interrupts2 signals for the Brush sugarsplit { a: ...; sleep 30; touch late } | join | touch joined, SIGINTexits within 25 s with 130; neither late nor joined appears; the split directory is removed
W28 quarantine-retires7 quarantine; operator retire by recorded UUID(1) stop the Kit VM under fixture hold; workers reset fixture. (2) a shell that ran fixture loses its VM; workers reset all, then reset. (3) Kit quarantine; reset. (4) shell loss; stopevery command exits 0, prints what it removed (or the cause), and the VMs are gone from sbx ls
W29 doc-examplespublic documentation examples as writtenevery doc-test-marked example in README.md, docs/*.md (except agents.md, which P31 covers), docs/guides/*.md, the man page source, and the site's LLM introduction (tests/acceptance/doc_examples.py), from host bash and/or marsh -c, in a fresh repo or plain dir; an example with a control= marker runs again with that text replaced by truestatuses and output as marked; every control run fails (the docs/split.md three-way merge recipe fails without GIT_ALTERNATE_OBJECT_DIRECTORIES="$SPLIT_OBJECTS", because the split's starting version is not in the original object store); status text shows attached shells and split counts; tee /dev/stderr works in a session; --help lists no flight
W30 capture-ignoreds8 capture of ignored output, rendering, phase timesGit repo with .gitignore = __pycache__/, *.log; a shell branch runs python3 -c 'import x', writes build/__pycache__/y.pyc and run.log, adds notes.txt to .gitignore, edits notes.txt, appends to blob.bin; join --timing -- CMDthe branch made a .pyc; files = M .gitignore, M notes.txt, M blob.bin; no diff --git names a .pyc or run.log; the rendering shows binary file changed: blob.bin (5 bytes; full patch in $SPLIT_DIR/py/diff.patch); stderr has timing: snapshot …; forks …; run …; capture py …; consumer …
W24 performanceproposed budget (spec states none)10k-file committed repo, warm; median of 3 for split -b a=true | join vs marsh -c trueoverhead ≤ 1.5 s

Specified details

Creator identity (host CLI callers share the host creator; W23 checks two distinct marsh -c session creators), JSON shapes, root depth 1, and the cancelled state are specified in workspaces.md, section 12.

Edit this page on GitHub