test(e2e): let the three editors' Save reach the wire

The prompt, template and knowledge editors all stopped at the same boundary: the
suite proved a document loads and survives an in-memory round trip, but no spec
ever let Save issue its mutation. That is the one operation that can destroy a
user's Go template, and the form→mutation hop — dirty tracking, raw/rich mode,
which field is sent — was covered by nothing.

Each spec now edits, saves, and asserts the request: updatePrompt/createPrompt
carries the edit plus every template atom (heading, {{.Target}}, the bash fence,
the table row), updateFlowTemplate the same for {{TARGET}}, and
updateKnowledgeDocument the edited content with the document id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-07-26 04:54:33 +07:00
co-authored by Claude Opus 4.8
parent 548cc12488
commit fe812a77e2
3 changed files with 127 additions and 2 deletions
@@ -158,4 +158,41 @@ test.describe('knowledges crud', { tag: '@crud' }, () => {
await expect(page).toHaveURL(new RegExp(`/knowledges/${KNOWLEDGE_DOC.id}$`));
});
});
test.describe('save', () => {
test.use({
cassette: knowledgesCassette({
mutations: {
updateKnowledgeDocument: [{ data: { updateKnowledgeDocument: KNOWLEDGE_DOC } as never }],
},
queries: {
knowledgeDocument: [
{ data: { knowledgeDocument: KNOWLEDGE_DOC }, variables: { id: KNOWLEDGE_DOC.id } },
],
},
}),
});
// The write half of the editor: until now the suite proved the document loads and round-trips
// in memory, never that Save puts the edited bytes on the wire.
test('Save sends the edited content and keeps the document id', async ({ page, pageErrorLog }) => {
await page.goto(`/knowledges/${KNOWLEDGE_DOC.id}`);
await typeIntoEditor(page, 'Content', 'E2E-SAVE-MARK');
const request = page.waitForRequest(
(candidate) =>
candidate.method() === 'POST' &&
candidate.postDataJSON()?.operationName === 'updateKnowledgeDocument',
);
await page.getByRole('button', { exact: true, name: 'Save' }).click();
const { variables } = (await request).postDataJSON();
expect(variables.id).toBe(KNOWLEDGE_DOC.id);
expect(variables.input.content).toContain('E2E-SAVE-MARK');
expectCleanPage(pageErrorLog);
});
});
});
@@ -5,7 +5,13 @@ import { expectCleanPage } from '../../helpers/errors.ts';
import { RICH_TEMPLATE_TEXT, TEMPLATE_DETAIL, templateDetailCassette } from '../../mocks/cassettes/templates.ts';
test.describe('template detail', { tag: '@coverage' }, () => {
test.use({ cassette: templateDetailCassette() });
test.use({
cassette: templateDetailCassette({
mutations: {
updateFlowTemplate: [{ data: { updateFlowTemplate: TEMPLATE_DETAIL } as never }],
},
}),
});
const EDITOR = 'Template content';
@@ -75,6 +81,38 @@ test.describe('template detail', { tag: '@coverage' }, () => {
// A real load failure must offer Retry in place, not the "Template not found" card that a
// genuine 404 shows — the two used to collapse into the same dead-end.
// Save is the only operation that can destroy a user's template, and no spec has ever let it reach
// the wire — the round-trip units stop at the editor boundary.
test('Save sends the edited body verbatim, placeholders intact', async ({ page, pageErrorLog }) => {
await page.goto(`/templates/${TEMPLATE_DETAIL.id}`);
const editor = page.getByRole('textbox', { name: EDITOR });
await expect(editor.getByRole('heading', { name: 'Recon' })).toBeVisible();
await editor.click();
await page.keyboard.press('ControlOrMeta+End');
await editor.pressSequentially(' E2E-SAVE-MARK');
const request = page.waitForRequest(
(candidate) =>
candidate.method() === 'POST' && candidate.postDataJSON()?.operationName === 'updateFlowTemplate',
);
await page.getByRole('button', { exact: true, name: 'Save' }).click();
const { variables } = (await request).postDataJSON();
expect(variables.templateId).toBe(TEMPLATE_DETAIL.id);
expect(variables.input.text).toContain('E2E-SAVE-MARK');
for (const atom of ['# Recon', '{{TARGET}}', 'nmap -sV']) {
expect(variables.input.text, `"${atom}" survived the save`).toContain(atom);
}
expectCleanPage(pageErrorLog);
});
test.describe('load failure', () => {
test.use({
cassette: templateDetailCassette({
@@ -9,7 +9,26 @@ import {
} from '../../mocks/cassettes/settings-prompts.ts';
test.describe('settings prompt detail', { tag: '@coverage' }, () => {
test.use({ cassette: promptDetailCassette() });
test.use({
cassette: promptDetailCassette({
mutations: {
createPrompt: [
{
data: {
createPrompt: {
__typename: 'UserPrompt',
createdAt: '2026-01-15T12:00:00Z',
id: '77',
template: 'saved',
type: 'pentester',
updatedAt: '2026-01-15T12:00:00Z',
},
} as never,
},
],
},
}),
});
const EDITOR = 'System prompt template';
@@ -57,4 +76,35 @@ test.describe('settings prompt detail', { tag: '@coverage' }, () => {
expect(raw).toContain('E2E-MARK');
expectCleanPage(pageErrorLog);
});
// The editor is where a Go template's byte fidelity is decided, and Save is the only operation
// that can destroy one. Until now no spec let it reach the wire, so the whole form → mutation hop
// was unverified: the round-trip units cannot see it.
test('Save sends the edited template verbatim, atoms intact', async ({ page, pageErrorLog }) => {
await page.goto(`/settings/prompts/${PROMPT_DETAIL_AGENT}`);
const editor = page.getByRole('textbox', { name: EDITOR });
await expect(editor.getByRole('heading', { name: 'Pentester' })).toBeVisible();
await editor.click();
await page.keyboard.press('ControlOrMeta+End');
await editor.pressSequentially(' E2E-SAVE-MARK');
const request = page.waitForRequest(
(candidate) => candidate.method() === 'POST' && candidate.postDataJSON()?.operationName === 'createPrompt',
);
await page.getByRole('button', { exact: true, name: 'Save' }).click();
const { variables } = (await request).postDataJSON();
expect(variables.template, 'the edit reached the wire').toContain('E2E-SAVE-MARK');
for (const atom of ['# Pentester', '{{.Target}}', '{{.Scope}}', '```bash', '| Scope | {{.Scope}} |']) {
expect(variables.template, `"${atom}" survived the save`).toContain(atom);
}
expectCleanPage(pageErrorLog);
});
});