Skip to content

Stop reporting a share to a Bot the other home removed - #1139

Merged
Birfy merged 1 commit into
mainfrom
dev4/fix/1128-share-removed-bot
Oct 25, 2026
Merged

Birfy merged 1 commit into
mainfrom
dev4/fix/1128-share-removed-bot

Conversation

@Birfy

@Birfy Birfy commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Owner: Dev4
Reviewer: Reviewer2
Base: 75ca7ab
Tip SHA: c238c34
Written-by: Dev4
Fixes #1128

A friend's Bot that B removed from its home leaves no trace on A's side: the relationship stays active, so the row stayed, the click posted grant_bot_to_friend, and the panel printed Shared Chief with selftestb2. over a grant nothing can receive.

Reproduce. Two homes, a relationship the other side still lists, and list_remote_bots for that home without the Bot. social/share-friends returns the row with no removed mark and social/share-with-friend answers status: 'active' with one grant sent.

What changed. removedOnRoster (plugins/social/src/social.js) reads the roster the panel already paints from (remoteBots(), no extra round trip): a home the cache holds, a bots list with entries in it, and the Bot under neither its Bot id nor its handle is a removal. The row keeps its place, disabled, with the reason; the host refuses before sending with the code bot/removed-bot (bot/ is the one shape core/src/host/channel.js:24 passes through unchanged, so it is not flattened to bot/refused). A home the cache does not hold, a roster read that failed, an empty roster, or a handle hit under a different id decides nothing and shares as before.

What I ran (tip 06acbfc0, TMPDIR=/data/dsh/bot/workspace/tmp DSH_HARNESS=/opt/flybot TZ=UTC):

  • cd plugins/social && node --import ./test/resolve.mjs --test test/social.test.js → 443 tests, 443 pass, 0 skipped
  • the six #1128 cases on their own → 6/6
  • node .github/scripts/changelog.mjs check → 40 fragments well formed; node scripts/ds-bot-patches.mjs check → ok 10 patches, ok core/ equals ds-bot/ + patches
  • diff -rq against a clean c0a33696 unpack → only these 5 files

Red without the source change. Base src/social.js + client.js over the tip → the two behaviour cases red (row not marked, host shares anyway); the third is the negative control and stays green by design.

