Unit tests cannot see the thing that actually goes wrong here, which is the
request side of a sequence : a round appended to the wrong record, a chain
whose dependentOn points outside its own batch, a write made with the wrong
account's credentials, a read-modify-write that drops the rounds 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. dependents_above was the single gap and is fixed.
pr checkout is the case worth copying: understack_patches walks
dependentOn record by record with direct getRecord reads, so there
is no listing to be short and no index to be behind. 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.
Two orderings are pinned. A close whose author's PDS cannot be read
stops rather than writing a closed that would outrank whatever sits
unread. And a merge whose own PDS is already down never reaches the
knot — asking it first would land the patch and then certainly fail to
record it, manufacturing by hand the half-state the code elsewhere
apologises for. That is an ordering worth a test rather than a
consequence of which line came first
The audit the frame in docs/testing.md prescribes — every place a
failed read becomes an empty answer — came back clean, which is worth
recording so it is not redone. pds_states failing warns and returns
states_complete: false, so the reach travels with the answer;
recorded_head returning None means "no lease", which leaves a push
fast-forward-only rather than overwriting on a guess; absence_is_open
and ownership both swallow into the safe direction and say so. The
one that did not carry its reach was dependents_above, fixed beside
this
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, which makes what it leaves behind a branch and nothing else:
stack create --add-change-ids puts the rewritten branch back, and a
reconcile whose push is refused writes no records at all. Both were
confirmed by mutation — removing the undo fails this test and the batch
one together, through two entirely different roads to the same
guarantee
The appview can be made to fail too, and the three shapes that depend
on it are told apart. World::web_fails is the lever; a 404 was
already modelled and several commands read one as a real answer ("no
number for this pull yet"), which is why an unreachable service needed
its own switch rather than reusing it.
A dead appview costs pr close its answer outright — resolving the
repo owner is what stops a close replacing a merge, and the redirect is
the only route from a repo DID to its owner — so it refuses and names
the service. It costs stack view a number and nothing else, since
the record key is the identifier that always exists. Separating those
two is the point: losing a service must stop a write whose safety
depended on it and must not stop a read it was only making prettier
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. Six tests now cover it: pr list
falling back to the PDS with a warning that names what is missing, the
same listing erroring rather than printing an empty one when there is
nothing of your own to fall back to, search failing rather than
reporting no matches (it is the one command with no non-index answer),
both stack write paths saying the index they rely on is unreachable,
and stack view staying silent about an index it never asked.
The write paths carry on after warning, which is the right answer and
worth writing down: two of the three ways a blind index could hurt them
are structurally guarded, since a member below a visible one is named
by its dependentOn and shows up as missing_below. The third cannot
be guarded at all — nothing points upward, so a contributor's member
stacked on top is invisible with no trace — and the warning is the
whole of the available mitigation
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
A model of the reader, beside the mocks of the writer.
tests/support/appview.rs walks the records the way Tangled's own
GetStack does and reports what it would arrive at — a chain, an
ambiguity, a malformed link or a cycle — and
Scenario::assert_stack_reads_alike asserts that atgc and the appview
read the same stack off the same records. That is the half of the exit
criterion above that the mocks cannot reach. They check what atgc sent,
and a stack can satisfy every rule a PDS enforces — every write
authorized, every precondition met, every record valid against its
lexicon — and still be one tangled.org draws wrongly. The retire fork
was exactly that, and the whole suite stayed green through it.
A fork is not an error to the appview, which is the fact worth
carrying out of this: GetStack asks for the pull depending on a
given one and takes whichever row comes back, so a fork is a coin toss
rather than a refusal, and the live member above can silently drop out
of the stack view. Three tests plant chains directly — a fork, a
dangling link, a cycle — because no current atgc can be made to write
one and an oracle with no failing case proves nothing. It is a reading
of somebody else's source and it goes stale; the module doc says which
file and which date
The index the stack writes actually read. stack resubmit and
stack merge take Source::EVERY — the PDS, Bobbin and the web
scrape — unconditionally and whatever ATGC_USE_BOBBIN says, because a
merge has to see a member somebody else opened. So an index row is an
input to a destructive decision rather than a convenience for a
listing, and every stack test until now read one source, on a repo the
acting account owns. Three tests cover the index against those records:
the default asks no index at all, a member the index has not caught up
with is still a member, and a stale row does not turn a retire into a
deletion — which holds only because the state rule inverts on your own
repo, where Bobbin has no third party to be better informed by. Three
more drive a stack opened against a repo the account does not own,
which the file had no coverage of at all: every member reads ?, both
write commands refuse rather than guess, and consulting the index is
the whole of the recovery
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/stack_flows.rs, tests/pr_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 round
appended to the wrong record, a chain whose dependentOn points outside
its own batch, a write made with the wrong account's credentials, a
read-modify-write that drops the rounds 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