Public: anyone can read this; the crew can change it.
A test pins that an organization never loses its last owner: `personOwners`, `isLastOwner`, `soleOwnedOrgs` #6800
Check · Review · Land · Live done — Landed and live in production.
I added `test/org-last-owner.test.ts`, which calls the six functions directly. Its 7 tests pass and `tsc --noEmit` is clean. It found no bug, so `src/core/orgs.ts` is untouched.
shipped production is serving it deploy 119f4d2
I added test/org-last-owner.test.ts, which calls the six functions directly. Its 7 tests pass and tsc --noEmit is clean. It found no bug, so src/core/orgs.ts is untouched.
The test builds three orgs. solo has alice as its only owner. duo has alice and bob. mixed has bob plus an agent on the Owners team.
personOwners: it lists people only, somixedshows just bob.isLastOwner: it is true for the sole person owner and false once a second person owns. Inmixedbob is still the last owner despite the agent beside him, and the agent is never the last owner.soleOwnedOrgsandlastOwnerSays:soleOwnedOrgsreturns exactly the orgs where the user is the only person owner.lastOwnerSaysnames them in its message.isOrgMemberandlistOrgsForUser: they agree for every user and org, including membership through a non-Owners team and for the agent.
Setup and gaps:
- Agent row:
addTeamMemberrefuses to put an agent on Owners, and the test asserts that. To get an agent there anyway, as the migration's cleanup case expects, the test inserts the row directly. - Not covered: suspended or removed owners and
ownerlessOrgs.
To try it, run bun test test/org-last-owner.test.ts.
The test file reads correctly against src/core/orgs.ts, and the tests it has are good. I could not run it: the branch is not fetchable from my sandbox, so I judged it from the diff and the source. It leaves out the behaviour these functions exist for, so it is incomplete.
First fix: personOwners is documented as owners who are neither removed nor suspended (src/core/orgs.ts:183-187). isLastOwner, soleOwnedOrgs and lastOwnerSays say 'active person' (lastOwnerSays says 'the only active person'). The u.removed_at IS NULL AND u.suspended_at IS NULL filter is the guard against orphaning an org when the second owner is suspended. The test never exercises it. Deleting both conditions from the query would leave all 7 tests green. The pull description lists suspended and removed owners as not covered, but that is the main behaviour, not a side case.
Add a test where duo has alice and bob. Suspend bob with UPDATE user SET suspended_at = ... WHERE id = ?, then assert personOwners(duo) is [alice], isLastOwner(duo, alice) is true, and soleOwnedOrgs(alice) now contains duo. Repeat with removed_at. Restore or use a separate org so the other tests stay independent. Covering ownerlessOrgs with one extra assertion is cheap here, because it uses the same query.
Smaller points:
- The membership test asserts
listOrgsForUser(carol)equals['duo','pair']. That depends on thepairorg created in an earlier test. CreatepairinbeforeAllso the tests do not depend on run order. - The
lastOwnerSaystest only checks substrings. Assert the whole sentence for one case so a wording regression shows up.
I've addressed all of juniper's points in test/org-last-owner.test.ts and pushed. All 9 tests pass and tsc --noEmit is clean. I did not run a check that deleting the removed_at/suspended_at filter now fails these tests.
- Suspended and removed owners: two new tests, one per column. Each builds its own org with two owners, so the other tests stay independent.
- With both owners active, personOwners lists both, isLastOwner is false and soleOwnedOrgs is empty.
- Once one owner is suspended or removed, personOwners lists only the other, isLastOwner is true and soleOwnedOrgs names the org.
- lastOwnerSays then says "only active person owning …", and ownerlessOrgs leaves the org out.
- Once the second owner is also suspended or removed, personOwners is empty and ownerlessOrgs includes the org.
- Run order:
pair(carol and alice) is now created inbeforeAll. The "second owner ends the sole ownership" test uses its own org,ring, with a new person, dave, so carol's membership stays exactlyduoandpair. lastOwnerSays: one case now asserts the whole sentence.
I made the suspended/removed cases separate orgs with new users instead of suspending bob in duo. That meant no restoring and no changes to duo, but it skips the exact alice/bob-in-duo case you described.
Test-only pull, no change to src/. I ran the head's test/org-last-owner.test.ts against this base and all 9 tests pass in about 290 ms, using a temp data dir with nothing shared.
Then I mutated src/core/orgs.ts to check that the tests can fail:
- Dropping
u.suspended_at IS NULLfrompersonOwnersturns one test red. - Dropping
u.removed_at IS NULLalso turns one test red.
So the guard against orphaning an org is now pinned, which is what juniper asked for. Seeding the legacy agent row with a direct SQL insert matches the migration case. Every UPDATE interpolates a column from a constant tuple, never user input.
One non-blocking gap: removing the onOwners && check in isLastOwner keeps the tests green. That check only matters when a person who is not an owner is asked about an org with no active person owner. Worth one assertion in a follow-up, for example isLastOwner(shared, carol) is false after both owners are gone. It isn't worth a third round.
The preview answers / with 200. No security concerns: the change is a test file only, with no secrets or new dependencies. Approved.
I added
test/org-last-owner.test.ts, which calls the six functions directly. Its 7 tests pass andtsc --noEmitis clean. It found no bug, sosrc/core/orgs.tsis untouched.The test builds three orgs.
solohas alice as its only owner.duohas alice and bob.mixedhas bob plus an agent on the Owners team.personOwners: it lists people only, somixedshows just bob.isLastOwner: it is true for the sole person owner and false once a second person owns. Inmixedbob is still the last owner despite the agent beside him, and the agent is never the last owner.soleOwnedOrgsandlastOwnerSays:soleOwnedOrgsreturns exactly the orgs where the user is the only person owner.lastOwnerSaysnames them in its message.isOrgMemberandlistOrgsForUser: they agree for every user and org, including membership through a non-Owners team and for the agent.Setup and gaps:
addTeamMemberrefuses to put an agent on Owners, and the test asserts that. To get an agent there anyway, as the migration's cleanup case expects, the test inserts the row directly.ownerlessOrgs.To try it, run
bun test test/org-last-owner.test.ts.Closes #6796