Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions apps/discord-bot/src/features/ResponseBridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,15 @@ import {
resolveDiscordThreadTitleBadges,
resolveDiscordTitlePrEvidence,
resolveSettledDiscordThreadTitleUpgrade,
resolveSilentDiscordThreadPrBadge,
resolveTemporaryDiscordThreadTitleBadge,
resolveThreadChangeRequestLookupCwds,
mergeStickyTitlePr,
nextMirroredThreadTitleAfterApply,
planDiscordThreadTitleApply,
shouldApplyDiscordThreadPrBadge,
shouldApplyDiscordThreadTitleBadge,
shouldDeferInitializedPrWhileBusy,
shouldConvertWorkingTipsToWakeUp,
summarizeExternalUserInput,
threadTitleChangeRequestState,
Expand Down Expand Up @@ -2095,6 +2097,143 @@ describe("resolveSettledDiscordThreadTitleUpgrade", () => {
}),
).toBe("🔀 ⏳ Empasa pickup carrier rollout");
});

it("does not thrash-rename ⏳ → ▫️ ⏳ while still busy after no-PR is confirmed", () => {
// Turn start painted activity-only; VCS later confirms no PR. Second rename to
// ▫️ ⏳ is pure channel noise — settle will paint ▫️ when busy clears.
expect(
resolveSettledDiscordThreadTitleUpgrade({
thread: {
title: "for pr 2101, i wish we could push freely to draft PRs",
branch: "t3-discord/pr-2101-draft-pushes",
worktreePath: "/var/lib/t3/worktrees/t3code/t3-discord-6c17cdca",
messages: [assistantMessage()],
session: { status: "running", activeTurnId: "turn-1" } as never,
latestTurn: {
turnId: "turn-1" as never,
state: "running",
completedAt: null,
} as never,
},
mirroredThreadTitle: "⏳ for pr 2101, i wish we could push freely to draft PRs",
attemptedThreadTitle: "⏳ for pr 2101, i wish we could push freely to draft PRs",
cachedPr: null,
canApplyNoPrBadge: true,
}),
).toBeNull();
});

it("still paints ▫️ ⏳ in one shot when busy starts on a plain title with no-PR known", () => {
expect(
resolveSettledDiscordThreadTitleUpgrade({
thread: {
title: "for pr 2101, i wish we could push freely to draft PRs",
branch: "t3-discord/pr-2101-draft-pushes",
worktreePath: "/var/lib/t3/worktrees/t3code/t3-discord-6c17cdca",
messages: [assistantMessage()],
session: { status: "running", activeTurnId: "turn-1" } as never,
latestTurn: {
turnId: "turn-1" as never,
state: "running",
completedAt: null,
} as never,
},
mirroredThreadTitle: "for pr 2101, i wish we could push freely to draft PRs",
attemptedThreadTitle: "for pr 2101, i wish we could push freely to draft PRs",
cachedPr: null,
canApplyNoPrBadge: true,
}),
).toBe("▫️ ⏳ for pr 2101, i wish we could push freely to draft PRs");
});

it("still upgrades ⏳ → 🔀 ⏳ mid-turn when a real PR appears", () => {
expect(
resolveSettledDiscordThreadTitleUpgrade({
thread: {
title: "Empasa pickup carrier rollout",
branch: "t3-discord/empasa-pickup-carrier",
worktreePath: "/var/lib/t3/worktrees/scanner/t3-discord-c434b753",
messages: [assistantMessage()],
session: { status: "running", activeTurnId: "turn-1" } as never,
latestTurn: {
turnId: "turn-1" as never,
state: "running",
completedAt: null,
} as never,
},
mirroredThreadTitle: "⏳ Empasa pickup carrier rollout",
attemptedThreadTitle: "⏳ Empasa pickup carrier rollout",
cachedPr: { state: "open", hasFailingChecks: false },
}),
).toBe("🔀 ⏳ Empasa pickup carrier rollout");
});
});

describe("shouldDeferInitializedPrWhileBusy / resolveSilentDiscordThreadPrBadge", () => {
it("defers only the no-PR badge while busy is unchanged", () => {
expect(
shouldDeferInitializedPrWhileBusy({
currentPr: null,
nextPr: "initialized",
currentActivity: "busy",
nextActivity: "busy",
}),
).toBe(true);
expect(
resolveSilentDiscordThreadPrBadge({
currentPr: null,
nextPr: "initialized",
currentActivity: "busy",
nextActivity: "busy",
}),
).toBeNull();
});

it("does not defer when painting dual badges from a plain title", () => {
expect(
shouldDeferInitializedPrWhileBusy({
currentPr: null,
nextPr: "initialized",
currentActivity: null,
nextActivity: "busy",
}),
).toBe(false);
expect(
resolveSilentDiscordThreadPrBadge({
currentPr: null,
nextPr: "initialized",
currentActivity: null,
nextActivity: "busy",
}),
).toBe("initialized");
});

it("does not defer real PR upgrades or settle (busy → idle)", () => {
expect(
shouldDeferInitializedPrWhileBusy({
currentPr: null,
nextPr: "open",
currentActivity: "busy",
nextActivity: "busy",
}),
).toBe(false);
expect(
shouldDeferInitializedPrWhileBusy({
currentPr: null,
nextPr: "initialized",
currentActivity: "busy",
nextActivity: null,
}),
).toBe(false);
expect(
resolveSilentDiscordThreadPrBadge({
currentPr: null,
nextPr: "initialized",
currentActivity: "busy",
nextActivity: null,
}),
).toBe("initialized");
});
});

