-
released this
2026-08-22 22:41:42 -05:00 | 1 commits to main since this releaseA truncation warning that promised an ordering it does not have. Wording only;
no behaviour changed.Fixed
-
list_messagessaid "This is the newest 200 of 358" and the window is not the newest
anything. The cap isuids[-cap:]— the highest UIDs, which is arrival order in that
label. ForINBOXthat is close enough to date order that the sentence went unnoticed for
twelve versions. For the Bin it is the order things were trashed, so a 2009 newsletter
deleted last week sits above a 2019 one deleted in 2019.A connector read that sentence, was asked for the oldest messages in the Bin, and reported
a pagination bug in this server — reasoning correctly from a claim that was false. A
read-only census of the live Bin found 358 real matches and zero dropped messages older
than the window showed, somessages_foundwas right and the answer happened to be
right; nothing in the mechanism guarantees the second part.The warning now says "Only N of M" — matching
icloudmail, which never made the claim —
and names the question the result cannot answer: not "the oldest N", withbeforeas
the way to ask it properly. -
The behaviour is deliberately unchanged. Sorting by date before capping means an
ENVELOPE fetch for every match in order to discard most of them, andbeforealready
answers the question. The defect was one word, and one word is what moved.
Notes
- The general shape: a warning about incompleteness is read more carefully than the data,
so an inaccuracy in it is load-bearing. This one turned a correct result into a bug
report against the server. The test pins the wording rather than the behaviour, which is
unusual here and is the point.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-22 22:07:15 -05:00 | 2 commits to main since this releaseG5. The Gmail tenant's irreversible surface.
expunge_messagebehind
MAIL_EXPUNGE_ENABLED,unsubscribe_messagebehindMAIL_UNSUBSCRIBE_ENABLED.The tool list changed, so reconnect the Gmail connector.
Added
-
expunge_message, and it routes through the Bin because nothing else works.
expected_subjectrequired by the signature, checked against a real ENVELOPE before
anything moves, and an untitled message cannot be destroyed through it at all.A straight port of the mail tenant's method would have lied.
icloudmailflags
\Deletedand expunges in place; that destroys the message there. Probe 11 measured the
identical sequence here and it archives. Gmail has a per-account setting — "when a
message is marked as deleted and expunged from the last visible IMAP folder" — whose
default is Archive, and it is not readable over IMAP. Measured:expunge inside result a label the message also has others for the LABEL goes, the message does not its last label archived into All Mail All Mail nothing at all the Bin destroyed So the Bin route is unconditional — the setting cannot be read, so it is the only
implementation that means the same thing on every account. This would have shipped the
exact failure this project exists to refuse, inside the one tool whose whole description
is a promise of irreversibility. Written up inCLAUDE.mdunder An expunge does not mean
the same thing on every server. -
unsubscribe_message, one-click only on this tenant. Nourlargument, ever — the
URI comes from the message's own signed header, read server-side, because a tool that
POSTs to a URL it was handed is an open POST proxy reachable by text sitting in an email.
Amailto:-only sender is refused withmailto_only: that half sends mail and this
tenant has no submission path until G4.send_enabled=Falseis passed to the gate
hard-coded rather than read from config, so a deployment that turned
MAIL_SEND_ENABLEDon cannot get a gate approving a mailto this client then cannot send.The gate logic, the flag allowlist,
allow_unauthenticatedcarrying an authentication
refusal and nothing else, redirects reported and never followed — all of it is
mail/unsubscribe.pyunchanged since 0.24.0. None of it was re-decided. -
unsubscribe_messagedeliberately does NOT nest inside the write gate, unlike
expunge_messagewhich does. It changes nothing in this mailbox — it sends a request to a
third party — so the write allowlist has no bearing on whether it is safe, and requiring
it would tie a decision about talking to strangers to a decision about filing mail.
Fixed
- A server that can unsubscribe no longer calls itself READ-ONLY. Because the tool
registers with no write allowlist at all, the read-only sentence and a tool that
irreversibly tells a stranger the mailbox is live could coexist — and did, in the first
draft. Caught by the surface test rather than by review. READ-ONLY is kept for the case
where it is true, which is also the default deployment.
Notes
-
The phase-ordering argument was measured and turned out to be about a different
mailbox.docs/gmail-plan.mdhad unsubscribe shipping after send, because 24 of 40
iCloud senders offering anything offered onlymailto:. On Gmail: 200 INBOX messages
sampled, 61 offering a facility — 30 both, 27 one-click only, 4 mailto only. 57 of 61
(93%) usable with no sending capability at all. G4 would add almost nothing to this
tool's reach here.The lesson is not "unsubscribe goes early" but that a phase-ordering argument built on
one account's header census is a fact about that account. Re-measuring cost four lines. -
Every Gmail probe had been failing to clean up for weeks, in exactly the way probe 11
now documents. Each teardown expunged inside its own scratch label and printed
"removed N"; a sweep found 14 supposedly-destroyed fixtures sitting in All Mail,
including from probe 8 — the probe whose own finding is that a label expunge does not
delete.probes/_gmail_cleanup.pyis now the single correct implementation and every
Gmail probe calls it. Gitignored, so nothing here changes. -
Live pass run:
probes/g5_verify.py, 21 checks, all green, including the one that
matters — afterexpunge_messagereturns, the message is gone from every label on the
real server, All Mail included. Written up indocs/operations.md. Still owed: one
real one-click unsubscribe, chosen deliberately, because succeeding at that tells a
stranger this mailbox is live and is not a thing to do for a test.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-22 21:19:14 -05:00 | 3 commits to main since this releaseTwo small things in the Gmail tenant's preflight, both spotted by reading its
boot log rather than by any test. No tool, argument or description changed, so
no reconnect — a service image update is enough.Fixed
-
oauth_storageran twice on this tenant. The shared OAuth block runs it
for every tenant, and the Gmail branch ran it again, so the container probed
the same directory twice and printedcheck=oauth_storagetwice. Nothing
broke, and that is the problem with it: an operator counting preflight lines
against the docs finds one too many and has to work out which is real. -
Gmail dialled out to IMAP even when a local check had already failed. The
other three tenant branches guard onlocal_ok; this one did not, which was
an oversight from when the tenant was added rather than a decision. A
container about to report a broken timezone or a missing secret would open an
IMAP connection first and put a second, less self-explanatory failure line in
front of the one that actually says what is wrong.
Notes
-
Neither was reachable from the suite, so the tests are new and are written
as properties over every tenant — "no check runs twice on any tenant" and
"a broken local config stops the network checks on every tenant" — rather than
as two assertions about Gmail. Both defects arrived with a new tenant, and
the next one would arrive the same way.Confirmed by restoring the old branch and watching both fail, per A new guard
is not proved until it has been broken inCLAUDE.md.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-22 21:10:16 -05:00 | 4 commits to main since this releaseAn unmatched folder name no longer reads as a misconfiguration. One error
string on the Gmail tenant; no tool, argument or description changed, so no
reconnect is needed — a service image update is enough.Fixed
-
resolve_folder's refusal said the wrong thing about WHY a label was
missing. It read "there is no folder named X available to this server … a
name that is not listed cannot be read at all", which is true of an
allowlisted deployment and false of a label created ten minutes ago:
list_foldersis cached forFOLDER_LIST_TTL, an hour by default, so a new
label is genuinely absent from that list for up to an hour after it exists.Found in the field, and the cost was a wrong recommendation rather than a
wrong read. A model asked for a label created minutes earlier, got this
refusal, and reported to the operator that the label "wasn't allowlisted" and
should be added to the read allowlist — on a tenant whoseMAIL_FOLDERSis
deliberately empty, meaning every label is readable and there is no allowlist
to add anything to. The refusal itself was correct; its explanation produced a
confident, wrong config change.It now says the server cannot currently see the name, that a recently
created label may simply not be listed yet, that the list is cached for up to
an hour, and not to advise changing any server setting on the strength of the
message. The 0.29.7 half — this is not the same as the folder being empty —
is unchanged and still the more important half. -
The test pinned the sentence rather than the property, so it would have
passed on any wording that happened to contain the old phrase and failed on
every improvement to it. It now asserts what the refusal has to prevent: a
reader concluding "no mail", and a reader concluding "misconfigured". Both are
things that actually happened.
Notes
- An error a model acts on is part of the interface. The tenant's own
CLAUDE.mdalready says a refusal must be written to be acted on rather than
merely reported; this is the same rule extended one step, to what the reader
will infer about the cause and recommend on the strength of it.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-22 13:59:41 -05:00 | 8 commits to main since this releaseG2. The Gmail tenant can write.
update_message,move_messageanddelete_message,
behindMAIL_WRITABLE_FOLDERSand registered nowhere when it is empty — which it still is
in the stack file, because the setting does not go in until the live pass has run.The tool list changed, so reconnect the Gmail connector. Claude caches tool
descriptions per connector and does not refetch them because a container restarted.Added
-
update_message(message_id, read, starred). Two optional flags, neither defaulted;
passing neither is an error rather than a no-op reported as success.starred, where
the mail tenant saysflagged— Apple's clients draw a flag, Gmail draws a star and
has a Starred view a user asks for by name. Probe 10g confirmed the IMAP\Flaggedbit
and the[Gmail]/Starredlabel are the same thing, so the rename costs nothing
underneath. -
move_message(message_id, destination_folder), and it says what it really does.
Gmail has labels, not folders: this removes the source label and adds the destination,
and the message KEEPS every other label it had and stays in All Mail. The description
says so in those words, because a model that reports "moved" to a user who then finds
the message under another label has told them something false about their own mailbox.The destination goes through the same four-tier resolver the read tools got in 0.29.7,
sospam,trashandSent Mailall land correctly — and a name matching nothing is
an error, never a silent success. The source does not: it comes out of a handle this
server minted, and running that through a fuzzy matcher would let a forged near-miss
resolve onto a real label. -
delete_message(message_id), trash-only, no second argument, and none ever. Gmail's
delete_messageis born the way 0.30.0 made the mail tenant's, so the two connectors
never mean two things by one name.expunge_messageis G5's.
Changed
-
A move here is ONE command.
icloudmailmoves in three — COPY, flag\Deleted,
scopedUID EXPUNGE— because iCloud advertises noMOVE. Gmail does, probe 10b issued
one and read aCOPYUIDback, so that workaround is not ported. Its intermediate state
— a message flagged deleted but not expunged, hidden in most clients while still
counting against the label — cannot arise here. -
_trash_folderresolves the\Trashrole FIRST on this tenant, the opposite order
from next door, and the inversion is load-bearing rather than tidy.TRASH_FOLDERis
shared by both mail tenants and defaults to Apple'sDeleted Messages, which is not a
label on any Gmail account — so config-first would send every delete to a label that
does not exist, or, if a user happens to have made one by that name, to an ordinary
label Gmail's Bin view never shows while the tool reported success. Written up in
CLAUDE.mdunder A role folder resolves the way the provider guarantees it. -
The server instructions stop claiming to be read-only when they are not. The
sentence is now built from the same setting the tools are registered on, so the promise
in the initialize response and the surface behind it cannot drift apart. -
New preflight check
gmail_trash, silent when writes are off. Not a restatement of
mail_writable: that asks whether the write allowlist is readable, this asks whether
the one label every delete lands in is reachable at all. A deployment can pass every
other check and still refuse every delete.
Notes
-
probes/gmail_probe_write.pyran first, and G2 was scoped without it. Probes 1–9
read CAPABILITY, provedCOPYadditive and proved an expunge inside a label removes the
label — but never issued aMOVE, never read aCOPYUID, and never touched Trash,
which is three of the four things these tools are built out of. Findings in
docs/gmail-plan.mdunder Probe 10. The two that changed the code:A COPY to
[Gmail]/Trashremoves the source label, unlike a COPY to any other
label, so a delete really does take the message out of the mailbox and the description
may say so. And Trash HIDES a message's other labels rather than stripping them — a
two-label message put in Trash was invisible in both whileX-GM-LABELSstill reported
them, and moving it back out brought the untouched one with it. That is what lets
delete_messagepromise a reversible delete instead of hedging. -
Two of five new tests passed against deliberately broken code, and both were fixed
rather than trusted. One asserted a true thing the broken version also satisfied; the
other could not reach its own hazard because the fake ASSIGNED where imaplib APPENDS.
The rule is now inCLAUDE.mdunder A new guard is not proved until it has been
broken. -
Not yet run live. G2's pass is written up in
docs/operations.mdbeside M1–M5 and
G1 and is owed before this is trusted on real mail — including the absence check, which
no unit test can confirm on the deployed service.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-22 13:01:04 -05:00 | 9 commits to main since this releaseM6. Destroying mail is its own tool, its own grant, and its own decision.
delete_message(permanent=true)is gone;expunge_messagereplaces it. Gmail's write phase
(G2) is what forced the question — itsdelete_messagehad to be specified before it was
written — and the answer applies to the tenant that already shipped one.Breaking, and in the way that costs a minute: the mail connector's tool list changed, so
reconnect it — Claude caches tool descriptions per connector and does not refetch them
because a container restarted. Per-tool permission toggles are keyed to tool names, so
expunge_messagearrives at its default anddelete_message's existing toggle survives.
Same shape as 0.14.0's rename, and the same reason it ships on its own rather than riding
along with the next change.Changed
delete_messagemoves to trash and does nothing else. Nopermanent, no
expected_subject, one argument. It is the tool a model reaches for when a user says
"delete", and it can no longer do anything a user would not expect from that word.
Added
-
expunge_message, behindMAIL_EXPUNGE_ENABLED.\Deleted+UID EXPUNGEon one
message,expected_subjectrequired rather than conditionally required, and no
message_idin the response because there is nothing left to name.A boolean was the weakest available gate on the one operation with no undo — one token
in a JSON object, on the same tool as the safe operation, described in a paragraph
appended to a description whose opening sentence was about moving to trash. Splitting buys
three things a parameter cannot: a description where every sentence is about
irreversibility, a grant that can be given and withheld separately, and a decision visible
in the call rather than buried in an argument. -
The gate nests, and it is the only one here that does.
MAIL_EXPUNGE_ENABLEDlayers
overMAIL_WRITABLE_FOLDERSrather than sitting parallel to it: that list says which
folders may be changed, this says whether destroying is permitted at all, and an expunge
is both. Turning it on alone registers nothing. While it was a boolean, naming a folder
writable was already consent to destroy what was in it, with nothing in the boot log to
say so — the grants line now carriesexpunge=on|off.MAIL_FOLDER_ADMINdeliberately does not nest, and the two are not inconsistent:
managing a collection is a different kind of grant, while destroying is a stronger degree
of the same one.
Notes
-
CLAUDE.mdsaid not to do this, and now says when to. "Do not split a tool to dodge
a bad argument" —delete_eventkeepsthis/following/allin one parameter, and
that rule stands. The distinction is written down rather than left to whoever remembers
this release:delete_event's scope is one operation with three equally-undoable targets,
while delete-versus-expunge is two operations that differ in whether the mail still
exists. The test is not could this be one argument — it usually could — but does the
choice change what a wrong call costs, and would an operator grant one side without the
other. Two yeses is two tools. -
Named
expunge_message, notdelete_message_permanently. Two names differing only by
a suffix are the easiest pair to confuse, which defeats the split. Expunge is IMAP's own
verb, so nothing about "delete this email" reads as selecting it by accident. -
A test tried to assert the wrong property and the fake refused it. "A delete never
expunges" is false about a correct implementation: iCloud advertises noMOVE, so a move
is COPY, flag the original, expunge the original. The COPY is the whole difference —
one call leaves the message somewhere and the other does not — and that is what is pinned
instead. -
The trash-missing error no longer offers the irreversible path as the way out. It read
"or pass permanent=true to remove the message outright", which is a suggestion to
destroy mail because a configuration value is unset. It now namesTRASH_FOLDERand says
explicitly not to reach forexpunge_messageinstead. -
Not yet run live. M6's live pass is written up in
docs/operations.mdbeside M1–M5
and is owed before this is trusted on real mail — including the two absence checks, which
no unit test can confirm on the deployed service.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
released this
2026-08-22 12:25:27 -05:00 | 12 commits to main since this releaseTwo defects found the same way the last three were: a person used the connector and
asked what an answer meant. Checking the Gmail spam folder produced{"messages": [], "count": 0}— read, reasonably, as "the Spam folder is clear". It held 29 messages. Then
inspecting one of them failed twice with aTypeErrorabout a type nobody had heard of.Fixed
-
A folder name that matched nothing was answered with an empty result instead of an
error.folders=["SPAM"]reached IMAP unchanged;SELECTrefused it;search_messages
caught the refusal per folder — correctly, because one bad label must not lose the rest
of a search — and the caller was handed a clean, successful, empty answer. The real label
is[Gmail]/Spam.The bug is not that the guess failed. It is that failing was indistinguishable from
succeeding, which is this project's recurring defect in its purest form: nothing in the
response could tell "this folder is empty" from "this folder does not exist", so there
was no way to be suspicious of it.list_folderswas the only route to the exact
strings, and nothing required calling it first.Names are now resolved against the readable labels before any command is sent, in four
tiers — the exact name, then case-insensitively, then with the[Gmail]/prefix
optional, then by RFC 6154 role, sospam,junk,binandarchiveland on the right
label on an account in any language. A name that still matches nothing is refused
with the closest real label named: "There is no folder named 'Spem'… Did you mean
'[Gmail]/Spam'?" The four tiers are ordered so a user label literally calledSpam
outranks the role folder it shadows.Resolution only ever considers labels
MAIL_FOLDERSalready allows, so it is not a way
around the allowlist, and the handle path — where amessage_idnames its own folder —
is deliberately left matching exactly. -
A single 8-bit byte in a header took
get_message_securityout entirely. The error
wassequence item 0: expected str instance, Header found, and it repeated because
nothing about it suggested retrying was pointless.Message.getdoes not always return a string. The compat32 policy wraps any header whose
raw bytes are not ASCII in anemail.header.Header, and that is the ordinary shape of
spam — an unencoded em dash in aFromor aList-Unsubscribeis a syntax error the
RFCs forbid and bulk senders emit anyway. So the tool whose entire job is judging
suspicious mail was lost to the malformation that makes mail suspicious.Two more failures sat behind it, neither reported yet and both reachable from the same
message:decode_headerraisedLookupError: unknown encoding: unknown-8bit— which is
the read path, not the security path — and_unsubscriberaisedAttributeErroron
Header.strip. Every header now leaves the message throughmail/headers.py, which
flattensHeader,strand absent alike, and falls back to UTF-8 for any charset label
Python cannot look up rather than raising.The bytes are decoded, not replaced.
str(Header)renders every non-ASCII byte as
U+FFFD;decode_headerhands the original bytes back, and they are nearly always UTF-8,
so the character the sender wrote is what a reader sees. A header that is both 8-bit
and RFC 2047 needs two passes to get there — the firstdecode_headershort-circuits
on aHeaderand leaves=?utf-8?B?…?=sitting in the output as literal text.
Notes
-
The Gmail fake refused nothing before this.
select_folderaccepted any name, so the
silent-empty defect could not be reproduced against it however many tests were written —
the same lesson as 0.29.6's crowded label, one file along. It now answers
NO [NONEXISTENT] Unknown Mailboxthe way the server does, and serves whole-message bytes
soget_messageandget_message_securityrun end to end for the first time. -
Verified by reverting. Each new test was run against the unpatched code: the tool
returns{'messages': [], 'count': 0}for["SPAM"], and the malformed message raises
TypeError,LookupErrorandAttributeErrorin three separate places. -
The header fix is in
mail/, so the iCloud tenant gets it too — it parses the same
messages with the same functions and had the same crash waiting in it. -
icloudmailstill has the folder defect. Its_search_targetsmatches names
literally in exactly the same way. Not fixed here: its folder story is different enough —
wildcards, no SPECIAL-USE on Drafts — that the resolver is not liftable unchanged, and
guessing at it without a live pass is how the last one shipped.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-22 00:19:33 -05:00 | 14 commits to main since this release0.29.5's own fix carried a regression, and the acceptance run caught it in one question.
Asked to searchis:unread, the connector reported "the newest 200 of 458" — then pointed
out that 458 could not be reconciled with the 904 the count tool gives, and said so rather
than picking one. Both numbers were wrong for different reasons.Fixed
-
The truncation warning understated the number it exists to disclose. 0.29.5 set
found
to the count of DISTINCT messages, but built that from the per-folder window, which is
capped atMAX_MESSAGES. One label held 856 matches and contributed 200, so 656 vanished
from the total the warning quotes. The true figure was 1088; it said 458.This is the worst possible line to get wrong in this direction. Its entire job is to
say the answer is incomplete, and it was itself incomplete — a reader told there are 458
believes 458 exist and stops looking.X-GM-MSGIDis now fetched for every matching uid,
not just the window; that is one integer per message and no envelopes, so the expensive
fetch still runs only over what is returned. -
The default search could not see archived mail at all. Excluding All Mail was the
obvious way to avoid duplicates, and it was wrong: an archived message carries no other
label, so All Mail is the only place it exists. On the live account that was 42 unread
messages a default search could not reach, with nothing in the response to suggest the
answer was partial.All Mail is now searched last. That is safe only because 0.29.5 made results collapse
on message id — placed last it supplies the messages nothing else has and loses every
duplicate to a real label, so coverage is complete and the folder shown is still a real
label wherever one exists.After both fixes,
is:unreadreports 1088, which reconciles exactly:
904 (All Mail) + 155 (Trash) + 29 (Spam).
Notes
-
The first test written for this passed against the broken code, because every folder in
the fixture held fewer messages than the cap, so the window never dropped anything. The
fixture now has a crowded label — the shape the live account actually has. A test whose
fixture cannot reach the failing branch is not a test, and the mutation run is what
exposed it. -
Three fixes deep in the same defect now: the aggregate count (0.29.2), the fan-out
(0.29.5), and the count of what the fan-out did not return (0.29.6). Each was found one
layer further out than the last, and none by the suite. -
Every one of them was found by a person using the connector and asking what a number
meant. That is the acceptance test this project did not have, and it has now paid for
itself three times. -
G1's live pass re-run and extended: 54 checks, 0 failed. One of its assertions had to
change, because it encoded the old exclusion — the probe was requiring the behaviour that
hid the 42.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-21 23:57:10 -05:00 | 15 commits to main since this releasesearch_messagesreturned one message once per label it carried. Found by driving the
deployed connector through an acceptance run — a person asking it twelve questions — rather
than by any test here.Fixed
-
A search that fans out across labels returned duplicates.
from:google older_than:6m has:attachmentcame back as three results that were one message, sitting in INBOX,
Sent Mail and a user label; a month of attachments came back as 25 rows for 23 messages.
Gmail files one message under every label it has, andsearch_messagesconcatenated the
per-label hits.This is 0.29.2's defect one tool along. That release fixed the aggregate count and did
not sweep the fan-out — the same overlap, the same wrong number, in the tool next door.Results are now collapsed on
X-GM-MSGID, Gmail's own per-message id, which is
identical across every label a message appears under (verified live). The RFC822
Message-IDheader was considered and rejected: it is written by the sender, so it is
absent on some mail and forgeable on the rest, and a dedup key an attacker can choose is
one that can hide a message by colliding with another.foundcounts messages too, not rows — otherwise the truncation warning claims more
matches than exist. -
The surviving copy is now the useful one. Default searches order labels INBOX first,
then user labels, then role folders, and keep the first match. Previously the survivor was
whateverLISTreturned first, which could report a message as sent — or as spam —
because a copy of it was also there. An explicitfoldersargument keeps the caller's
order, since they expressed a preference.
Notes
-
The fake could not express the bug.
_FakeIMAPimplementedlist_foldersand
folder_statusand nothing else, sosearch_messageshad never been run end to end in a
test — only_search_targetswas, which checks the folder list and not what comes back
from it. The fake now holds messages that carry LABELS rather than a location, which is
the property under test: a fake where every message sits in one folder cannot fail a
deduplication bug, however many tests are written against it. -
Mutation-tested, and the first attempt was wrong. Disabling the
continuestill
passed, because the dict collapses by key on its own; only giving every row a unique key
removes deduplication, and that fails all four new tests. A mutation that does not fail is
a test that does not test. -
G1's live pass re-run after the change: 52 checks, 0 failed. Required, because the read
path was touched.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-21 23:05:03 -05:00 | 16 commits to main since this releaseThe Gmail tenant served no
/healthz, so its container never started. Third defect
found by deploying rather than by testing, and the one that named itself least.Fixed
-
build_gmail_mcpregistered no healthcheck route. The image carries
HEALTHCHECK CMD ... urlopen('http://127.0.0.1:8787/healthz'), so the request 404'd,
failed three times, and Swarm killed the task before it reached RUNNING — over and over.Nothing in the symptom named the healthcheck. The boot log was flawless:
tenant=gmail,
all seven preflight checks passed,check=imap detail=10 folder(s) readable,serving | tenant=gmail path=/gmail port=8787. Portainer showed the task stuck instarting
forever. nginx returned 502, because an unhealthy task is never added to the service VIP,
so there was nothing behind the proxy to reach. Three layers, three plausible wrong
diagnoses, and the actual cause invisible from all of them.Preflight cannot catch this: it runs before the app is built and asks about IMAP and
Forgejo, not about our own routing table. -
subscriptionshad the identical gap, since 0.21.0. The only reason it was not the
tenant that failed is that it has never been deployed. Fixed in the same commit.
Added
tests/test_healthcheck.py— every tenant inTENANTS, asserted against the built
app's route table. Written as a loop rather than a test beside each tenant, because the
failure mode is a tenant being forgotten, and a per-tenant test is forgotten in exactly
the same motion as the route it would have checked. A fifth tenant fails here on the day
it is added.
Notes
- Three defects, three layers, none reachable from the one below. The gate proved the
code; G1's live pass drove the tool layer and found the unread overcount; the first boot
found the tenant dispatch; the first deploy found this. Each needed the layer above to
exist before it could fail — which is the argument for standing a thing up rather than
declaring it done when the suite is green.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-