Unit tests cannot see the thing that actually goes wrong here, which is the
request side of a sequence : a version attached to the wrong ticket, a write
made with the wrong account's credentials, a read-modify-write that drops the
comments it was not editing.
The Evidence-adoption audit is done and came back clean, recorded so
it is not redone. Every listing read in the command layer either checks
its own reach before concluding, or is best-effort with a written
argument for why silence is right there — pr create's two warnings,
which may not block a create; issue's state walk, which warns and
shows ? because a listing shows a state where a write path
decides on one. pr checkout is the case worth copying: a version
names its base and head oids, so the command fetches the head from the
knot that holds it rather than walking a listing. Where the question is
"what does this record point at", the links are the answer
A PDS can be made unreachable, per account, which completes the set:
index, appview, knot XRPC, knot push, PDS. Per-DID rather than a single
switch, because atgc's own PDS going down is the uninteresting case —
every command fails at its first read and says so. The interesting one
is somebody else's answering while yours does not: a pull's author, or
a repo owner, whose records decide a state you are about to write over
Every service atgc leans on can now be made to fail, and the knot was
the last one. Checkout::refuse_pushes installs a pre-receive hook
in the bare repo pushes land in — a knot refuses at the protocol layer,
and git reports that differently from a filesystem it cannot open, so
the hook is the faithful shape rather than an unwritable directory.
A refused push is the only knot failure that happens before any record
exists; pr create reports it, names the fork route, and writes no
ticket
The merge half-state has a test. A merge is two writes to two
services with no transaction over them: the knot moves the branch, then
the PDS records a merged status per pull. The second failing leaves
work that is genuinely landed and pulls every listing still calls open,
and the refusal names them one per line so somebody can finish the job
by hand — which is the message a person reads at the worst moment they
will have with this command, and nothing checked that it said anything.
It asserts the knot really was asked, as well: a half-state and a
refusal-before-anything-happened want opposite responses, and getting
that backwards would make the message alarming in the wrong direction
An index that is down can be written as a test, which it could not
be before: the mock answered 200 with rows or 200 with none, so every
index fixture here modelled an index that answers. A refused request is
a different fact — an empty answer is evidence about the world, a 500
is evidence about nothing — and the fallbacks for it were written and
untested.
World::bobbin_fails is the lever. search fails rather than reporting
no matches, because it is the one command with no non-index answer
The mock PDS serves listRecords newest first, by descending record
key, which is what a real one does — and its cursor walks down rather
than up. It served them ascending for its whole life, which made every
paging test arrange its keys backwards from reality and made the one
rule that reads the order rather than the count untestable rather
than untested: the backwards walks stop when a page ends below the
subject's own key, a condition that against an ascending mock can never
hold on the page it is meant to hold on.
Reversing it broke no test, which is the finding rather than the
relief: nothing in the suite depended on the order, so the paging rules
were never being exercised. The test that was impossible before is now
there — two hundred planted status records, and a state change that
reads two pages rather than four because each walk stops at the floor.
Stubbing the floor out makes it fail, which is the property the entry
above asked for and could not have
tempfile as a dev-dependency. Eleven hand-rolled temp-directory
helpers across testutil.rs, config/, cmd/, logging/ and
tests/ — not nine — plus seven more directories built inline at the
test that used them, four with a Drop impl of their own, most
disambiguating by pid, which is an assumption about how the suite runs.
Dev-only, so it never ships. Production write_atomic stays as it is:
it sets the mode on the open(2) call, which NamedTempFile::persist
would regress.
The one thing that did not survive a straight swap, recorded because it
is invisible until something asserts on it: tempfile creates a
directory 0o777 & ~umask , where the helpers it replaced handed back
a path that did not exist yet and let create_private make it 0700.
create_private is DirBuilder::recursive(true).mode(…), and that
applies its mode only to a directory it actually creates — so on an
existing one it is a silent no-op. Two consequences. The narrowing test
caught it directly, and now creates a child of the throwaway root so
the directory under test is one create_private made. And the five
helpers standing in for ~/.config/atgc are pinned to 0700 through
Builder::permissions, because a test that plants a 0600 store file
inside a world-traversable directory is checking something weaker than
the thing it names. The ordinary scratch directories are left at the
default, which is what create_dir_all gave them before
Integration environments: whole sequences of commands driven through
the built binary against mock services on loopback ports
(tests/support/, tests/pr_flows.rs, tests/issue_flows.rs). What
they assert is the request side — what atgc sends, in what order, as
whom — which no fixture can observe and no unit test can reach: a
version attached to the wrong ticket, a write made with the wrong
account's credentials, a read-modify-write that drops the comments it
was not editing. Two
properties make it hermetic: Command::env is not unsafe, so a child
may have its own HOME, and jacquard returns an unexpired session
without contacting anything, so a fixture session restores offline. The
argument against mocking a parser is unchanged; see docs/testing.md
Cli::command().debug_assert() runs in main.rs's
no_duplicate_ids_or_dangling_references_anywhere_in_the_command_tree.
It walks every subcommand, which is what nothing else in the suite
does — parsing a line only builds the subcommands on that line, so a
malformed definition three commands away stays invisible until
somebody types it. Found nothing on the tree as it stands, which is
worth saying only because the check was proven to bite first: giving
--no-input a -q short makes it fail with "Short option names must
be unique for each argument, but '-q' is in use by both 'quiet' and
'no_input'". The --account collision that
auth_switch_and_logout_do_not_collide_with_the_global_account_flag
documents is the same class, and was found by hand months late
The fixture models Tangled's multi-account shape instead of collapsing
it. Every account is served by its own mock PDS on its own port and
answers only for itself, so asking the wrong host is a 404 the way it
is in production; a write whose bearer does not own the repository is a
403, which the mock had never checked. A third account, Carol, and
Scenario::upstream put the repo in the hands of somebody neither
contributor is — the case where "the owner's PDS" and "my PDS" stop
being the same host. All 1,230 tests pass against it, which is the
evidence that the merge bug was the only one of its kind the suite
reaches
scripts/test-isolated.sh runs each test binary with a home of its own
and fails the run if anything was left in it. Containment and detection
are different problems and neither is enough alone: a sandbox stops a
test writing to ~/.config/atgc but says nothing, so the escape happens
into a home that is thrown away and nobody learns of it. Four unit tests
had been depositing a did:plc:alice directory in the real
configuration directory of whoever ran the suite, green every time,
because HOME cannot be redirected from inside the test process — a
test that reaches the real directory finds it, works, and passes. Under
bubblewrap the home is unreachable as well as empty; without it the
script says so and still does the checking, so a CI image that forbids
user namespaces gets the half that finds bugs
The auth verbs are driven end to end. The mock grew an
authorization server — PAR, authorize, and a token endpoint that spends
each refresh token once — so tests/login_layout.rs logs in for real
from an empty config directory
(a_login_creates_the_layout_it_later_reads) and refreshes an expired
session through auth token
(a_refresh_rewrites_one_identity_and_leaves_the_rest_alone),
including two processes refreshing one identity at once.
tests/auth_flows.rs covers the account store under a command that
rewrites it and one that only reads it, auth token's promise that
stdout is the token and nothing else, and auth logout by name and
--all
The mocks match Tangled's services in four more places, each cited
from Tangled's source where the mock implements it. A PDS demands a
DPoP nonce on first contact and turns it over. knot2 lists records
oldest first, and the mock's list methods page two records at a time,
so each listing walks a cursor. A submitted patch's base must be
a commit the knot holds. The mirror answers a refused merge push with
a bare 500. Two atgc bugs showed: record and tag walks stopped at the
first short page, which knot2 can send with more behind it, and
pr merge printed a refused push as an unexplained 500