From e1df3452595e6a7e7f953afd903122bf7794f80a Mon Sep 17 00:00:00 2001 From: AstroHan Date: Fri, 4 Sep 2026 16:59:36 +0800 Subject: [PATCH] test(desktop): sink the slash-command submit rule to its routing function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `dispatches /side instead of steering it into a running turn` timed out on an unrelated PR's CI, on a branch that changes nothing it touches. What it guards is one rule: a slash command submitted mid-turn is dispatched, not queued as follow-up text. That rule lived as an inline ternary at the call site in app-shell, so nothing but a full Electron run could reach it, while the two functions it wrapped already had a routing module and a test file of their own. Moving the condition into `resolveFollowUpModeAtSubmit` puts it where the rest of the submit routing is decided and makes it a three-line assertion instead of a browser, a fake backend and a real Turn. The parameter takes the parsed command rather than a boolean derived from it. `slashCommand` passes as shorthand, which leaves app-shell three tokens lighter than the ternary it replaces, and the routing module only needs to know whether a command is there. The E2E test goes with the move, and takes two assertions with it. The menu offering /side is still covered by `shows only slash commands executable in the current session state`. The menu withholding /compact mid-turn is not: that spec waits for the seeded Turn to finish before it opens the menu, so `streaming` is false and its count includes /compact. That guard — `!(streaming && id === 'compact')` — now has no automated coverage in any tier. This change did not break it and does not catch it; it belongs to the same tier-three move, per #4727. The deleted spec also ended by asserting the side chat panel appeared. The unit test pins the routing decision, which is that outcome's precondition, not the outcome. Refs #4727 Generated-by: Claude Code --- apps/desktop/e2e/slash-command-menu.spec.ts | 29 ------------------- apps/desktop/renderer-architecture.json | 2 +- .../follow-up-submit-routing.test.ts | 23 +++++++++++++++ apps/desktop/src/renderer/app-shell.tsx | 11 ++++--- .../src/renderer/follow-up-submit-routing.ts | 5 ++++ 5 files changed, 34 insertions(+), 36 deletions(-) diff --git a/apps/desktop/e2e/slash-command-menu.spec.ts b/apps/desktop/e2e/slash-command-menu.spec.ts index d78016aa9b..4a1c4a026b 100644 --- a/apps/desktop/e2e/slash-command-menu.spec.ts +++ b/apps/desktop/e2e/slash-command-menu.spec.ts @@ -17,7 +17,6 @@ * under the License. */ -import { FAKE_HOLD_OPEN_PROMPT } from '@maka/runtime/test-only/fake-backend'; import { expect, test, COMPOSER_INPUT } from './fixtures'; test('shows only slash commands executable in the current session state', async ({ @@ -208,34 +207,6 @@ test('opens the slash menu after a DOM block break', async ({ await expect(menu).toBeVisible(); }); -test('dispatches /side instead of steering it into a running turn', async ({ - invocableSkillsWindow: page, -}) => { - const composer = page.locator(COMPOSER_INPUT); - const runningPrompt = FAKE_HOLD_OPEN_PROMPT; - await composer.fill(runningPrompt); - await composer.press('Enter'); - await expect(page.locator('.maka-user-message', { hasText: runningPrompt })).toBeVisible(); - await expect(page.getByRole('button', { name: '停止' })).toBeVisible(); - - await composer.click(); - await composer.pressSequentially('/'); - const menu = page.getByRole('listbox', { name: '命令和技能' }); - const commands = menu.getByRole('group', { name: '命令' }); - const side = commands.getByRole('option', { name: /打开侧聊.*\/side/ }); - await expect(side).toBeVisible(); - await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0); - await side.click(); - await expect.poll(() => composer.textContent()).toBe('/side '); - await expect(page.locator('.maka-quote-workbar-panel')).toHaveCount(0); - - await composer.fill('/side discuss separately'); - await composer.press('Enter'); - - await expect(page.locator('.maka-quote-workbar-panel')).toHaveCount(1); - await page.getByRole('button', { name: '停止' }).click(); -}); - test('an open menu keeps its container and skills group across projection refreshes', async ({ invocableSkillsWindow: page, }) => { diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json index 1ab3bb754b..305684c186 100644 --- a/apps/desktop/renderer-architecture.json +++ b/apps/desktop/renderer-architecture.json @@ -891,7 +891,7 @@ "react": 1 }, "importSpecifiers": 148, - "nonTriviaTokens": 15620 + "nonTriviaTokens": 15617 }, "src/renderer/use-app-shell-composer-quotes.ts": { "importDeclarations": 2, diff --git a/apps/desktop/src/main/__tests__/follow-up-submit-routing.test.ts b/apps/desktop/src/main/__tests__/follow-up-submit-routing.test.ts index b829b09021..596a3dd656 100644 --- a/apps/desktop/src/main/__tests__/follow-up-submit-routing.test.ts +++ b/apps/desktop/src/main/__tests__/follow-up-submit-routing.test.ts @@ -50,6 +50,7 @@ describe('follow-up submit routing', () => { assert.equal( resolveFollowUpModeAtSubmit({ hasActiveTurn: true, + slashCommand: null, }), 'queue', ); @@ -57,6 +58,7 @@ describe('follow-up submit routing', () => { resolveFollowUpModeAtSubmit({ requestedMode: 'steer', hasActiveTurn: true, + slashCommand: null, }), 'steer', ); @@ -66,6 +68,27 @@ describe('follow-up submit routing', () => { assert.equal( resolveFollowUpModeAtSubmit({ hasActiveTurn: false, + slashCommand: null, + }), + undefined, + ); + }); + + it('dispatches a slash command mid-turn instead of steering it into the Turn', () => { + assert.equal( + resolveFollowUpModeAtSubmit({ + hasActiveTurn: true, + slashCommand: { kind: 'side' }, + }), + undefined, + ); + // An explicit steer request loses to the command too: Shift+Enter on + // `/side` still opens the side chat. + assert.equal( + resolveFollowUpModeAtSubmit({ + requestedMode: 'steer', + hasActiveTurn: true, + slashCommand: { kind: 'side' }, }), undefined, ); diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx index e650ee81e6..e3ea229f2c 100644 --- a/apps/desktop/src/renderer/app-shell.tsx +++ b/apps/desktop/src/renderer/app-shell.tsx @@ -1919,12 +1919,11 @@ function AppShellContent({ const runningTurnIds = sessionId ? sessionsRef.current.find((session) => session.id === sessionId)?.runningTurnIds : undefined; - const followUpAtSubmit = !slashCommand - ? resolveFollowUpModeAtSubmit({ - requestedMode: metadata?.followUpMode, - hasActiveTurn: hasActiveTurnAtSubmit({ liveTurn, runningTurnIds }), - }) - : undefined; + const followUpAtSubmit = resolveFollowUpModeAtSubmit({ + requestedMode: metadata?.followUpMode, + hasActiveTurn: hasActiveTurnAtSubmit({ liveTurn, runningTurnIds }), + slashCommand, + }); if (sessionId && followUpAtSubmit) { const queued = await enqueueFollowUp(sessionId, text, followUpAtSubmit, { ...metadata, diff --git a/apps/desktop/src/renderer/follow-up-submit-routing.ts b/apps/desktop/src/renderer/follow-up-submit-routing.ts index 951a467d99..3c2959f83a 100644 --- a/apps/desktop/src/renderer/follow-up-submit-routing.ts +++ b/apps/desktop/src/renderer/follow-up-submit-routing.ts @@ -35,7 +35,12 @@ export function hasActiveTurnAtSubmit(input: { export function resolveFollowUpModeAtSubmit(input: { requestedMode?: FollowUpMode; hasActiveTurn: boolean; + /** The parsed command, if the text was one. Only its presence matters here. */ + slashCommand: object | null; }): FollowUpMode | undefined { + // A slash command tells the app to do something; it is not text for the + // Turn that happens to be running. Dispatch it instead of queueing it. + if (input.slashCommand) return undefined; if (input.requestedMode) return input.requestedMode; // Mid-turn submits always queue; Shift+Enter carries the one-shot steer as // the requested mode.