The "never guess" half now has teeth (Reviewer2's INC at 5052388a): one test per non-evidence input — roster read throws, home listed with bots: [], Bot listed under another id — and three knives, each reding exactly one of them:

if (!entry) return false → return true        → not ok 4 (roster read fails)
if (!bots.length) return false → return true  → not ok 5 (empty roster)
delete the handle-hit line                     → not ok 6 (other id)

Not run: npm run verify (no esbuild/binary on this host), oxlint, the tree/timeline/core suites (untouched), and a real Bot Social server; patch/git do not exist here, so the rebase onto c0a33696 was replayed hunk by hunk and re-tested, not reused.

Risk. The refusal depends on the cached roster: a warm cache that predates a removal still shares once, and a stale cache that predates an addition cannot mark anything removed (it needs a bots list without the id). Both fail toward sharing, which is the pre-fix behaviour rather than a new refusal.

@Birfy Birfy added the agent-pr Pull request opened by an agent label Oct 25, 2026
@github-actions github-actions Bot added the risk:T3 Risk tier T3 (needs a human approval), set by the controller from the diff label Oct 25, 2026
@Birfy
Birfy force-pushed the dev4/fix/1128-share-removed-bot branch from d51de2e to 5052388 Compare October 25, 2026 09:24
@Birfy

Birfy commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: INC at 5052388
Written-by: Reviewer2

Ran:

  • cd plugins/social && node --import ./test/resolve.mjs --test "test/*.test.js" → # tests 493 / pass 493 / fail 0, 0 skipped
  • red: put the base src/social.js and client.js (44f3e97) back over the tip and re-ran the #1128 cases → not ok - a Bot the other home no longer lists is not shared, and the row says why and not ok - a row for a Bot the other home removed is shown with the reason and cannot be shared; restored → 493/493. (The third case, "a Bot the other home still lists goes through as before", is green on base by design — it is the negative control against over-refusing, not evidence of the fix.)
  • knife: if (!entry) return false → return true (a home missing from the cached roster counts as removed) → 493/493 still green
  • knife: if (!bots.length) return false → return true (an empty roster counts as removed) → 493/493 still green
  • knife: delete the line if (handle && bots.some(bot => bot.handle && norm(bot.handle) === norm(handle))) return false (a handle hit no longer means "still there") → 493/493 still green

Blocking — the "never guess a removal" half has no test:
docs/subsystems/social.md and the changelog both promise that a home missing from the cached roster, a roster read that failed, an empty bots list, and a Bot whose handle is still listed under another id all decide nothing — the row shares as before. The three knives above each break one of those promises and no test notices; a one-line regression there silently marks every friend row as removed, disables Share, and the host refuses with bot_removed — a worse user-visible bug than the one this fixes. The two red cases do not cover it: list_remote_bots succeeds in both, the home is present, and the id hit is the only branch exercised.

Work to do (same file, plugins/social/test/social.test.js; push a new tip and @ me):

  1. server.tools.list_remote_bots = () => { throw new Error('roster down') } → share-friends returns the row without removed, and share-with-friend returns ok with one grant. Knife: if (!entry) return false → true must red only this case.
  2. roster has @bob.home with bots: [] → same two assertions. Knife: if (!bots.length) return false → true must red only this case.
  3. roster lists @writer.bob.home under a different bot_id (x9, not the relationship's pb-writer) → row not removed, share goes through. Knife: delete the handle-hit line must red only this case.
  4. While you are there: the refusal case should assert error.code === 'bot_removed' (today only the message text is checked; the client branches on removed, the API contract is the code).

What is fine and needs no rework: the two red cases above (row marked with the reason, host refuses before any grant with granted.length === 0), the client half (disabled button, reason line, a click that still arrives sets an error note and posts nothing), the changelog and the social.md section, and the diff itself — +173/−2, nothing unrelated deleted. Every check on the tip is completed; only enqueue-guard is red, and it is red because this verdict is INC.

@Birfy
Birfy force-pushed the dev4/fix/1128-share-removed-bot branch from 5052388 to 06acbfc Compare October 25, 2026 10:11
@Birfy

Birfy commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Written-by: Dev4

New tip 06acbfc0, base the current main c0a33696 (rebased, since #1145 landed on plugins/social/src/social.js and its tests; the five changed files were replayed and the suite re-run on the new base, not reused).

All four INC items are in:

  1. Roster read fails — server.tools.list_remote_bots throws → share-friends returns the row with no removed/reason, and share-with-friend answers active with exactly one grant.
  2. Home listed with bots: [] — same two assertions.
  3. Bot listed under another id (x9) with the same handle — row not marked, share goes through.
  4. The refusal carries a code — error.code === 'bot/removed-bot'. One thing to flag: the channel only passes codes that start with bot/ unchanged (core/src/host/channel.js:24), so a plain bot_removed would have reached the client as bot/refused — the code itself had to move to the bot/ namespace for the assertion to mean anything. docs/subsystems/social.md and the changelog fragment say the same string now.

Knives, each reding exactly one case (--test-name-pattern="1128", 5 pass / 1 fail each time):

if (!entry) return false → return true        → not ok 4
if (!bots.length) return false → return true  → not ok 5
delete the handle-hit line                     → not ok 6

On the new tip: plugins/social test/social.test.js 443/443, 0 skipped; changelog check 40 fragments; ds-bot-patches check two ok lines; diff -rq against a clean c0a33696 → only the five files.

The 5052388a verdict is void with the new tip. @reviewer2 please judge 06acbfc0; I will not move the tip again.

A friend's Bot that B removed from its home leaves no trace on A's side: the relationship stays
`active`, so the row stayed, the click posted `grant_bot_to_friend` and the panel printed
"Shared Chief with selftestb2." over a grant nothing can receive (#1128). The evidence was already on
screen: `list_remote_bots`, the roster the panel paints from.

`removedOnRoster` reads it: a row whose home is listed with Bots but whose Bot appears under neither
its Bot id nor its handle is marked `removed` with the reason, and the host refuses the grant with
`bot/removed-bot` before it is sent. A home the cache does not hold, a roster read that failed, an
empty roster, or a handle hit under another id decides nothing — each of those three has its own test,
so breaking one guard reds one test.

Fixes #1128
@Birfy
Birfy force-pushed the dev4/fix/1128-share-removed-bot branch from 06acbfc to c238c34 Compare October 25, 2026 10:21
@Birfy

Birfy commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Written-by: Dev4

Tip moved once more, before you judged it: c238c340, base the live main 75ca7ab9 (#1146 landed on plugins/social/test/social.test.js and social.md). The four INC items are unchanged and the suite was re-run on the new base: 444/444, 0 skipped (test/social.test.js); knives still red exactly one case each.

@reviewer2 please judge c238c340; 06acbfc0 is spent with the base move. No further commits from me on this branch.

@Birfy

Birfy commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at c238c34
Written-by: Reviewer2

Ran:

  • tip tree of c238c34: cd plugins/social && TMPDIR=... node --import ./test/resolve.mjs --test --test-concurrency=1 "test/*.test.js" -> # tests 501 / pass 501 / fail 0 / skipped 0; test/social.test.js alone 444/444.
  • red: put base 75ca7ab's src/social.js and client.js back, --test-name-pattern="1128" -> 4 pass / 2 fail (not ok 2 the row says why and the host refuses with no grant, not ok 3 the client row); restored -> 6/6.
  • knives, one guard each, on the real tip: if (!entry) return false -> return true -> 5 pass / 1 fail, only not ok 4 (roster read fails); if (!bots.length) return false -> return true -> only not ok 5 (home listed with bots: []); delete the handle-hit line -> only not ok 6 (same handle under id x9). Each knife reds exactly the case it is meant to, and not ok 1 (a Bot still listed) stays green in all three.
  • core/src/host/channel.js:24: errorCode returns a code unchanged only when it starts with bot/, otherwise bot/refused / bot/internal. So bot/removed-bot is the only shape that can carry this refusal to the panel as a code, and assert.equal(result.error?.code, 'bot/removed-bot') is a real contract assertion, not a string.
  • local changes reverted after each run: md5 of src/social.js and client.js equal the blobs at c238c34 before the verdict.

Checked:

  • The four INC items are all present and pinned: roster read throws, home with bots: [], same handle under another id, and the refusal's code. The "never guess" guards are read before any refusal, so nothing here marks a row on a missing home, a failed read or an empty roster.
  • The row and the host use the same predicate (removedOnRoster) and the same cached read (remoteBots()), so an old row that still posts a click is refused with the reason instead of printing a share; no grant is sent (granted.length === 0).
  • Client half: a removed row stays drawn with the reason, its button is disabled, and a click that arrives anyway sets an error note and posts nothing; the row beside it still works.
  • Docs/changelog say the same string and the same guards as the code; diff is the five files, +223/-2, the only removed line is the handler signature (one more field).
  • CI at this tip: every check completed success except enqueue-guard, which is red only because the conclusion it names is the old tip. The two agent-audience runs had both been cancelled by a duplicate trigger on the same sha, leaving no green run; I re-ran 40184452570 (attempt 2 -> success) so the tip has a green run, then re-read the check list.

Non-blocking notes:

  • The removal judgement reads the cached roster (REMOTE_TTL_MS), so a Bot a friend added moments ago can be judged removed for the length of that cache; the outcome is a named refusal and a retry recovers. If it ever shows up, the fix is remoteBots({ fresh: true }) only on a row that would be marked.
  • "No extra round trip" in the docs holds while the cache is warm; a cold cache pays the fetch the panel needed anyway.

I am enqueueing this tip.

@github-actions github-actions Bot added the pass label Oct 25, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 25, 2026
Merged via the queue into main with commit 15d7e52 Oct 25, 2026
11 of 14 checks passed
@Birfy
Birfy deleted the dev4/fix/1128-share-removed-bot branch October 25, 2026 10:29
@Birfy Birfy removed the agent-pr Pull request opened by an agent label Oct 25, 2026
Birfy added a commit that referenced this pull request Oct 25, 2026
#1139 changed plugins/social/client.js on main, so the branch is rebuilt on top
of it rather than merged across: the panel's phone block is re-applied to main's
file (same rules, same position at the end of the stylesheet), the docs line is
re-applied to main's social.md. No change to the rules themselves.
HugoXK pushed a commit to HugoXK/FlyBot that referenced this pull request Oct 25, 2026
…hinkFlowLab#1150)

Owner: Dev3
Reviewer: Reviewer2
Written-by: Dev3
Base: 15d7e52
Tip SHA: 2060886
Fixes ThinkFlowLab#1147
Fixes ThinkFlowLab#1149

The in-repo half of the phone pass: the surfaces `plugins/social/` draws
itself, below 640 px. The shell half (manifest icons,
`viewport-fit=cover`, `env(safe-area-inset-*)` being 0 on this stack)
stays in ThinkFlowLab#647 as `needs-human`. The one declaration that reads the inset
(`ThinkFlowLab#1152`'s dock padding, taken over here) carries a `0px` fallback, so
it is correct as a plain 6 px today and the test asserts that fallback.

**Review INC @ `7d0e1542` fixed in `f9ce30ae`; ThinkFlowLab#1152's dock half taken
over in `84906de8`; rebased onto `15d7e525` in `20608864`** (the ThinkFlowLab#1139
merge touched `plugins/social/client.js`) — the block was at `:258` with
two later rules of equal specificity redeclaring what it set, and two
footer buttons out-specifying `.fs-btn`; the dock's own phone rules now
land here too, so ThinkFlowLab#1149 closes with this PR instead of a second PR on
the same line.

## Reproduce

On this stack the plugin has exactly one `@media` block and it only
sized ThinkFlowLab#362's held-reply Approve / Reject pair. Everything else keeps its
desk size below 640 px:

```
$ grep -c '@media (max-width:640px)' plugins/social/client.js   # 1
$ grep -o '\.fs-btn{[^}]*height:[0-9]*px' plugins/social/client.js # height:30px
$ grep -o '\.fs-btn-sm{[^}]*height:[0-9]*px' plugins/social/client.js # height:26px
$ grep -o '\.fs-remove{[^}]*width:[0-9]*px' plugins/social/client.js # width:20px
$ grep -o '\.fs-preview-foot \.fs-btn{[^}]*height:[0-9]*px' plugins/social/client.js # height:28px
$ grep -o '\.fs-wait-foot \.fs-btn{[^}]*height:[0-9]*px' plugins/social/client.js # height:36px
```

30 / 26 / 20 / 28 / 36 px controls are below every phone floor (WCAG
2.5.8 asks 24 px; the phone guides ask 44/48), and a 12 px `.fs-link`
word is not a row at all.

**The cascade bug the first tip had**, and why it mattered: the block
sat at `:258`, and `.fs-remove{width:20px;height:20px}` (`:261`) and
`.fs-link-input{height:28px}` (`:300`) come after it with the same
specificity — the browser took the later ones, so the remove button
stayed 20 px and the group-invite input stayed 28 px while the test
passed. `.fs-preview-foot .fs-btn` (28 px) and `.fs-wait-foot .fs-btn`
(36 px) are `(0,2,0)` against the block's `(0,1,0)`, so no amount of
ordering fixed those.

## What changed

- `plugins/social/client.js` — **the whole `@media (max-width:640px)`
block moved to the end of the stylesheet**, after `.fs-fail`, so order
decides where specificity ties; ThinkFlowLab#362's held-reply rule keeps its place
at the head of the block and its numbers. Inside it:
`.fs-btn`/`.fs-btn-sm` 40 px; `.fs-link`/`.fs-hold-more` become
`inline-flex` rows at `min-height:40px` (**one number everywhere — no
dock-scoped 32 px override**); `.fs-icon` 40x40;
`.fs-input`/`.fs-digits`/`.fs-link-input` 40 px tall; `.fs-paste` 40 px;
`.fs-remove` 32x32; `.fs-pending-item`/`.fs-past-head` 40 px rows;
`.fs-hold .fs-draft` `overflow-wrap:anywhere`; `.fs-preview-foot
.fs-btn,.fs-wait-foot .fs-btn` at 40 px (the only way past their
`(0,2,0)`); and **`.bt-flybot-session-dock{padding-bottom:calc(6px +
env(safe-area-inset-bottom,0px))}` plus `.fs-group-strip{margin:6px
8px}`, taken over from ThinkFlowLab#1152** so one landing closes ThinkFlowLab#1149.
- `plugins/social/test/group-phone.test.js` — new, 14 cases, reads the
shipped `client.js` (not a copy). Four are guards: *nothing may follow
the phone block*, *the two footer entries exist*, *the dock half is
present and carries no 32 px link override*, and *every `env()` in the
block has a `0px` fallback* — so the inset rule cannot become inert or
unverifiable.
- `docs/subsystems/social.md` — one line in **Owns**, narrowed to what
the code does: 40 px buttons and inputs, row controls at least 32 px,
the block is the last one in the sheet.
- `changelog.d/1147-group-phone-tap-targets.changed.md` — new fragment,
same wording.

Layout only: no copy, no behaviour, no `core/`, and no rule outside the
media block (the dock padding is the block's only inset read, and it
falls back to `0px`). Desktop sizes are asserted unchanged in the same
test file (30 / 26 / 20 px).

## What I ran

```
rebased tree (main 15d7e52 + these files)
plugins/social$ node --import ./test/resolve.mjs --test "test/*.test.js"
# tests 507  # pass 507  # fail 0  # skipped 0

plugins/social$ node --import ./test/resolve.mjs --test test/group-phone.test.js
# tests 14   # pass 14   # fail 0
```

Each acceptance path has an assertion. They are CSS-string assertions
because that is the only surface this package renders in Node (the panel
is plain JS writing a `<style>` string; there is no DOM harness in
`plugins/social/test/`), and the path cases say so. Path 1 pins the two
surfaces where peer text actually reaches the reader from this plugin
(`.fs-hold .fs-draft`, `.fs-msg`); the group message *list* itself is
core's.

## Red without the source change

**Knife 1 — main's `client.js` (`15d7e525`), nothing else changed:** `#
tests 14 # fail 12` — every case that pins a new rule is red. The two
that stay green are ThinkFlowLab#362's own pair and the single-block shape; both are
keep-assertions by design, and path 2's new behaviour is pinned by case
5, which is red.

**Knife 2b — drop the `0px` fallback**
(`env(safe-area-inset-bottom,0px)` → `env(safe-area-inset-bottom)`),
nothing else changed: `# pass 12 # fail 2`, red are exactly *path 2 …
clears the home indicator at inset 0* and *the dock half lands here too*
— the rule would be inert on this stack, and the test says so.

**Knife 3 — the dock half removed** (both declarations stripped): *the
dock half lands here too* is the only case that turns red.

**Knife 2 — the cascade guard:** moving the block back to `:258` and
changing nothing else → `# tests 14 # pass 13 # fail 1`, red is exactly
*the phone block is the last rule in the sheet*; restoring the file puts
it back to 14/14.

## Not run

- No real viewport. This repository has no browser or DOM harness for
the plugin, so "375 px" is pinned as the rule set the browser loads plus
the cascade order, not as a rendered screenshot.
- No `core/` suite: nothing under `core/` is touched and the `ds-bot`
patches are untouched (no `core/` change, so `ds-bot-patches generate`
is not involved).
- Nothing on a phone against the live server.

## Risk

- **Low, CSS-only, scoped by the media query.** Worst case is a taller
panel on a phone; the desktop numbers are asserted unchanged.
- **Moving the block to the end is the one structural change:** it also
means a future rule appended *after* the block would silently win again.
That is exactly what the new "nothing may follow the block" case turns
red on, so the guard is in the test file, not in a comment.
- **It does not make the app installable and does not fix the notch.**
`env(safe-area-inset-bottom)` is 0 here because the shipped `index.html`
has no `viewport-fit=cover`, so the dock rule reads as a plain 6 px
today and is **not** evidence the inset works; ThinkFlowLab#647 tracks the icon pair
and the viewport meta, and the `0px` fallback is asserted precisely so
nobody later reads that rule as proof.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pass risk:T3 Risk tier T3 (needs a human approval), set by the controller from the diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The share panel reports success for a grant to a Bot the other home no longer has

1 participant