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
22 changes: 22 additions & 0 deletions apps/desktop/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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
Expand Down
17 changes: 16 additions & 1 deletion apps/desktop/e2e/skill-draft-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -73,6 +73,21 @@ async function failWorkspaceSkillRevision(page: Page): Promise<void> {
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
Expand Down