fix(dashboard): bind editMessage to channel to stop repost loop

The retry refactor captured `const editMessage = channel.editMessage` and
called it detached, losing `this`. Every status edit then threw
"this.client is undefined", so each cycle failed all retries and reposted a
fresh (notifying) status message — ~50 reposts in 30 minutes. Bind the method
to the channel so the edit runs in place. Adds a regression test showing a
detached method fails while a bound one succeeds.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-08-21 12:58:04 +09:00
parent 8600213bcc
commit 65ef6e8830
2 changed files with 39 additions and 1 deletions

View File

@@ -312,6 +312,41 @@ describe('editStatusMessageWithRetry', () => {
expect(ok).toBe(false); expect(ok).toBe(false);
expect(slept).toBe(0); expect(slept).toBe(0);
}); });
it('works with a channel method that relies on `this` when bound (regression)', async () => {
class FakeChannel {
client = { ok: true };
edits = 0;
async editMessage(): Promise<void> {
// Mirrors the real editMessage: throws if `this` is lost.
if (!this.client) throw new Error('this.client is undefined');
this.edits++;
}
}
const channel = new FakeChannel();
// A detached method reference loses `this` → every attempt fails.
const detached = channel.editMessage;
const detachedOk = await editStatusMessageWithRetry({
editOnce: () => detached(),
maxRetries: 0,
retryDelayMs: 0,
sleep: async () => {},
});
expect(detachedOk).toBe(false);
expect(channel.edits).toBe(0);
// Binding to the channel (the fix) preserves `this` and succeeds.
const bound = channel.editMessage.bind(channel);
const boundOk = await editStatusMessageWithRetry({
editOnce: () => bound(),
maxRetries: 0,
retryDelayMs: 0,
sleep: async () => {},
});
expect(boundOk).toBe(true);
expect(channel.edits).toBe(1);
});
}); });
describe('createCoalescingTrigger', () => { describe('createCoalescingTrigger', () => {

View File

@@ -916,7 +916,10 @@ export async function startUnifiedDashboard(
// immediately spawn a fresh status message. Retry the edit a couple of // immediately spawn a fresh status message. Retry the edit a couple of
// times with a delay first; only give up (and repost) if all fail. // times with a delay first; only give up (and repost) if all fail.
const editId = statusMessageId; const editId = statusMessageId;
const editMessage = channel.editMessage; // Bind to the channel: a detached method reference loses `this` and the
// edit throws ("this.client is undefined"), which would make every
// update fail and repost a fresh (notifying) message.
const editMessage = channel.editMessage.bind(channel);
// First render after start: 0 retries → repost immediately if the edit // First render after start: 0 retries → repost immediately if the edit
// fails. Steady state: retry twice at 15s before reposting. // fails. Steady state: retry twice at 15s before reposting.
const maxRetries = firstStatusRender ? 0 : STATUS_EDIT_MAX_RETRIES; const maxRetries = firstStatusRender ? 0 : STATUS_EDIT_MAX_RETRIES;