From fe812a77e23948b1a3c3ec5b21164f2cf2a67260 Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Sun, 26 Jul 2026 04:54:33 +0700 Subject: [PATCH] test(e2e): let the three editors' Save reach the wire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/e2e/specs/crud/knowledges.spec.ts | 37 +++++++++++++ .../e2e/specs/crud/template-detail.spec.ts | 40 +++++++++++++- .../e2e/specs/settings/prompt-detail.spec.ts | 52 ++++++++++++++++++- 3 files changed, 127 insertions(+), 2 deletions(-) diff --git a/frontend/e2e/specs/crud/knowledges.spec.ts b/frontend/e2e/specs/crud/knowledges.spec.ts index 75b83321..128a5925 100644 --- a/frontend/e2e/specs/crud/knowledges.spec.ts +++ b/frontend/e2e/specs/crud/knowledges.spec.ts @@ -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); + }); + }); }); diff --git a/frontend/e2e/specs/crud/template-detail.spec.ts b/frontend/e2e/specs/crud/template-detail.spec.ts index 2fca012a..7da457f3 100644 --- a/frontend/e2e/specs/crud/template-detail.spec.ts +++ b/frontend/e2e/specs/crud/template-detail.spec.ts @@ -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({ diff --git a/frontend/e2e/specs/settings/prompt-detail.spec.ts b/frontend/e2e/specs/settings/prompt-detail.spec.ts index c61f6b1b..f29864ef 100644 --- a/frontend/e2e/specs/settings/prompt-detail.spec.ts +++ b/frontend/e2e/specs/settings/prompt-detail.spec.ts @@ -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); + }); });