Files
maka/apps/desktop/e2e/skill-draft-lifecycle.spec.ts
agent-radar 7be535059e
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 19:05:14 +08:00

133 lines
5.7 KiB
TypeScript

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { Page } from '@playwright/test';
import { expect, test, COMPOSER_INPUT } from './fixtures';
/**
* Revision drafts, per session, with a Skill staged in them.
*
* A staged Skill is a `/skill:<id>` chip inside the draft text, so every path
* here — begin edit, prepare the branch, fail the send, cancel back — moves it
* by moving the text. The point of these journeys is that nothing has to carry
* the Skill separately for that to hold.
*
* The Skill itself comes from the real catalog (the invocable-skills fixture
* plus the Skills module page), not a Desktop-only starter IPC.
*/
async function openInstalledWorkspaceSkill(page: Page): Promise<void> {
await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(page.getByText('Workspace Only', { exact: true })).toBeVisible();
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(page.locator(COMPOSER_INPUT)).toBeVisible();
}
async function seedEditableTurn(page: Page): Promise<void> {
const firstSend = page.locator(COMPOSER_INPUT);
await firstSend.fill('original message');
await firstSend.press('Enter');
await expect(page.getByText(/Fake backend received: original message/)).toBeVisible();
}
/** Type the draft, then append the Skill chip — the order a user works in. */
async function composeWithSkill(page: Page, text: string, name: RegExp): Promise<void> {
const composer = page.locator(COMPOSER_INPUT);
await composer.fill(text);
await composer.click();
await composer.pressSequentially(' /');
const option = page.getByRole('listbox', { name: /技能/ }).getByRole('option', { name });
await expect(option).toBeVisible();
await option.click();
}
async function beginRevision(page: Page): Promise<void> {
const userMessage = page.getByLabel('你发送的消息').first();
await userMessage.hover();
await userMessage.getByRole('button', { name: '编辑并重发' }).click();
await expect(page.locator('[data-revision-notice="true"]')).toBeVisible();
}
async function failWorkspaceSkillRevision(page: Page): Promise<void> {
const disabled = await page.evaluate(() =>
window.maka.skills.setEnabled('workspace-only', false),
);
expect(disabled.ok).toBe(true);
const composer = page.locator(COMPOSER_INPUT);
await composer.press('Enter');
await expect(page.getByText('Skill 调用失败,消息未发送')).toBeVisible();
// The draft survives the rejection whole, and reads as the token rather than
// as a chip: the Skill was just disabled, so it is gone from the catalog the
// composer draws chips from. A chip here would promise a Skill that no longer
// resolves — the text is the honest rendering, and re-enabling it below sends.
await expect(composer).toContainText('edited with skill');
await expect(composer).toContainText('/skill:workspace-only');
}
test('a successful revision retry clears both child and source drafts', async ({
invocableSkillsWindow: page,
}) => {
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);
const enabled = await page.evaluate(() =>
window.maka.skills.setEnabled('workspace-only', true),
);
expect(enabled.ok).toBe(true);
await page.locator(COMPOSER_INPUT).press('Enter');
await expect(page.locator('[data-revision-notice="true"]')).toHaveCount(0);
await expect(page.locator(COMPOSER_INPUT)).toHaveText('');
await page.getByRole('button', { name: '查看上一版本' }).click();
await expect(
page.getByLabel('你发送的消息').getByText('original message', { exact: true }),
).toBeVisible();
await expect(page.locator(COMPOSER_INPUT)).toHaveText('');
});
test('cancelling a failed revision restores the complete pre-edit draft', async ({
invocableSkillsWindow: page,
}) => {
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);
const composer = page.locator(COMPOSER_INPUT);
await composeWithSkill(page, 'previous unsent draft', /Project Only/);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);
await page.getByRole('button', { name: '取消' }).click();
await expect(page.locator('[data-revision-notice="true"]')).toHaveCount(0);
// Restored through a single controlled write, which rebuilds the editor from
// the serialized draft; the Skill comes back as a chip because the composer
// redraws it from that text, not because anything carried it separately.
await expect(composer).toContainText('previous unsent draft');
await expect(
page.locator('[data-astryx-token-value="/skill:project-only"]'),
).toContainText('Project Only');
});