describe("planDiscordThreadTitleApply", () => {
Expand Down
77 changes: 74 additions & 3 deletions apps/discord-bot/src/features/ResponseBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1553,6 +1553,53 @@ export function shouldApplyDiscordThreadPrBadge(
return discordThreadTitleBadgeRank(next) >= discordThreadTitleBadgeRank(current);
}

/**
* Silence: never thrash-rename `⏳ Title` → `▫️ ⏳ Title` mid-turn.
*
* Busy is painted first (before VCS confirms no PR). Adding only the no-PR ▫️
* badge while still busy is a second channel-name system message with no user
* value — settle can compose ▫️ and clear ⏳ in one rename.
*
* Real PR upgrades (🔀 / ❌ / ✔️ / ✖️) still apply mid-turn.
*/
export function shouldDeferInitializedPrWhileBusy(input: {
readonly currentPr: DiscordThreadPrBadgeState;
readonly nextPr: DiscordThreadPrBadgeState;
readonly currentActivity: DiscordThreadActivityBadgeState;
readonly nextActivity: DiscordThreadActivityBadgeState;
}): boolean {
return (
input.nextPr === "initialized" &&
input.currentPr === null &&
input.currentActivity === "busy" &&
input.nextActivity === "busy"
);
}

/**
* PR column after demotion guards + mid-busy ▫️ silence.
*/
export function resolveSilentDiscordThreadPrBadge(input: {
readonly currentPr: DiscordThreadPrBadgeState;
readonly nextPr: DiscordThreadPrBadgeState;
readonly currentActivity: DiscordThreadActivityBadgeState;
readonly nextActivity: DiscordThreadActivityBadgeState;
}): DiscordThreadPrBadgeState {
const prAllowed = shouldApplyDiscordThreadPrBadge(input.currentPr, input.nextPr);
const appliedPr = prAllowed ? input.nextPr : input.currentPr;
if (
shouldDeferInitializedPrWhileBusy({
currentPr: input.currentPr,
nextPr: appliedPr,
currentActivity: input.currentActivity,
nextActivity: input.nextActivity,
})
) {
return input.currentPr;
}
return appliedPr;
}

/**
* @deprecated Dual-slot: use `shouldApplyDiscordThreadPrBadge` for PR; activity always applies.
* Kept for older tests that still pass exclusive states.
Expand Down Expand Up @@ -1618,12 +1665,19 @@ export function resolveSettledDiscordThreadTitleUpgrade(input: {
: parseDiscordThreadTitleBadges(input.attemptedThreadTitle);

// Sticky PR: refuse demotion; keep current PR column when next would weaken it.
// Also silence mid-busy ▫️-only upgrades (⏳ Title → ▫️ ⏳ Title thrash).
const prAllowed = shouldApplyDiscordThreadPrBadge(currentBadges.pr, prState);
const appliedPr = prAllowed ? prState : currentBadges.pr;
const appliedPr = resolveSilentDiscordThreadPrBadge({
currentPr: currentBadges.pr,
nextPr: prState,
currentActivity: currentBadges.activity,
nextActivity: activity,
});

// Demotion refused and activity unchanged → leave the mirrored title alone
// (preserves ❌ 🔀 etc. without re-decorating from a null PR cache).
if (!prAllowed && activity === currentBadges.activity) {
// Same for deferred ▫️ while still busy: nothing to rename until settle.
if (appliedPr === currentBadges.pr && activity === currentBadges.activity) {
return null;
}

Expand Down Expand Up @@ -4370,7 +4424,14 @@ export const runBridge = (
? mirroredBadges
: parseDiscordThreadTitleBadges(latest.attemptedThreadTitle);
const prAllowed = shouldApplyDiscordThreadPrBadge(currentBadges.pr, effectivePrState);
const appliedPr = prAllowed ? effectivePrState : currentBadges.pr;
// Silence mid-busy ▫️-only upgrades so VCS "no PR" does not second-rename
// after the busy paint (⏳ → ▫️ ⏳ thrash). Settle composes both later.
const appliedPr = resolveSilentDiscordThreadPrBadge({
currentPr: currentBadges.pr,
nextPr: effectivePrState,
currentActivity: currentBadges.activity,
nextActivity: activityAfterLookup,
});

// Still nothing actionable (no PR column, no activity, nothing mirrored).
if (appliedPr === null && activityAfterLookup === null && currentBadges.pr === null) {
Expand All @@ -4386,6 +4447,16 @@ export const runBridge = (
return;
}

// Deferred ▫️ while still busy and activity already matches → no rename.
if (
appliedPr === currentBadges.pr &&
activityAfterLookup === currentBadges.activity &&
latest.mirroredThreadTitle !== null
) {
yield* commitComputedTitle(null, "silence-defer-initialized-while-busy");
return;
}

const desiredTitle = decorateDiscordThreadTitle(
threadAfterLookup.title,
{
Expand Down
Loading