fix(slack): serialize restoreInitial races

This commit is contained in:
Frank Yang
2026-03-29 15:36:22 +08:00
parent fc60229755
commit 0c6ca907d1
2 changed files with 45 additions and 2 deletions

View File

@@ -144,6 +144,43 @@ describe("Slack status reaction lifecycle", () => {
expect(adapter.setReaction).toHaveBeenCalledTimes(3);
});
it("restoreInitial re-applies initial emoji after an in-flight debounced transition", async () => {
let releaseThinking: (() => void) | undefined;
const { adapter, active } = createSlackMockAdapter();
adapter.setReaction = vi.fn(async (emoji: string) => {
if (emoji === DEFAULT_EMOJIS.thinking) {
await new Promise<void>((resolve) => {
releaseThinking = resolve;
});
}
if (active.has(emoji)) {
throw new Error("already_reacted");
}
active.add(emoji);
});
const ctrl = createStatusReactionController({
enabled: true,
adapter,
initialEmoji: "eyes",
timing: { debounceMs: 0, stallSoftMs: 99999, stallHardMs: 99999 },
});
void ctrl.setQueued();
await vi.advanceTimersByTimeAsync(1);
expect(active.has("eyes")).toBe(true);
void ctrl.setThinking();
await vi.advanceTimersByTimeAsync(1);
const restorePromise = ctrl.restoreInitial();
releaseThinking?.();
await restorePromise;
expect(active.has("eyes")).toBe(true);
expect(active.has(DEFAULT_EMOJIS.thinking)).toBe(false);
});
it("does nothing when disabled", async () => {
const { adapter, active } = createSlackMockAdapter();
const ctrl = createStatusReactionController({

View File

@@ -282,6 +282,7 @@ export function createStatusReactionController(params: {
} else {
// Debounced execution for intermediate states
debounceTimer = setTimeout(() => {
debounceTimer = null;
void enqueue(async () => {
await applyEmoji(emoji);
pendingEmoji = "";
@@ -380,12 +381,17 @@ export function createStatusReactionController(params: {
}
const alreadyInitial = currentEmoji === initialEmoji;
const initialAlreadyQueuedImmediately = pendingEmoji === initialEmoji && debounceTimer === null;
const pendingBeforeClear = pendingEmoji;
const hadDebouncedPending = debounceTimer !== null;
clearAllTimers();
if (alreadyInitial || initialAlreadyQueuedImmediately) {
if (alreadyInitial && (!pendingBeforeClear || hadDebouncedPending)) {
pendingEmoji = "";
return;
}
if (pendingBeforeClear === initialEmoji && !hadDebouncedPending) {
await chainPromise;
return;
}
await enqueue(async () => {
await applyEmoji(initialEmoji);