From 22a0ae3c719df2adc396e4640d3aee0fd17bf20c Mon Sep 17 00:00:00 2001 From: Joob1n Date: Mon, 21 Sep 2026 15:59:52 +0800 Subject: [PATCH] fix(desktop): settle the Skill draft's preconditions before it sends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `skill-draft-lifecycle` fails on most branches right now, including main, with "Skill 调用失败,消息未发送" never becoming visible. Two independent races sit between disabling the Skill and pressing Enter, and each one makes the rejection the journey asserts never render: - The toggle goes through the raw `window.maka.skills.setEnabled` bridge, not the Skills page, so nothing re-fetches the composer's `/` source. Until Runtime's projection drops the Skill it is still invocable, the send resolves it, and the send succeeds. - Picking the Skill left the `/` menu open. While the editable reports `aria-expanded`, the composer swallows Enter as menu acceptance rather than sending, so the draft is never submitted at all. Wait for both before pressing Enter: the editable back to `aria-expanded="false"`, and the Skill gone from `listInvocable`. The second uses a new helper beside the existing `waitForInvocableSkills`, which already polls the same authoritative projection for the opposite transition. Generated-by: Maka --- apps/desktop/e2e/fixtures.ts | 22 +++++++++++++++++++ .../desktop/e2e/skill-draft-lifecycle.spec.ts | 17 +++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/apps/desktop/e2e/fixtures.ts b/apps/desktop/e2e/fixtures.ts index 45e05ec985d..25fa016b4df 100644 --- a/apps/desktop/e2e/fixtures.ts +++ b/apps/desktop/e2e/fixtures.ts @@ -116,6 +116,28 @@ export async function waitForInvocableSkills( .toEqual(expect.arrayContaining(expectedIds)); } +/** + * Wait for Runtime's projection to stop offering a Skill. + * + * A Skill is toggled through the raw bridge here rather than the Skills page, so + * nothing re-fetches the composer's `/` source on its own. Pressing Enter before + * Runtime has dropped the Skill lets the send resolve it and succeed, and the + * rejection the journey expects never renders — the composer keeps offering a + * Skill that is already disabled. + */ +export async function waitForSkillNotInvocable( + page: Page, + absentIds: readonly string[], +): Promise { + await expect + .poll(async () => + page.evaluate(async () => + (await window.maka.skills.listInvocable(undefined)).map((skill) => skill.id), + ), + ) + .not.toEqual(expect.arrayContaining(absentIds)); +} + /** * Pre-seed a real-looking connection into the throwaway workspace so onboarding * clears and the composer is enabled. Actual sessions still run on the fake diff --git a/apps/desktop/e2e/skill-draft-lifecycle.spec.ts b/apps/desktop/e2e/skill-draft-lifecycle.spec.ts index 227a8bc2a73..80c9ef1e93d 100644 --- a/apps/desktop/e2e/skill-draft-lifecycle.spec.ts +++ b/apps/desktop/e2e/skill-draft-lifecycle.spec.ts @@ -18,7 +18,7 @@ */ import type { Page } from '@playwright/test'; -import { expect, test, COMPOSER_INPUT } from './fixtures'; +import { expect, test, COMPOSER_INPUT, waitForSkillNotInvocable } from './fixtures'; /** * Revision drafts, per session, with a Skill staged in them. @@ -73,6 +73,21 @@ async function failWorkspaceSkillRevision(page: Page): Promise { expect(disabled.ok).toBe(true); const composer = page.locator(COMPOSER_INPUT); + // Two independent races sit between disabling the Skill and pressing Enter, + // and both make the rejection this journey asserts never render: + // + // 1. The toggle above goes through the raw bridge, not the Skills page, so + // nothing re-fetches the composer's `/` source. Until Runtime's projection + // drops the Skill it is still invocable, the send resolves it, and it + // succeeds — no banner. + // 2. Picking the Skill left the `/` menu open. While it reports + // `aria-expanded`, the composer swallows Enter as menu acceptance instead + // of sending, so the draft is never submitted at all. + // + // Settle both: the menu closed, and the Skill gone from the invocable set. + await expect(composer).toHaveAttribute('aria-expanded', 'false'); + await waitForSkillNotInvocable(page, ['workspace-only']); + await composer.press('Enter'); await expect(page.getByText('Skill 调用失败,消息未发送')).toBeVisible(); // The draft survives the rejection whole, and reads as the token rather than