Fifteen cases onto main, without the v1 column - #141
Merged
Conversation
T1972. The instruction that weeks start on Monday was ignored and every day came back numbered from Sunday, so anything grouped or sorted by the column is off by one day - plausible, self-consistent numbers.
T1970. The unit named in the formula was ignored and every answer came back in seconds, so a person reads a six-figure number where they expected a small one. Hours are checked against days, which holds whatever today's date is.
T1980. The trash is the promise that a delete is not final; rows were not being written to it, so there is nothing to notice until someone goes looking and the row is gone for good.
T1980. Written and run, green on both columns (run 32914315455). The shape is gone; the runner is not kept.
T1611. The time was thrown away and only the day compared, so everything on that day landed on the same side of the line - and the rows that come back look right, because they are all from the day that was asked about.
Develop answered the real filter with no rows at all (run 32915607313) while the parent answered with everything - an empty answer needs a control before it can be read as the product's. The same filter with a cutoff before every row now has to return every row.
Passing the filter alongside the read is answered with nothing on develop and with everything on the parent (runs 32915607313, 32916759955) - neither is the product filtering. The case now saves the filter on the view and reads the view, which is how a person uses one.
T1611. Two shapes, three runs, and the control never passed: the case never established the product's own way of asking, so nothing about the product was measured. The shape is gone; the runner is not kept.
v2 is what this lab guards: fixes land there, and a bug returning there is a regression someone must act on. v1 is asked the same cases as a reference — what does the engine our older customers are still on do with this — and nothing it reports fails a run. Reaching v1 takes more than the environment switch, which is the part that looks done and is not. Routing asks FORCE_V2_ALL first and the base's own v2 flag second, and the product stamps every base it creates as v2, so turning the switch off falls through to the second rule and still answers v2. Measured over 129 cases: with the switch off, not one observation differed from the v2 baseline, and the response header said why — reason `new_base` instead of `env_force_v2_all`. So the base is unstamped before the runner touches it. What that cannot do is make a base that was BORN on v1, which is what real v1 customers have. That gap is why v1 gates nothing, and why the column is worth having anyway: it is evidence to follow up, not a verdict. The engine is read live rather than captured at import — one process runs both blocks, and a constant would pin the second to the first engine and report its answers under the other's name. The v2 routing assertion is unchanged and still absolute; the v1 branch is its mirror, not a relaxation, because a v1 run answered by v2 is a fabricated column and throws just as hard.
Established by re-asking each v1 failure on a clean v1 base without the case's own fixture — the only method that separates a real v1 defect from a fixture v1 could never hold. Of 34 cases that answered differently on v1, these eleven were not answering the question the case asks. Two different things, which is why the field is a sentence and not a boolean. Six are features v1 does not have: required links, undo capture columns, field validation (an EMPTY table refuses the same column, so T5685 was never the ordering bug). Five are fixtures v1 cannot hold: v1's own API normalizes a bare user id into a full object, writes complete view column metadata, names the field on an ordinary unique violation, clears a multi-select cell without complaint, and clears its inbound link cells on a plain delete. Declared, never inferred. Reading "v1 said it does not support that" out of an error message fails open — a case that genuinely breaks, whose error happens to read that way, would be skipped forever and nobody would learn.
Under the guarded one, judged by nobody. Interleaving the engines would double the width of the thing people actually read and put cells that fail the run next to cells that cannot, which is the confusion this column has to avoid to stay welcome. The fail-closed contract still covers v2 exactly as before: every planned (case x commit) cell, one payload, a hole is a failure. The same problems on the v1 side collect in `referenceIssues`, which is printed and fails nothing — a reference nobody can afford to leave red is a reference nobody keeps. Two quieter things this had to fix. The engine goes in the payload FILE NAME, not only the payload: two engines writing one stem would leave one silently overwriting the other, and the report would then read a collision as a missing cell. And the Regression Track keeps taking v2 only — its Run Key is (run, attempt, case, commit) with no engine, so both engines would land on one row; widening the key would change what every historical row means, and the v1 column does not want a queryable history. Both engines run in one job. perf-lab gives each its own because overlapping measurements pollute each other; bug observations do not, and a second job would re-pay the whole bootstrap to save one app boot.
Import cases hand work to a queue, assert, and then tear down the space the queue is still writing to. The worker's completion handler lands afterwards, updates a table that is gone, and rejects with nobody to catch it — and vitest fails the file on that. Run 33055688034 is the whole argument: 247 tests passed, 11 skipped, none failed, every payload written, the report job's acceptance gate green, and the execute job red anyway on one such rejection from a v1 import case. The v1 pass did not create the race, it doubled the chances of losing it. Ignoring them costs no signal. A case's evidence only ever arrives through bugCheckpoint() and is written to its payload before anything may throw, and the payloads are what the report job judges. Vitest still prints them — the same bargain the v1 column takes: visible, gating nothing.
Sharing a job saved one bootstrap and was wrong in a way that only shows up on reflection: two passes against ONE database means the second engine runs on state the first one left, and the guarded column is what would have been reading it. Run 33056650739 came out clean anyway, which is exactly why this would have gone unnoticed. Split, each engine gets its own containers and its own database built from the commit's own migrations, and the two jobs run at the same time — the wall clock is one engine's, not two. It is also what teable-perf-lab has done all along, for the same reason plus its own: it restores a seed dump per engine so both start from byte-identical state. The execute matrix becomes commit x engine; the comparison keeps reading columns, not jobs, so the plan now hands it `commitPlan` — each commit once. Gating stays a property of the commit (which revision is the newest), and whether a reproduction can fail anything stays a property of the engine. While writing this down: perf-lab never needed the base unstamped because its cases run on the base the prisma e2e seed writes straight into the database, which never went through the product's create-base path and so carries v2_enabled = false. Every case here builds its own base through the API — the thing that keeps cases from disturbing each other — and the API stamps it.
These cases have the product create a SECOND base mid-run — an import, a duplicate, a share saved into a new one. That base is stamped v2 on creation and cannot be unstamped before its tables are built, so half the case talked to v1 and half to v2, and a mixture is not evidence. It showed: four of the eight were red or 💥 on v1, and one flipped between two runs that agreed on everything else. Unstamping the whole space before each observation would reach them and would be worse. That base's tables were built by v2; unstamping only makes v1 read a structure v2 wrote, which is the exact false premise that got T6626 and T6595 thrown out this week — and it would be automatic and every run. The reason string says "this method cannot ask v1", not "v1 lacks the feature", because they are different claims and only the first is true here. The day a base can be created on the engine that asked for it, these eight lines come out. v1 now answers for 110 of 129 cases. The card gained one line for it — the card is what people read, so a reference column absent from it is a column nobody sees — and that line says it decides nothing.
* Ask the field endpoint what the field editor already asks Four cases from the last week of teable-ee fixes, all reachable through the public API and all reproduced on their own fix's parent. Two of them are the same missing check on two column types: a totalling column created through the API asking for something its source cannot give - the sum of a tickbox, a count over a button - was accepted and written, leaving a column that read 0.00 on every row and whose settings opened with no source and nothing selectable. The editor never offers those combinations; the endpoint took them anyway. They stay two cases because they were fixed two days apart in two places, and each is green on the other's parent. The third: a filter box produces text, and the row-number column was the one numeric column whose comparison refused it, answering 500 to a view a person had just built and saved. The fourth: a conditional column reading a table in another base dropped which base, so reopening its settings drew an accessible table as one the person has no permission to see. Also three rows in the triage ledger for what was read and not taken: an unshipped share-view scope bypass, a connection timeout no request can provoke, and a fix whose pre-fix state is "v1 answered", which this harness reads as the case being unable to run rather than as the bug. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Say in the doc that v1 never had the row-number filter problem The reference column answered correctly on every commit in the matrix, which is worth recording: customers still on the older engine never saw this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Do not publish a table because someone copied one T6790: duplicating a table carried each view's sharing into the copy - the switch, the rules, and the password - and only minted a new address. The copy was a live public page from the moment it existed, openable by anyone who had ever been given the source's password, with nothing in the interface saying so. On the existing duplicate-shared-view runner, behind a new `assert` value, because it is the same setup and the same request as T6573 and differs only in what it reads off the copy - which is the point: this answer replaced that one. The checkpoint reads three things off every copied view, since the switch, the address and the rules are three separate ways a copy can be reachable, and then checks the source kept its own link, so "not shared" cannot have been reached by unsharing everything. A password is set on the source first; an inherited address is a page nobody opened, an inherited password is a page other people can already open. Reproduced on the fix's parent 9c97d777c: the copy came back enableShare true, with the source's password verbatim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep an any-of-these total inside the row it belongs to T7004: a total over linked rows narrowed to "status is todo OR status is doing". Written with OR, the condition escaped the link - the query stopped asking "and linked to this row" and totalled every matching row in the other table. The number it produced was a real sum of real rows, in the right units and the right order of magnitude, so nothing looked wrong. What the report leads with is the other half: a project joined to nothing at all that already shows a figure. The checkpoint reads both rows. The unlinked one must total nothing, which is what a person notices; the linked one must total exactly its own selected work, which is what says the condition is still applied - a build that fixed the scope by dropping the condition would pass the first test alone. The other table carries three kinds of row and the runner refuses a fixture missing any of them: linked and selected, linked and excluded, and selected but somebody else's. Without the third, a total that ignored the link entirely would still read correctly. Reproduced on 9c97d777c, which is before this fix: the project joined to nothing totalled 520, every todo and doing row in the table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Copy a base whose tables were named before the naming changed T6990: Postgres constraint names are unique per table, not per schema, and old bases carry a self-referencing key called fk___id on every table. Duplicating a base drops those keys, copies the rows and puts them back - but the step that listed them matched on the name and the schema and not on the owning table, so each table's list came back holding the other's rows. The drop ran twice for one table, the second found nothing, and the copy died there. Reported from production as an unhandled rejection in the browser, with the base half-made and nothing a person could change to get past it. The keys go in with SQL because nothing produces that name any more - it is what an old base has been carrying since before the convention changed, which is the same reason nobody hitting this could get out of it from the interface. The fixture counts how many tables carry the name before the checkpoint: with only one there is nothing to collide. The duplicate goes through raw axios with the status open. The generated client throws a bare "Internal Server Error" and drops the body, and the body is the part worth reading - a 500 that turned out to be something else would make this case red for the wrong reason. On 168f206df it reads: Raw query failed. Code: `42704`. Message: `ERROR: constraint "fk___id" of relation "tblivzjntazg6wE9b1W" does not exist` which is the Sentry error the issue was filed on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Record that v1 carried the copied share state too The acceptance matrix answers red on both pre-fix columns for the v1 reference, which matches the issue's reading of the legacy duplicate path and contradicts what a local run of the same commit said. Both are written down; which of the two the harness should be trusted on is a separate question. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Ask for the largest of a borrowed list and get a number T7099: a column only becomes a list by borrowing across a one-to-many, and a conditional total over such a column had been taught to look inside the list for sum and average but not for largest, smallest, all-of and any-of. Those four went straight at the stored list, which the database refuses - there is no largest of a list - so the column could not be made at all. Sum over the same source builds fine, which makes it look like something about the field rather than about the function. Asking for the total sits inside the checkpoint, because asking is what fails. Kept outside, the same refusal scores as "this case could not run here" rather than as the bug - which is what the first version of this runner did. Two things the case learned the hard way, both now written into it: The expected answers come from the list the product actually built, read back off the middle row, not from the rows this case seeded. Asserting against the seed is asserting against the case's own model of the product. Only the number half is covered. An unticked box does not reach a borrowed list at all - a pair of leaves, one ticked and one not, produces [true] - so all-of and any-of answer the same whether they work or not, and a case built on them would be green on every column. The runner takes only max and min. Reproduced on 46b7ae3cb: "Failed to backfill computed fields [max_of_the_borrowed_list]: function max(jsonb) does not exist", which is the commit message's own words. Also two triage-ledger rows: T7019, an incident about contention between replicas that one application against one database cannot reproduce, and T6988, whose fix's own e2e needs a stubbed snapshot loader and a hand-set doc version that this harness's over-the-wire subscription does not have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Count what the bracket in the condition says T7080: "orders for this customer that are either unpaid or flagged" is one condition with a bracket in it, and the interface builds it as a group inside a group. The fast path answering this kind of column read the outer conditions and dropped the bracket, so it counted every row matching the customer. The count is wrong upwards and looks ordinary - real rows, right customer, wrong ones. Nothing marks the column, and reopening it still shows the condition in full, bracket included, so there is nothing to see. A second column runs beside it with the same reference match and no bracket. It goes through the same fast path and is correct on both sides of the fix, so the checkpoint reads it FIRST: if the flat one is wrong too, the reference match itself is broken and the failure says so instead of blaming the bracket. The settle loop waits on that control rather than on the column under test - waiting on the latter would be waiting for the bug to go away. The runner refuses three fixtures, each for a stated reason: no row the bracket excludes (a bracket that excludes nothing counts the same either way), no host counting anything (a column stuck on zero passes for the wrong reason), and no host whose reference matches nothing (that row is what says the reference match still applies). Reproduced on c3d0fb6ac, with the control staying correct throughout: host-a withBracket 2, expected 1, rowsMatchingTheReferenceAlone 2 host-b withBracket 2, expected 1, rowsMatchingTheReferenceAlone 2 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Give back the distinct choices in the order the rows are in T7044: two wrong answers from one summary of a choice column. "Todo" then "Done" came back as "Done, Todo" - sorted, not in the order of the rows - and when both children said "Todo", the count of distinct values answered 2, which is the number of rows. Neither reads as a fault. A reordered pair of words looks like a choice the product made, and 2 is a number somebody would act on. What makes it findable is the other summaries on the same row: join and compact are correct, so the row shows "Todo, Done" and "Done, Todo" side by side. Those two ride along as the control, and the checkpoint reads them first - if they disagree with the rows, the whole summary is broken and this is not the distinct-values bug. Both phases run even when the first found something, and the failure carries everything at once. On a pre-fix commit the order is already wrong in phase one, so a checkpoint that stopped there would never reach the count - which only becomes wrong once two children agree - and half the report would be asserted and never demonstrated. Measured on 1c2da4254, both faults in one red: at first, the distinct choices come back as ["Done","Todo"], expected ["Todo","Done"]; after the edit, the count of distinct choices reads 2, expected 1 - which is the number of linked rows, so it is counting rows with joined "Todo, Done" and compacted ["Todo","Done"] correct throughout. The runner refuses children whose choices are already in alphabetical order: a summary that sorted them instead of keeping row order would look correct. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Record that v1 still gives back the sorted answer The acceptance matrix answers red for the v1 reference on every column, develop included: the fix is v2-only, so anyone on the older engine sees both faults today rather than having seen them once. The v1 column is a reference and never gates a run, so this is reported, not enforced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Treat two linked records sharing a name as two records
T7082: two records are the same record when they have the same id, not when
they happen to be called the same thing - and nothing stops two rows sharing a
name, because a name is a value someone typed. A summary of the distinct linked
records compared what it displayed rather than what it held, so two different
records both called "Same" collapsed into one and a real linked record left the
answer.
It leaves quietly. The column is not marked, the result is a plausible list of
names, and the only way to notice is to count it against the summary beside it
that keeps everything. Whatever reads the column next is short by one.
The checkpoint asserts no list of names. It asserts that the distinct summary
equals the keep-everything summary, because when every linked record is a
different record those two ARE the same answer. That invariant holds whatever
shape the cells come back in, which is not a hypothetical here: the issue was
fixed twice and the second commit changed exactly that shape.
Measured across all three states:
d09c75728 keeps three {id,title} distinct ["Same","Other"]
692c2b4b5 keeps three {id,title} distinct ["Same","Same","Other"]
develop keeps three titles distinct the same three titles
The middle row is why both commits are named in sourceCommits: after the first,
the identity is right and the two answers still disagree. A written-in list of
names would have been "correct" there and rewritten by the second commit - a
case following the product rather than holding it in place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Write down why T7070 cannot be asked here
The fixture builds and the write is refused, but in the wrong phase: the insert
raises it, while the fix repairs the deferred propagate. The commit's own e2e
reaches that second call site by draining an outbox inside the v2 test
container; this harness runs the Nest application, where a small fixture
computes inline and never gets there. Both link directions tried.
The attempt also turned up something that is not T7070 and is not fixed: on
develop, a base with a missing link key column refuses inserts into the table on
the other side outright, before any propagation. Recorded under the table so the
next pass does not rediscover it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Write down two more that cannot be asked here
T7066 was written and run twice against the fix's parent, green both times. Its
precondition is that the summaries exist before the row does, so the answer is
worked out during the write - built first as one call writing the row and its
links, then as the two writes the report's own steps describe. Neither
reproduced, and the T7044 sibling case is green on that parent too, measured, so
it does not cover this one either. The shape stays behind the runner's
whenTheRowIsWritten and alsoCheckAfterAnEdit config values so a third attempt
does not start from nothing.
T7047 reads through the v2 contract's own list endpoint rather than the public
record API, and most of what it changes is performance-shaped - skipping
count(*), paging by cursor instead of OFFSET - so much of it introduces the path
it repairs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Let a table whose formula joins people columns take a row
T7024: "everyone involved, listed once, separated by commas" over seven people
columns, written four functions deep. Each layer re-stated the whole of the
layer inside it, so the statement the database was asked to plan grew with every
one and reached megabytes. The row is recomputed inside the write, so nothing
came back: the page spun and the gateway gave up. The table could not accept a
row - not slowly, at all - and all a person could see was a timeout.
Making the formula column is inside the checkpoint. What grew is the statement,
and planning it is what fails, so it fails when the column is made as readily as
when a row is added: on the fix's parent 42c9ba98a the case never reaches the
write, because creating the column already answers
Unexpected unit of work error: Error: Client has encountered a connection
error and is not queryable
which is the message from the customer's own backend log. Built with the column
in setup - as this runner first was - that failure scores as "this case could
not run here", the one verdict that hides the bug.
The write carries its own time limit rather than being allowed to hang, for the
same reason: a request that never answers would run out the whole case. The
limit is generous on purpose. This is not a measurement of speed and does not
belong in the performance lab - the difference asserted is between an answer and
no answer.
Seven people columns because that is where the report was filed and because the
statement grew with the count; fewer may plan something large that still
completes, which would be green on both sides.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Tell a share link that its database is away, not that we broke
T6926: a space can be bound to a customer's own database, and that binding can be
switched off - revoked credentials, a retired connection, a migration part way.
The share link, the view and the permission are all still correct; there is
simply nowhere to read from.
What came back was an unhandled 500. Whoever holds a share link is usually
outside the company, with no account, nothing else to look at and nobody to ask.
A 500 tells them the product is broken and there is nothing to do about it. A 503
naming an unavailable database tells them - and anything watching the endpoint -
that the same page will work later.
The checkpoint asserts the status AND the code: a 503 that does not say why is
indistinguishable from any other outage, and being distinguishable is the whole
of the fix. A 200 is called out separately, because a share link answering
normally while its database is away would be worse than the bug.
The binding goes in with SQL - binding a space to another database is not part of
this observation, and a switched-off connection is not something a request can
ask for - and the fixture opens the link and requires a 200 first, or a 503
afterwards could just as well mean the share was never set up.
Reproduced on aa1f9f883: 500, code internal_server_error, carrying the raw
DataDbBindingNotReadyError from resolveSpaceDataDb, which is where the commit
message says it came from.
The separator in the T7024 case is now a plain comma. The customer's was an
ideographic one and this repository is English-only; what grows the statement is
the nesting, not the character, and the case was re-run on the fix's parent to
confirm it still reproduces.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Declare what the v1 column can and cannot say about these two
T6926 is skipped on v1, for a reason about this harness rather than the product:
the case makes its own space and base, and case-base.ts unstamps only the base it
manages, so a base born inside a runner is born on v2. The v1 run answered
"requested of v1 but v2 answered (reason=new_base)" - the harness refusing to
fabricate a reference column. Any runner that creates its own base inherits this.
T7082 is red for v1 on every column, develop included: both fixes are v2-only, so
a summary on the older engine still merges two records sharing a name and still
loses one. Reported, not enforced - the second case here to say that about v1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Make a column that picks by case and ends in linked records
T6980: "cost depends on where the cost comes from" - a manually entered figure
for some rows, a different figure for others, and otherwise whatever is linked.
The first two answers are numbers; the last is a list of linked records, stored
as a document rather than as a number.
The step merging the branches compared only the ones with a case attached. Those
agreed - both numbers - so it never looked at what the otherwise branch held. The
database was then asked to choose between numbers and a document in one
expression and refused, killing the column and the schema change it was part of.
Nothing in the interface says the last branch is a different kind of thing.
Two number branches, and the runner refuses fewer: their agreeing with each other
is exactly what stopped the merge looking further, and with one branch there is
nothing to agree with. The linked column is checked to hold a list before the
checkpoint - holding one value it would be the same kind of thing as the numbers
and there would be nothing to reconcile. The link is many-to-many because every
row needs the same list, which one-to-many will not allow.
Making the column is inside the checkpoint, since reconciling the branches
happens while it is built. Reproduced on f44a82cf8:
Failed to backfill computed fields [Cost]: CASE types jsonb and double
precision cannot be matched
The rows falling to the otherwise branch are read but not pinned to a value: what
a list of linked records renders as has changed before, and pinning it would tie
this case to something it is not about.
Also a ledger row for T7105, which is cost rather than behaviour - a request for
27 columns selected all 235, and answered with the same 27 either way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Write down two more dead-letter fixes this harness cannot reach
T6711 and T6904 both put their observation inside the background schema-operation
runner: whether a leftover table.import is marked dead or repaired, and whether a
computed task planned against a still-provisioning table is dead-lettered or
retried. Neither difference reaches an HTTP response.
The T7070 attempt already established by measurement that a small fixture here
computes inline and the deferred path is not reached, so these are recorded
against that finding rather than re-derived.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Make a second undo retry the step that failed, not the one before
T7038: undo moved the place it had walked back to BEFORE carrying out the step,
and never moved it back when the step failed. A failed undo therefore still
counted as done, so the next press started from behind it and reversed the step
before - one the person had not asked to reverse. Here that step is the row's
creation, so pressing undo twice deletes a row nobody asked to delete.
The failed undo is not the problem. It is honest and visible. The second press is
the one that quietly takes something else away, and that is what the checkpoint
reads: the row still there, and still holding the value the failed step could not
put back.
Measured, first press then second:
f44a82cf8 failed "must have a unique value" fulfilled one row left
develop failed "must have a unique value" failed, same message both rows
The first press is identical on both sides. The whole difference is the second.
A step fails to reverse for an ordinary reason: the column does not allow
duplicates, the value was changed away, and another row has taken the old value
since. That row is written on a DIFFERENT window id - the stack is keyed by it,
and writing it on the same one would put it on the history this case walks back
through.
This case covers one of the six risks the report lists. The other five are
concurrency and crash shapes that a single client against one process cannot
show, and the doc says so rather than letting the issue id imply coverage.
Written down in the doc because it cost a run: every write has to carry the
window id, the generated client takes no per-call headers, and an undo with
nothing on the stack answers "empty" - which the first version of the fixture
check read as "not fulfilled" and let through, green on a pre-fix commit against
an empty stack. The writes now go through raw axios and "empty" is rejected by
name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Write down T7057, and where the permission family actually stands
T7057 changes which fields a substring index covers, not what a search returns:
an all-field search over an uncovered field falls back to the unindexed path and
answers the same. Same reading as T6821.
The other half of this is a note rather than a rejection. Four uncovered fixes -
T6997, T7025, T7027 and T6944 - all wait behind one piece of setup that does not
exist here yet: the authority matrix on, a role that makes a field unreadable,
and a second user holding it. None of them is blocked by the harness; the matrix
is driven through public endpoints and a second signed-in user comes from the
same test utils runners already import. The note records the endpoints and the
worked example so the next attempt does not go looking for them, and says which
of the four is the best first customer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Build the authority-matrix fixture, and show a grid its reader cannot open
T6944: under the authority matrix a role can withhold one column, and the rest of
the table stays readable - that is the point of withholding a column rather than
the table. But a view remembers what it is grouped by and the page sends that
grouping with every request for rows, so asked to group by a column the reader
may not see, the server refused the request outright. The person got not a view
without its grouping but a view with NO ROWS, and a message naming neither the
column nor the grouping. An administrator opening the same view sees everything,
which is the worst shape a support conversation can have.
Reproduced on 12407c409 with the customer's own words:
400 {"message":"Group references a field that is not readable",
"code":"validation_error"}
Most of this commit is framework/authority-matrix.ts, which stands up the three
things together - the matrix enabled on a base, a role that withholds something,
a second signed-in person holding that role. Three more reported bugs need
exactly that, so it lives beside fixture-db rather than inside this runner, and
is setup-only for the same reason: asking for it inside a checkpoint throws. The
URLs are literals, not imports from the enterprise client, because a case runs
against revisions weeks apart and a moved export would break it everywhere
instead of failing honestly on the commit that moved it.
sourceCommits names a4c8c3396 alone. Two commits carry this issue id and the case
is red on BOTH sides of the earlier one - measured on 12407c409 and on 7bc91231d,
which is after it - so f70f0d508 is recorded in the ledger as the half this case
does not settle rather than claimed here.
The control is the same request without the grouping, which must return every
row: it says the person can read this table, so the refusal is about the grouping
and not about them. The fixture also requires the withheld column to be absent
from what comes back, or grouping by it would be an ordinary request.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Attribute the grouped-grid fix to the commit that actually made it
The acceptance matrix caught a wrong claim. The case was written naming
a4c8c3396, one of the two commits carrying T6944's id, and the matrix answered
red on f44a82cf8 - which is after it. Bisected one commit at a time: red on
12407c409, 7bc91231d, f44a82cf8 and 8fd1e28b9; green at 2ae77481c, which carries
T6997's id, "evaluate v2 reads over masked values".
So sourceCommits names 2ae77481c while bug.issue stays T6944, because T6944 is
what a person reported and this is that person's symptom. Both T6944 commits are
now ledger rows for the half this case does not settle, with the measurements
that say so.
The doc records the mistake as well as the correction: anything a case claims
about which commit fixed what has to come from a column, not from an issue id.
Also noted that T6980 is red for v1 on every column including develop - the third
case here to find the older engine still carrying a fix that only landed on v2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
No cases in this change - only what was learned by failing to write them. Each row carries the shapes tried and what they measured, so the next pass spends its time somewhere else. Structural, and unlikely to change: T6893 and T6694 move handlers onto v2, so their pre-fix state is "v1 answered", which assertServedByV2 reads as the case being unable to run; T6895 replaces a timing-out POST with a stream, so no request both sides answer; T7105 is cost rather than behaviour; T7057 changes index coverage, not results; T7035 and T7059 are browser fixes. Inside the background runner, where nothing this harness reads can see the difference: T6711, T6904 and T6982. T6982 was written and run twice, and turned up that a table carrying provision_state = 'pending' is not out of reach here - the assumption both shapes rested on. Written and run, and still not reproduced: T7061 (twice - whether a computed plan splits into stages is the planner's decision and a case cannot ask for a split), T6925 (a third shape, which found that a formula mixing a date branch and a text branch is accepted, unflagged, and computes nothing on develop), and T6500 (twice - the field-conversion path T6925 named as untried, now tried). Two rows note fixes that are ready to write the day they ship: T7027, whose permission-filtered response still carries ids of what was filtered away, and the server-side half of 38d0e067e, which closes two comment permission paths that are currently open. The fixture both need already exists. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…#139) * Let a role that grants archiving actually grant it T7025: giving somebody an authority-matrix role also puts them in the base, and puts them in as a Viewer. A Viewer, by their base role alone, may not archive anything; the role says they may. Two gates read those two answers and the base role was checked first, so the answer was always the Viewer's and the role's grant never got a hearing. The refusal named neither the role that grants the action nor the thing that withholds it, while the settings screen showed everything correctly configured - because it was. Reproduced on 8fd1e28b9: 403 {"message":"not allowed to operate record|archive on tbl…", "code":"restricted_resource"} The person arrives THROUGH THE ROLE ALONE, with no invitation to the space first. That is the whole shape - it is what makes them a Viewer. Invited as an editor, their base role would permit archiving on its own, the gate that reads it first would answer correctly by accident, and the case would be green on both sides. That is a new `join` option on framework/authority-matrix.ts: how somebody gets into a space is not a detail here, it is the bug. The checkpoint also asks to archive a row the role does NOT reach, and requires a refusal. That half never goes red - being refused everything is also being refused this - and it is not there to catch the reported bug. It is there because the cheapest wrong fix is to stop checking, and that fix passes the first half. The report describes a grouped and sorted view; the fix says that was incidental and the fixture leaves it out. Also a ledger row for T7027: its fix is unshipped and its symptom is a permission-filtered response still carrying ids of what was filtered away. Same call as T7065 - the fixture it needs now exists, so it is ready to write the day it ships. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Write down that T6982's repair is not visible from outside Written and run twice, green on the fix's parent both times. A real settings change was made and the job it recorded was rewritten by fixture-db into the interrupted shape the commit describes - pending, metadata_pending, no last_error, old enough for the keeper to claim - with table_meta.provision_state set to pending alongside it. The first shape asked that the table go out of reach and then come back. The second dropped that and asked only that it read within two minutes. On both 28a55d9ac and develop the table read immediately either way, so neither shape tells the two sides apart. The first shape was also wrong in kind, and the ledger row says so: requiring the symptom before observing it lets a fixture confirm only itself. The second verifies what it wrote instead. Worth knowing on its own: a table carrying provision_state = 'pending' was not out of reach in this environment, which is the assumption both shapes rested on. What the fix changes - whether the keeper repairs the job or marks it dead - lives in the job's own row, and the commit's e2e reads it through Prisma while driving the runner in process. Same seam as T6711 and T6904. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Write down four more that cannot be asked here, and T7061 T6893 and T6694 move handlers onto v2, so their pre-fix state is "v1 answered", which assertServedByV2 reads as the case being unable to run rather than as the bug - same as T7067. T6895 replaces a POST that timed out at the gateway with a stream, so there is no request both sides answer. T7061 was written and run twice, green on the fix's parent both times: first with a conditional lookup and one formula, then with three more formula steps after it, because the commit says the fault needs a stage that runs the lookup edge while the plan still has formula steps left over. Whether a plan splits into stages at all is the planner's decision - d74a81ab1 explicitly keeps small chains in one stage - and a case cannot ask for a split from outside. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Let a role that permits commenting actually permit it T7034: the same fault as its sibling, one action over. Giving somebody an authority-matrix role puts them in the base as a Viewer, and a Viewer by their base role alone may not comment; the role says they may. Commenting was gated on the base role alone, so the grant never reached the write. The person can see the record, open it and read the thread, and cannot add to it - told only that the resource is restricted, which names nothing they could change, because from the settings screen nothing is wrong. Reproduced on 855da66cf: 403 {"message":"not allowed to operate record|comment on tbl…", "code":"restricted_resource"} The checkpoint reads the thread back as well as the status: a write that answered and left nothing behind is the same silence with a friendlier status. It then comments on a row the role does NOT reach and requires a refusal - that half never goes red, and it is there because the same change also had to bound commenting by the role's row conditions, which the base-role path never applied at all, so a fix that stopped checking would pass the first half. Second customer for framework/authority-matrix.ts, and the shape it shares with record/archive-a-row-your-role-says-you-may is now recorded in both docs: a matrix grant that never reaches the write because a base role answered first has been found twice, in archiving and in commenting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Add a third failed shape to T6925, and what it turned up The ledger already carried T6925 with two shapes tried and green, and named the untried path: the computed backfill a field conversion runs, not the pass that fills a newly created column. This third shape went at the cause the commit names - an IF() whose branches are a date and a word, so the column is typed as text and every branch is trimmed - and it does not express the behaviour either. On develop that column is created without error and computes nothing at all: no date on the row whose checkbox is ticked, and not even the word on the row whose checkbox is not. With no correct answer on the fixed side, there is nothing for a pre-fix column to differ from. The conversion path is still untried. That last observation is written under the table as its own note. It is not T6925 and is not claimed as a fault - mixing a date branch with a text branch may simply not be supported - but a formula that is accepted, is not flagged, and answers nothing is worth somebody looking at, and the next person to try this shape will hit it in the first minute. Two things this cost, both worth remembering: check:source-commits caught that T6925 was already a ledger row before the case could ship, and an edit to the runner silently did not apply because prettier had rewrapped the line it matched on - the run afterwards was against the previous file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Try the conversion path T6925 named, and write down that it is still not it T6500 is the field-conversion backfill the T6925 ledger row records as untried, so this tries it: a number column with rows and a formula reading it, converted to text inside the checkpoint, then in a second shape converted back again - which is what the report describes people doing. Green on the fix's parent both times. Neither direction reproduces "operator does not exist: double precision = text". Something narrower decides whether the stored column and the freshly computed value end up different kinds, and a formula that simply echoes the column is not it. The production reports name six computed fields across two tables in one base, so the shape may need a chain rather than one formula. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Read a view that says both things about a column T6597: which columns a view shows has been recorded two ways over this product's life - an older note saying whether a column is SHOWN, and the current one saying whether it is HIDDEN. Views made long enough ago carry both, and no request writes that shape any more; it is simply what is in the table. Read back, the two were passed through side by side, and what a view says about a column is checked on the way out. So the request for the table's views failed - every view at once, not one column in one of them. Reproduced on c9aef116b: 500 {"message":"Invalid View projection","domainCode":"view.invalid_projection", "issues":[{"code":"unrecognized_keys","keys":["visible"]}]} The checkpoint asks for the views at all, and then that the entry has been settled into one answer - older note gone, current one kept, order and width intact. The entry carries those two so a settled entry can be told from an emptied one: dropping it whole would satisfy "the older note is gone" while losing what the view needs. The stored notes go in with SQL because nothing writes that shape any more, which is also why a base carrying it cannot get out from the interface, and the fixture reads them back first to require the older key is really there. The feature name in the engine assertion was a guess and the assertion refused it: GET /table/{tableId}/view answers as getViews, not createTable. That is the check doing what it is for - a case pinned to the wrong feature watches code its bug does not live in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Record that v1 answers this one the other way round v1 is red on every column including develop, and not for v2's reason: it does not fail the request, it answers 200 and hands back the entry exactly as stored, both notes included. So on the older engine this data never caused an outage and was never settled either. The two engines fail this case in opposite directions, which is worth saying before anyone reads the v1 column as "v1 was affected too". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Give a column its place when the stored notes give it none T6545: the notes a view keeps about each column say where it sits and how wide it is, and views made long enough ago have entries with a width and no position at all - a shape nothing writes any more. Nothing filled the gap in on the way out, and what a view says about a column is checked there, so the request for the table's views failed: every view at once, not one column in one of them. Reproduced on 66919acae with Invalid View projection and expected "number". Second shape on the runner the T6597 case introduced, differing only in the `legacy` config value: same fixture, same observation, two shapes of old data. The two were fixed three days apart, this one first, and what differs is which part of the entry the check rejects - an unrecognised key there, a missing number here. The doc first said this one "comes back looking fine and is wrong". The run said otherwise and the doc now says what was measured. The checkpoint name lost its reference to the other shape's keys for the same reason. The width is asserted as well as the position: filling the gap by replacing the entry would satisfy "it has a position" while throwing away the only thing the old notes actually said. The position is compared against the column's own index rather than a number written into the case, so nothing here encodes a particular default. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Give a shared form's picture one address instead of two T6604: where a form's picture lives is stored as a short path, and the address a browser can fetch is worked out from it when the form is read. A shared form is read through two layers and both worked it out, the second over the first's answer, so what came back was one address with another on the front of it. The person who opens the link sees a broken picture while the same form inside the product looks right - inside, it is read through one layer only. Measured on d961a7a03: http://127.0.0.1:PORT/api/attachments/read/public/http:/127.0.0.1:PORT/api/ attachments/read/public/form/e2e-lab-cover-image The first version of this case counted "http://" and passed on both sides. Joining one address onto another leaves the inner one with a SINGLE slash, so that count found one address in a string plainly holding two. It counts the scheme now, and the doc says why - the wrong reading was invisible until the value itself was printed. Addresses are counted rather than compared against an expected string: what the storage prefix is depends on how the instance is deployed, and pinning it would make this case about configuration. Ending at the stored path is what says the address still points at the right thing. The stored value has to be a short path and the runner refuses an address - an address is exactly what the fix passes through untouched. Before the checkpoint the form is read from inside the product as the control, where the same view is read through one layer and comes back right. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Write down two more that change cost, not answers T6890 replaces a leading (expr IS NULL) sort key with Postgres's own NULLS FIRST/LAST; the commit and the issue both say the order stays identical to v1's, and what changes is the length of the sort list and whether an index can serve it. T6669 stops producing a search hit index by re-running the whole v1 pipeline for ids it then throws away; the index itself is the same on both sides. Neither has anything for a case to tell apart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Record that v1 leaves this entry unplaced too Same reading as the sibling case: v1 answers 200 and hands the entry back exactly as stored, so on the older engine this data never caused an outage and was never filled in either. Red on every column of the acceptance matrix, develop included. Reported, not enforced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The v1 column is gone. Every case now runs on v2 only, which is what main has been doing all along, and the two-dimension question this merge opened - engine crossed with computed-update mode - disappears with it. What went, and what it cost: - framework/engine.ts, case-base.ts, run-bug-case.ts, the run-plan scripts and the comparison model all come from main. `skipV1` no longer exists, and the five cases that declared it lost that line. Three of those reasons were about the product rather than the harness - conditional totals are a v2 column type, the base-duplicate fix only touched the v2 route - and they moved into the case docs so the judgment survives the field. Eight of the twenty-three cases on this branch were already covered on main and are deleted here rather than merged: - T1970 and T1972 were byte-identical to main's y556 and y563 apart from the id. - T7025, T7034, T7046, T7087, T7080 and T7082 are covered by main's y402, y404, y470, y486, y471-y472-y478-y492 and y465, which are equal or better: y465 exercises all four relationship directions where this branch's version tried one, and y402 asserts the archived row leaves the view and the count follows. Fifteen cases remain, none of them duplicates. T6944 stays alongside main's y386 deliberately: y386 drives a browser and asserts the client stops sending an unreadable group field, this one asserts the server tolerates it when something else does - an API caller, an older client. Two ledger rows are dropped for the same reason as the cases: main's T7066 row is more thorough than this branch's, and main covers T7035 with a browser case, so this branch's "cannot be asked here" row for it is simply wrong. Case names keep their current form; renumbering is somebody else's pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the 15 cases and 23 triage-ledger rows from this branch onto main, and
drops the v1 reference column on the way.
The v1 column is gone
Every case runs on v2 only, which is what main has been doing all along.
framework/engine.ts,case-base.ts,run-bug-case.ts, the run-plan scripts andthe comparison model all come from main;
skipV1no longer exists.That also closes a question this merge opened and nobody wants to answer: main
added a computed-update mode dimension (sync / hybrid) while this branch added
an engine dimension, and merging both meant deciding how the two cross. With
v1 gone there is one dimension again.
Five cases carried a
skipV1line. Three of those reasons were about the productrather than the harness — conditional totals are a v2 column type, the
base-duplicate fix only touched the v2 route — and moved into the case docs, so
the judgment survives the field that carried it.
Eight cases deleted rather than merged
They were already covered on main, and main's are equal or better:
T6944 stays alongside main's y386 on purpose. y386 drives a browser and
asserts the client stops sending an unreadable group field; this one asserts the
server tolerates it when something else does — an API caller, an older client.
The 15 that remain
Each was reproduced on a commit before its own fix and verified green on
develop; the acceptance matrices are linked from the commit messages.base-share/a-share-link-whose-database-is-away(T6926) ·base-share/a-shared-forms-picture(T6604) ·base-share/copy-a-base-whose-tables-share-a-key-name(T6990) ·field/a-cross-base-conditional-column-keeps-its-base(T7064) ·filter/a-row-number-filter-typed-into-the-box(T7071) ·formula/a-column-that-picks-by-case(T6980) ·lookup/an-any-of-these-total-stays-inside-its-link(T7004) ·lookup/distinct-choices-in-the-order-they-appear(T7044) ·lookup/the-largest-of-a-borrowed-list(T7099) ·record/add-a-row-to-a-table-that-joins-people-columns(T7024) ·table/a-duplicated-table-starts-unshared(T6790) ·undo/a-second-undo-after-one-that-failed(T7038) ·view/a-column-the-view-does-not-place(T6545) ·view/a-grid-grouped-by-a-column-you-cannot-read(T6944) ·view/a-view-that-says-both-things-about-a-column(T6597)All 15 were re-run together on
developunder main's framework after the merge:15 passed.
Ledger
23 rows added, two dropped. The two dropped are corrections rather than
tidying: main's T7066 row is more thorough than this branch's (it tried hybrid
computed updates and reversed physical row order too), and main covers T7035
with a browser case, which disproves this branch's "cannot be asked here" row for
it.
Names
Case files keep their current names. Renumbering to the
yscheme is a separatepass by whoever owns the test-case table.
🤖 Generated with Claude Code