Skip to content

fix(refs): resolve unjoined-but-public channels by name#249

Merged
lmjabreu merged 2 commits into
mainfrom
lmjabreu/channel-ref-finds-public-channels
May 28, 2026
Merged

fix(refs): resolve unjoined-but-public channels by name#249
lmjabreu merged 2 commits into
mainfrom
lmjabreu/channel-ref-finds-public-channels

Conversation

@lmjabreu
Copy link
Copy Markdown
Contributor

Summary

  • resolveChannelRef's name branch only called client.channels.getChannels({ workspaceId }), which is membership-scoped and excludes public channels the current user hasn't joined.
  • Effect: tw channel <action> "Public Channel I Haven't Joined" fails with CHANNEL_NOT_FOUND, while id:N and URL refs work because getChannel(id) isn't membership-filtered.
  • Fix merges getChannels with workspaces.getPublicChannels and dedupes by id. Same shape as src/commands/channel/list.ts:177-180 already uses for the --scope public path.

Why this exists

The bug was first suspected to be "active vs archived" — that turned out to be wrong: getChannels({ workspaceId }) already returns both states (Twist's API is tri-state — see list.ts:163-169). The real axis is joined vs unjoined, which I confirmed live and which also holds for non-admin users (getPublicChannels is not admin-gated). Scale check in Doist workspace 1585: 998 public channels, 829 of which I haven't joined — so the bug bites in production, not just edge cases.

Test plan

  • npm run lint — clean
  • npm run type-check — no errors in refs.ts / refs.test.ts (pre-existing @doist/cli-core export errors elsewhere on main are unrelated)
  • npx vitest run src/lib/refs.test.ts71/71 pass (4 new tests: unjoined-public exact match, unjoined-public substring match, dedup of joined-and-public, ambiguity across joined+public)
  • Live smoke in workspace 1767 (Testing Environment), against a fresh test channel — no existing channels touched:
    1. tw channel create tw-cli-resolver-smoke-<ts> --workspace 1767 → id 837258
    2. Left the channel via SDK removeUser
    3. tw channel archive "tw-cli-resolver-smoke-<ts>" --workspace 1767{ id: 837258, archived: true } ✅ (previously: CHANNEL_NOT_FOUND)
    4. tw channel delete id:837258 --workspace 1767 --yes → cleaned up

Smoke required #246's channel create/archive/delete commands to exercise an action on the resolved channel; they were checked out temporarily and reverted after.

Out of scope

🤖 Generated with Claude Code

resolveChannelRef's name branch called client.channels.getChannels({ workspaceId }),
which is membership-scoped and excludes public channels the current user hasn't
joined. Acting on such a channel by name failed with CHANNEL_NOT_FOUND while the
id: and URL forms worked, because getChannel(id) isn't membership-filtered.

Merge getChannels with workspaces.getPublicChannels and dedupe by id so joined
and unjoined-but-public channels resolve consistently. Same shape as the
--scope public path in src/commands/channel/list.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@doistbot doistbot requested a review from Bloomca May 23, 2026 08:27
@scottlovegrove
Copy link
Copy Markdown
Collaborator

@doistbot /review

Copy link
Copy Markdown
Member

@doistbot doistbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR successfully expands name-based channel resolution to include unjoined public channels, fixing a frustrating gap in how the CLI handles larger workspaces. The approach is solid and well-tested, ensuring users can interact with any public channel regardless of their membership status. A few refinements are noted regarding aligning the deduplication logic with existing codebase patterns, deferring the public channel API fetch until necessary to reduce overhead, and tweaking the test suite to better prove deduplication without hardcoding the underlying fetch strategy.

Share FeedbackReview Logs

Comment thread src/lib/refs.ts Outdated
Comment thread src/lib/refs.ts
Comment thread src/lib/refs.test.ts Outdated
Comment thread src/lib/refs.test.ts
- Switch from Map-based dedup to the Set<joinedId>.filter() pattern already
  used in src/commands/channel/list.ts so the two name-merge sites stay in
  step.
- Drop call-shape assertions from the exact-match test; result coverage is
  enough and call expectations would lock in the fetch strategy.
- Switch the dedup test to a substring query. matchByName's exact-match
  branch returns on the first .find, so an exact query passes whether dedup
  works or not. A substring query exercises the partial-match counting and
  fails without dedup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@Bloomca Bloomca left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code change looks good. I do agree that it can add a bit of noise (in general I assume most of the time we want to look at joined channels), but let's try and see.

@lmjabreu
Copy link
Copy Markdown
Contributor Author

Thanks @Bloomca. Agreed it's worth watching. If the substring noise shows up on the bigger workspaces, the lazy-fetch fallback doistbot suggested (joined first, widen to public only on a miss) is the natural next step. Merging.

@lmjabreu lmjabreu merged commit 74ad85a into main May 28, 2026
5 checks passed
@lmjabreu lmjabreu deleted the lmjabreu/channel-ref-finds-public-channels branch May 28, 2026 06:15
doist-release-bot Bot added a commit that referenced this pull request May 28, 2026
## [2.44.2](v2.44.1...v2.44.2) (2026-05-28)

### Bug Fixes

* **refs:** resolve unjoined-but-public channels by name ([#249](#249)) ([74ad85a](74ad85a)), closes [#246](#246)
@doist-release-bot
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.44.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

scottlovegrove added a commit to Doist/comms-cli that referenced this pull request May 28, 2026
* fix(refs): resolve unjoined-but-public channels by name

resolveChannelRef name-branch only called getChannels (membership-scoped),
so `tdc channel <action> "Public Channel I Haven't Joined"` failed with
CHANNEL_NOT_FOUND. Merge with workspaces.getPublicChannels and dedupe by
id — same shape as src/commands/channel/list.ts.

Ports Doist/twist-cli#249.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: defer getPublicChannels + harden dedupe test

- Skip workspaces.getPublicChannels when an exact match already exists in
  the joined list. Common case (user types channel they're in) avoids the
  workspace-wide call.
- Dedupe test now uses distinct object instances with the same id, so it
  proves dedupe-by-id rather than reference equality.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants