fix: auto-follow new thread when parent channel is followed#391
fix: auto-follow new thread when parent channel is followed#391yujiawei wants to merge 1 commit into
Conversation
Web users creating a thread under a followed channel did not get the new thread auto-followed — it only appeared in the Recent tab, requiring a manual right-click to follow. The backend auto_follow_threads fanout only covers bot-created threads and is eventually consistent, so the web manual entry points can't rely on it. Add a best-effort maybeFollowNewThread helper invoked from all three thread-create entry points (ThreadCreate, ThreadCreateModal, ThreadPanel). After threadCreate succeeds it checks the sidebar follow snapshot (SidebarService.sync tab=follow, target_type===2) and, only when the parent channel is already followed, calls FollowService.followThread with the new Thread.channel_id, then emits sidebar-reload. Failures are swallowed so they never block the create feedback. Fixes #292
Jerry-Xin
left a comment
There was a problem hiding this comment.
The PR is relevant to octo-web, but it misses an existing manual web thread creation path, so the fix is incomplete.
🔴 Blocking
- 🔴 Critical:
packages/dmworkbase/src/module.tsx:932registers the “create thread from message” context menu, andpackages/dmworkbase/src/module.tsx:993-1025creates the thread directly viaWKApp.apiClient.post(...). This path does not callmaybeFollowNewThread, so users creating a thread from a message under an already-followed channel still will not get the new thread auto-followed. Please wire the helper into this path too, and add a regression test for it.
💬 Non-blocking
- 🟡 Warning:
maybeFollowNewThreadis described as best-effort, but the create flows nowawaitit before showing success or callingonSuccess/onClose(ThreadCreate/index.tsx:66,ThreadCreateModal/index.tsx:81,ThreadPanel/index.tsx:983). Failures are swallowed, but slow/sidebar/syncor/follow/threadrequests can still delay create feedback. Consider fire-and-forget behavior if immediate feedback is the priority.
✅ Highlights
- The helper correctly uses the follow sidebar snapshot instead of
channelInfo.orgData.is_followed. - The new tests cover followed and unfollowed parent states for the three edited entry points.
- The helper keeps auto-follow failures isolated from the primary thread creation result.
mochashanyao
left a comment
There was a problem hiding this comment.
[Octo-Q · automated review]
Verdict: Approve — no blocking findings; notes below (data-flow traced).
octo-web PR#391 Review Report
Reviewer: Octo-Q (automated review)
Head SHA: 40d0b5daa961d81204ffb74a0dd210a53087b187
PR: #391 — fix: auto-follow new thread when parent channel is followed
1. Verification Summary
| Item | Status | Evidence |
|---|---|---|
maybeFollowNewThread data-flow correctness |
✅ | threadCreate → toThread() always sets channel_id = buildThreadChannelId(groupNo, short_id) (datasource.ts:361-389). Thread.channel_id is required non-empty string. FollowService.followThread({ thread_channel_id }) type-matches FollowThreadReq. |
SidebarService.sync return shape |
✅ | SidebarSyncResp.items: SidebarItem[] with target_type: number and target_id: string. SidebarTargetType.CHANNEL === 2 confirmed. |
All 3 datasource.threadCreate call-sites wired |
✅ | ThreadCreate/index.tsx:66, ThreadCreateModal/index.tsx:81, ThreadPanel/index.tsx:983 — all call maybeFollowNewThread after create. |
| Error isolation (best-effort) | ✅ | maybeFollowNewThread wraps entire body in try/catch, logs via console.warn, never throws to caller. Outer component catch blocks remain unreachable from this helper. |
| Test coverage | ✅ | 6 new Vitest cases (2 per entry point), symmetric: parent-followed → followThread called once with correct arg; parent-not-followed → followThread not called, create feedback still fires. |
"sidebar-reload" event listener exists |
✅ | useFollowSidebar.ts:126 — single listener calls load({ silent: true }). |
2. Findings
F1 — P2: 4th thread-create path in module.tsx not wired (completeness gap)
File: packages/dmworkbase/src/module.tsx:1003-1028
A 4th manual thread-creation path exists in the message context menu handler. It calls WKApp.apiClient.post(groups/${channelID}/threads, ...) directly (bypassing datasource.threadCreate), emits wk:thread-created, and navigates to the new thread — but does not call maybeFollowNewThread.
// module.tsx:1003-1028 (simplified)
const resp = await WKApp.apiClient.post(
`groups/${message.channel.channelID}/threads`, { name, source_message_id, source_message_payload }
);
Toast.success(...);
WKApp.mittBus.emit("wk:thread-created", { ... });
// ← no maybeFollowNewThread callDiff-scope 3 questions (R2):
- Pre-existing — this path was not auto-following before the PR either.
- PR diff does not touch
module.tsx. The behavioral divergence between the 3 wired paths and this unwired path is new (before this PR, no path auto-followed; now 3 do and 1 doesn't). - Not a regression — the
module.tsxpath behavior is unchanged. But it creates a user-visible inconsistency: creating a thread from the message context menu under a followed channel won't auto-follow, while creating from the other 3 entry points will.
Severity rationale (R1/R4): Does not make a previously-working path unavailable or produce incorrect data. The path was never auto-followed. This is a completeness gap, not a regression → P2.
Suggestion: Add maybeFollowNewThread(message.channel.channelID, resp) after the Toast.success in module.tsx:1012. Note: resp here is the raw API response, not a Thread object — either construct a Thread from it or refactor to use datasource.threadCreate (which also handles the wk:thread-created emit and toThread mapping).
F2 — P2: await maybeFollowNewThread blocks success feedback on 2 network round-trips
Files: ThreadCreate/index.tsx:66, ThreadCreateModal/index.tsx:81, ThreadPanel/index.tsx:983
All three call-sites await maybeFollowNewThread(groupNo, created) before showing Toast.success / calling onSuccess / onClose / loadThreads. The helper makes up to 2 serial network calls (POST /sidebar/sync + POST /follow/thread) before resolving.
Impact: The user perceives thread creation as slower than it actually is — the thread is already created on the server, but the success toast and UI transitions are gated on the follow logic completing. Under normal latency (~100-200ms per call) this adds ~200-400ms. Under degraded network conditions, it could be more (bounded by whatever HTTP timeout the API client uses).
Diff-scope: New — introduced by this PR (previously no follow logic existed between create and feedback).
Severity rationale: Not P1 — the delay is bounded and the try/catch ensures it always resolves. UX degradation under poor network, not a functional break → P2.
Suggestion: Consider fire-and-forget: maybeFollowNewThread(groupNo, created) without await. The helper's internal try/catch already prevents unhandled rejections. The sidebar-reload emit inside the helper will still refresh the sidebar asynchronously. Trade-off: the sidebar may not immediately show the new thread in the Follow tab if the user navigates there before the follow completes, but the scheduleThreadReload staggered polling (300/1000/2000ms) in useFollowSidebar will catch it.
F3 — P2: Redundant sidebar sync calls (up to 5 per thread creation)
Files: followNewThread.ts:22,31, datasource.ts:286 (emits wk:thread-created), useFollowSidebar.ts:126,207
A single thread creation now triggers:
SidebarService.syncfrommaybeFollowNewThread(1 call)FollowService.followThread(1 call, conditional)mittBus.emit("sidebar-reload")→useFollowSidebar.load({ silent: true })→ anotherSidebarService.sync(1 call)mittBus.emit("wk:thread-created")(fromdatasource.threadCreate) →scheduleThreadReload→ up to 3 staggeredSidebarService.synccalls at 300/1000/2000ms
Total: up to 5 /sidebar/sync calls per thread creation. The scheduleThreadReload dedup (requestedThreadReloadsRef) and early-exit on followedKeysRef.has(threadKey) mitigate this, but the immediate sidebar-reload and the first 300ms timer likely both fire redundant reloads.
Diff-scope: Amplified — wk:thread-created reloads were pre-existing; this PR adds 2 more sync calls on top.
Severity: P2 — performance waste, not a functional issue. The calls are silent (no loading spinner) and the server should handle them, but it's unnecessary load.
Suggestion: Since maybeFollowNewThread already emits sidebar-reload after following, consider skipping the explicit SidebarService.sync call inside the helper and instead reading the follow state from a lighter source (e.g., a local cache or the sidebar state already held by useFollowSidebar). Alternatively, suppress the sidebar-reload emit and let scheduleThreadReload handle the refresh.
F4 — Nit: "sidebar-reload" as any type-safety gap
File: followNewThread.ts:31
WKApp.mittBus.emit("sidebar-reload" as any) — "sidebar-reload" is not defined in the MittEvents type (App.tsx:3-30). All existing emitters/listeners use the same as any cast.
Diff-scope: Pre-existing pattern, PR adds one more instance.
Suggestion: Add "sidebar-reload": void to the MittEvents interface to get compile-time safety. Low priority.
3. Suggestions
- Wire
module.tsx(F1): Either addmaybeFollowNewThreadto the message-context-menu create path, or refactor it to usedatasource.threadCreateso it benefits from the same wiring. This closes the behavioral inconsistency. - Consider fire-and-forget (F2): Drop the
awaitonmaybeFollowNewThreadto avoid gating UX feedback on follow-side latency. The helper's try/catch already prevents unhandled rejections. - Reduce redundant syncs (F3): Coordinate with
scheduleThreadReloadto avoid 5 sync calls per creation.
4. Additional Findings
- Test mock fidelity: Tests directly instantiate class components and hand-feed mock data, bypassing React rendering and real service integration. This is acknowledged in test comments and acceptable for verifying wiring correctness, but won't catch issues in the real
SidebarService.sync→FollowService.followThreadintegration path. The[browser]smoke check mentioned in the PR body is the right complement. threadCreateside effect:datasource.threadCreateemitswk:thread-createdbefore returning. If any future listener for that event adds auto-follow logic, it would race withmaybeFollowNewThread. Currently safe (the only listener isuseFollowSidebarwhich only schedules reloads).
5. Data-Flow Trace
| Consumed Data | Upstream Source | Reaches Consumer? |
|---|---|---|
thread.channel_id |
datasource.toThread() → buildThreadChannelId(groupNo, short_id) → "${groupNo}____${shortId}" |
✅ Always non-empty string |
resp.items (from SidebarService.sync) |
POST /sidebar/sync → SidebarSyncResp.items: SidebarItem[] |
✅ Array with target_type/target_id fields |
SidebarTargetType.CHANNEL |
Enum in SidebarService.ts → value 2 |
✅ Matches server-side target type for channels |
WKApp.shared.deviceId |
StorageService (localStorage), fallback UUID |
✅ Always non-empty string after init |
groupNo (param) |
Passed from component props/state | ✅ Matches target_id in sidebar items for the parent channel |
FollowService.followThread param |
{ thread_channel_id: thread.channel_id } → FollowThreadReq |
✅ Type-compatible |
6. Blind-Spot Checklist (R5)
- C1 — Dual-path parity: N/A. No symmetric add/remove or create/delete pairs touched. The 3 wired create paths are consistent with each other. The unwired
module.tsxpath is a completeness gap (F1), not a parity violation (it uses a different API path, notdatasource.threadCreate). - C2 — Control-flow ordering / nesting reuse: Clear.
maybeFollowNewThreadis a leaf helper with no nested reuse. Thetry/catchwrapping is correct and theawaitordering (create → follow → feedback) is intentional. - C3 — Authorization boundary ≠ capability boundary: N/A. No new endpoints, tools, or capabilities exposed. The helper calls existing services (
SidebarService.sync,FollowService.followThread) with the same auth context as the calling component.
[Octo-Q] verdict: APPROVE
No P0/P1 findings. The core logic is correct: data-flow from threadCreate → maybeFollowNewThread → followThread is sound, error isolation is proper (best-effort, never blocks create feedback), and all 3 datasource.threadCreate call-sites are symmetrically wired. Tests validate the wiring at each entry point. The 4 findings are P2/nit: a missed 4th create path in module.tsx (pre-existing gap, not regression), UX latency from await blocking, redundant sidebar syncs, and a type-safety gap. None meet the P1 bar per R1 (no path becomes unavailable, no incorrect user-visible data, no regression).
lml2468
left a comment
There was a problem hiding this comment.
PR #391 审查结论 — request-changes
改动概览:7 文件 +357/-2,关联 #292。新增 best-effort 工具 Utils/followNewThread.ts 的 maybeFollowNewThread,在 threadCreate 成功后读 sidebar follow 快照,父群已关注则自动关注新子区;接入 ThreadCreate / ThreadCreateModal / ThreadPanel 三个手建入口,并新增 6 条测试(每入口 2 条)。
测试验证:testsPass=true,无新增失败=true(基线 57→57),build=true,lint=true。
问题统计:阻断问题 1 条,小问题 / nit 5 条;另有 8 条经核验不成立的误报已丢弃。
[major] 第 4 个建子区入口未接入新抽象,自动关注覆盖不全 — packages/dmworkbase/src/module.tsx:1011
本 PR 把自动关注逻辑抽到 Utils/followNewThread.ts 并接入了 ThreadCreate / ThreadCreateModal / ThreadPanel 三个入口。但仓库里还有第 4 个建子区入口:module.tsx 中消息右键『创建子区』(WKApp.apiClient.post(\groups/${...}/threads`)→emit('wk:thread-created')→showConversation),它没有调用 maybeFollowNewThread`。
抽象到公共 util 的目的恰恰是『所有手建入口语义一致』,漏掉一个入口意味着同一个用户操作(手建子区)在不同入口表现不一致——从右键消息建的子区不会自动关注,从面板 / 弹窗建的会。这是抽象边界不完整。
建议:在 module.tsx 该 onOk 成功分支(拿到 resp.channel_id、emit('wk:thread-created') 附近)同样 await maybeFollowNewThread(message.channel.channelID, resp)。resp 是 apiClient 原始返回,已确认含 channel_id 字段(符合 Thread 形状);若字段不全可只取 channel_id 构造最小入参。这样四个入口共用同一条自动关注链路,抽象才真正闭合。
其余 5 条小问题 / nit 已贴为行内评论(ThreadCreateModal:81、followNewThread.ts:15/21/25/33),核心是:自动关注被串在创建反馈的 await 关键路径上(应 fire-and-forget),以及 best-effort 契约(失败不阻塞)、resp.items 为空、target_type 判别等边界缺测试覆盖。
结论:request-changes(存在 1 条阻断问题:第 4 个建子区入口未接入,自动关注覆盖不全)。
| trimmedName, | ||
| sourceMessageId | ||
| ) | ||
| await maybeFollowNewThread(groupNo, result) |
There was a problem hiding this comment.
[minor] 自动关注串行 await 阻塞创建反馈,sidebar 拉取/关注慢时创建成功 toast 与弹窗关闭被延迟
三个入口都把 await maybeFollowNewThread(...) 串在 threadCreate 成功之后、创建反馈(Toast.success / onSuccess / onClose / loadThreads)之前。maybeFollowNewThread 内部要先 SidebarService.sync(一次网络全量同步)再 followThread(再一次网络),这两跳全部 await 完成后用户才看到『创建成功』和弹窗关闭。
这不是功能性 bug(maybeFollowNewThread 内部已 try/catch 吞掉异常,不会破坏成功路径),但属于一致性 / 体验层面的正确性问题:子区其实已经创建成功,反馈却要等一个 best-effort 的附带动作完成才出现;sidebar/follow 接口慢或抖动时,创建会显得『卡住』。函数注释也写明了设计意图是『失败不阻塞创建反馈』,但当前实现只做到了『失败不抛出』,没做到『不阻塞』。
建议:把自动关注做成 fire-and-forget,先给创建反馈再异步补关注,例如在调用处不 await(void maybeFollowNewThread(groupNo, created)),或在该函数内部不 await(自身 catch 已就绪)。ThreadCreate/index.tsx:64、ThreadPanel/index.tsx:983 同此问题。注意:测试当前依赖 await 顺序断言 followThread 被调,改成不 await 后需相应调整测试(可在函数内部仍同步发起、仅不阻塞外层)。
| thread: Thread | ||
| ): Promise<void> { | ||
| try { | ||
| const resp = await SidebarService.sync({ |
There was a problem hiding this comment.
[minor] auto-follow 链路把重量级 /sidebar/sync 放在创建反馈的 await 关键路径上,阻塞 toast/onSuccess
三个入口(ThreadCreate / ThreadCreateModal / ThreadPanel)都是 const created = await threadCreate(...); await maybeFollowNewThread(...) 之后才 Toast.success/onSuccess。而 maybeFollowNewThread 内部要先 await SidebarService.sync({tab:'follow'})(POST /sidebar/sync,见 SidebarService.ts:55),父群已关注时还要再串行 await FollowService.followThread。也就是说子区已经创建成功后,用户仍要多等 1 次(已关注时 2 次)网络往返才能看到『创建成功』反馈并关闭弹窗。函数注释自称『失败不抛出、不阻塞创建反馈』,但当前实现实际把它放进了反馈的 await 链,语义不符。
建议:不要 await 它。在 threadCreate 成功后立即 Toast.success/onSuccess/关闭弹窗,自动关注以 fire-and-forget 方式后台进行,例如调用处改为 void maybeFollowNewThread(groupNo, created)(函数内部已 try/catch 吞错,天然适合 best-effort 后台执行),让创建反馈不再被关注往返阻塞。
| // 让 follow 侧栏刷新,新子区即时进关注 tab | ||
| WKApp.mittBus.emit("sidebar-reload" as any) | ||
| } | ||
| } catch (err) { |
There was a problem hiding this comment.
[nit] 核心契约『自动关注失败不阻塞创建反馈』完全没测
followNewThread.ts 的整个设计意图(JSDoc 明写『失败不抛出、不阻塞创建反馈』)就是 best-effort——sync 或 followThread 失败时静默吞掉(line 33-34 的 catch + console.warn),且组件里 await maybeFollowNewThread(...) 是排在 Toast.success/onSuccess 之前的。一旦该函数意外抛出,成功反馈就会被跳过。但新增的 6 个测试全部只走 sync resolve + followThread resolve 的成功分支,catch 块零覆盖,关键保证(follow 失败仍要触发创建成功反馈 / onSuccess)没有任何断言守护。这是本 PR 最重要却缺失的路径。
建议:补两条用例——(a) hoisted.sidebarSync.mockRejectedValue(new Error('x')) 时,断言 threadCreate 已成功且 Toast.success/onSuccess 仍被调用、函数 resolve 不 reject;(b) 父群已关注但 hoisted.followThread.mockRejectedValue(...) 时,同样断言创建反馈照常触发。这两条直接覆盖 catch 分支与『不阻塞』契约。
| * | ||
| * 失败不抛出、不阻塞创建反馈。 | ||
| */ | ||
| export async function maybeFollowNewThread( |
There was a problem hiding this comment.
[nit] 新增工具 followNewThread.ts 没有独立单测,逻辑只被三个组件间接覆盖
本 PR 全部新逻辑都集中在 maybeFollowNewThread 这一个纯函数里,但没有 Utils/__tests__/followNewThread.test.ts,它只在 ThreadCreate / ThreadCreateModal / ThreadPanel 三个测试里被间接触发,且三处用的都是同一套 happy-path mock。这导致每个组件测试都重复了一遍 sidebar mock 样板,而真正该被穷尽测试的边界(见 line 25 / line 33 评论)反而都漏了。
建议:为该工具补一个独立 test 文件,直接 import maybeFollowNewThread 并集中覆盖:已关注 / 未关注 / sync 失败 / follow 失败 / items 为空 等分支;组件测试则退化为只验证『调用了 maybeFollowNewThread 且接线参数正确』,减少重复 mock。
| tab: "follow", | ||
| device_uuid: WKApp.shared.deviceId, | ||
| }) | ||
| const parentFollowed = (resp.items ?? []).some( |
There was a problem hiding this comment.
[minor] 未覆盖边界:resp.items 为 undefined 与 target_type 类型判别
line 25 的 (resp.items ?? []) 空值兜底,以及 line 26 的 it.target_type === SidebarTargetType.CHANNEL 类型判别,都没被测到。所有『父群未关注』用例的 mock item 都是 {target_type: 2, target_id: 'other'}——只在 target_id 上失配,从不在 target_type 上失配。因此一个回归(比如把 CHANNEL 误判成 THREAD,或漏判 target_type)不会被现有测试捕获;sync 返回 {}(items 缺失)也没人验证不会崩。
建议:补用例——(a) mockResolvedValue({}) 断言不抛、不调 followThread;(b) 在 items 里放一个 {target_type: THREAD(5), target_id: 'g1'}(id 相同但类型是子区)断言 parentFollowed 判 false、followThread 不被调,以锁住 target_type 判别。
lml2468
left a comment
There was a problem hiding this comment.
✅ APPROVE(校准 workflow request-changes → approve)。本 PR 范围正确:helper 用 sidebar follow 快照判父群已关注、失败 try/catch 吞掉;改的 3 入口都正确接入+6 测试、CI 全绿。workflow 标的 🔴 第 4 入口 module.tsx 经核实不在本 PR diff、pre-existing,按不拿 out-of-scope 卡新 PR 降 follow-up。0 真 blocker → APPROVE。🟡 follow-up:第 4 入口另开 PR;helper await 改 fire-and-forget;补 catch/边界测试。
yujiawei
left a comment
There was a problem hiding this comment.
Code Review — PR #391 (octo-web)
Head reviewed: 40d0b5daa961d81204ffb74a0dd210a53087b187
Scope: fix: auto-follow new thread when parent channel is followed (Fixes #292)
Verdict: Request changes
The implementation is clean and the approach is sound — a best-effort maybeFollowNewThread helper that reads the authoritative tab=follow sidebar snapshot and only follows when the parent channel is already followed. The parent-followed detection (target_type === CHANNEL && target_id === groupNo) is consistent with the existing convention in Hooks/useFollowSidebar.ts and Components/ChatConversationList/index.tsx, error-swallowing is correct, and the 6 new unit tests pass.
However, the fix is incomplete, which blocks merge given the PR explicitly claims Fixes #292 and covers "all three thread-create entry points."
P1 — A fourth thread-create entry point is not covered, leaving the #292 symptom live
packages/dmworkbase/src/module.tsx:933 registers the contextmenus.createThread message context menu ("create thread from a group message"). It is a live, user-facing path gated only by remoteConfig.threadOn + ChannelTypeGroup + non-system-message:
// module.tsx:1003
const resp = await WKApp.apiClient.post(
`groups/${message.channel.channelID}/threads`,
{ name: threadName.trim(), source_message_id: parseInt(message.messageID), source_message_payload: sourcePayload }
);
Toast.success(t("base.module.createThread.success"));
if (resp && resp.channel_id) {
WKApp.mittBus.emit("wk:thread-created", { ... }); // reload only — never calls followThread
...
}This path creates the thread via a direct apiClient.post('groups/.../threads'), not via channelDataSource.threadCreate, so a grep for threadCreate does not surface it — there are 4 create paths, not 3. It does not call maybeFollowNewThread. A user right-clicking a message under a followed channel and choosing "create thread" still does not get the new thread auto-followed — exactly the bug #292 sets out to fix.
The wk:thread-created emit here only schedules a sidebar reload (via useFollowSidebar's backoff), never a follow write, so it does not compensate — it merely surfaces threads the backend already auto-followed, and the backend fanout is bot-only + eventually-consistent (as the helper's own docstring notes).
Required: either invoke maybeFollowNewThread(message.channel.channelID, <thread from resp>) on this path too (the response exposes channel_id / short_id), or narrow the PR description / open a tracked follow-up. Cross-file sibling-shape blind spot — a threadCreate grep cannot see a raw apiClient.post('/threads').
Non-blocking notes (P2 / nit — fix at author's discretion)
-
P2 — Success feedback is serialized behind the follow round-trips. In all three wired call sites,
await maybeFollowNewThread(...)runs beforeToast.success/onSuccess/onClose/loadThreads(ThreadCreate/index.tsx:66,ThreadCreateModal/index.tsx:81,ThreadPanel/index.tsx:983). The helper does an unconditionalawait SidebarService.sync(...)and a conditionalawait FollowService.followThread(...)— up to two network round-trips that now gate the create acknowledgement on every thread create, even when the parent is not followed. Since the helper is best-effort and self-contains itstry/catch, fire the success feedback first and run it as a non-awaited side effect (or move theawaitafter the success calls). The codebase's own manual follow path (ThreadPanel.handleFollow) does optimistic UI first to avoid exactly this stall. -
P2 — The single
emit("sidebar-reload")is the weaker of two reload mechanisms. It does one non-retryingload(); if the next/sidebar/synchasn't yet reflected the new follow, the thread only appears thanks to the separatewk:thread-created→scheduleThreadReload[300,1000,2000]msbackoff thatthreadCreatealready fires. Consider dropping the redundant emit or reusing the retry/backoff so the helper is robust on its own. -
nit — Test coverage of the helper's two defining behaviors is missing. No test exercises the swallow-on-error contract (no
mockRejectedValueassertingonSuccess/Toast.successstill fire on a rejectedsync/followThread), and theemit("sidebar-reload")is never asserted. Thethread_channel_idargument is well covered. Adding a rejected-sync test and an emit assertion would lock in the two behaviors a "best-effort" helper most needs guarded. -
nit —
followThreadis called unconditionally when the parent is followed, without checking whether the new thread is already in the just-synced snapshot. Harmless today (the thread was just created; backend auto-follow is bot-only) and relies on backend idempotency; a cheap guard would make it self-consistent.
Summary
Approach and code quality are good, but the stated fix does not cover the message-context-menu create path, so #292 is only partially closed. Please address the P1 (cover the 4th entry point or rescope) before merge; the P2/nit items are optional polish.
QA Engineer Review — PR #391Verdict: PASS-WITH-RISK (review:done:qa:comment) Coverage (what the PR wires)6 new vitest cases under
Wiring is verified at all 3 modified call-sites. Existing Risks (why not full PASS)R1 — Bug #292 not fully resolved: 4th manual create path uncovered. R2 — Best-effort contract is undertested. R3 — R4 — RecommendationEither wire |
lml2468
left a comment
There was a problem hiding this comment.
qa-engineer persona — PASS-WITH-RISK. See verdict comment (review:done:qa:comment): bug #292 not fully resolved because the 4th manual create path (module.tsx:1003 message right-click) bypasses the helper. Wired-path tests are solid but best-effort contract (rejected sync / rejected followThread) and sidebar-reload emit are untested.
Security Engineer Review — PR #391Verdict: CLEARED (review:done:security:approve) STRIDE / OWASP scan
authZ boundary checkThe ConclusionNo findings. Helper introduces no new attack surface. Cleared for merge from a security standpoint. |
lml2468
left a comment
There was a problem hiding this comment.
security-engineer persona — CLEARED. See verdict comment (review:done:security:approve): no new auth surface; existing endpoints; STRIDE/OWASP scan clean; CI secret-scan + OSV + dependency-review all SUCCESS.
Code Reviewer Persona — PR #391Verdict: REQUEST_CHANGES (review:done:code:changes) Blocking — B1: Incomplete coverage of the bug-fix scope
The helper was extracted precisely to give all manual create entry points symmetric auto-follow semantics. Shipping it without the 4th wiring:
Fix options (pick one):
Non-blocking — N1:
|
lml2468
left a comment
There was a problem hiding this comment.
code-reviewer persona — REQUEST_CHANGES (review:done:code:changes). See verdict comment: B1 blocking — 4th create path module.tsx:1003 (right-click message menu) bypasses the helper, leaving #292 reproducible from that path post-merge. PR body claim of 'all three manual create entry points' is factually off (there are 4). Fix options listed. N1/N2/N3 non-blocking polish.
Aggregate Verdict: CHANGES_REQUESTEDHead SHA: Trio recap
Blocking ask (shared by qa-R1 and code-B1)
Pick one to unblock:
Non-blocking polish (not gating merge — file as follow-ups if deferred)
SecurityNo findings. Helper adds no new attack surface — pre-existing authenticated endpoints, no privilege change, no new state-changing surface. Cleared. Next stepEngineer to address B1 (the 4th path) — push a follow-up commit and the loop will re-tick. No human-review escalation needed: there is no Labels: applying |
What
Web users creating a thread under an already-followed channel didn't get the new thread auto-followed — it only showed up in the Recent tab and needed a manual right-click → follow. Bot-created threads already land in Follow because the backend
auto_follow_threadsfanout handles them, but that fanout is bot-only and eventually consistent, so the web manual-create paths can't rely on it.How
New best-effort helper
Utils/followNewThread.ts→maybeFollowNewThread(groupNo, thread):threadCreatesucceeds, reads the sidebar follow snapshot (SidebarService.sync({ tab: "follow" })).target_type === 2 && target_id === groupNo), not fromchannelInfo.orgData.is_followed(commented as unreliable).FollowService.followThread({ thread_channel_id: thread.channel_id }), thenemit("sidebar-reload")so the new thread shows in the Follow tab immediately.Wired symmetrically into all three manual create entry points:
Components/ThreadCreate/index.tsxComponents/ThreadCreateModal/index.tsxComponents/ThreadPanel/index.tsx(inlinewkConfirmcreate)No schema/API changes. Backend fanout untouched.
Tests
[api]6 new Vitest cases (2 per entry point), symmetric:followThreadcalled exactly once with{ thread_channel_id: "g1____t1" }followThreadnot called, create feedback still firesThreadPanel.archive.test.tsx(16) still green.A
[browser]smoke check (create a thread under a followed channel → it appears in the Follow tab ≤2s, persists after refresh) is left for post-merge regression.Fixes #292