deepseek-harness/apps/web/tests/command-image-envelope.expected.e2e.ts

98 lines
4.7 KiB
TypeScript
Raw Normal View History

feat(commands): route composer image attachments through slash commands A claimed slash command consumed only the text half of the composer submission: /goal with reference images executed, cleared the draft, and silently stranded the images in the rail. Model-visible attachment intent had no route through the command plane. The submission envelope is now modeled end to end. CommandDefinition input.images declares acceptance; the declaration rides the descriptor to every client, onto the minted CommandClaim, and into the input machine's claim snapshot. commands.execute carries the submission's base64 images and enforces the declaration in the executor: non-declaring commands, a missing attachment store, and exceeded batch limits settle as logged error results before the handler runs. Admission reuses the attachment package's new admitEncodedImages, extracted from api-proxy's prompt path so both wire endpoints share one limits/validation/commit sequence. Producers own model visibility: /goal submits one user followup (image blocks + a fixed reference line) after a successful create/edit so goal rounds read the images from session history; /plan folds them into its steered message. Grammar misfits (/goal pause, bare /plan, /plan off) return direct errors and the composer keeps the images. On the client, enter adjudication carries a SubmitEnvelope and every command route that cannot consume images throws a localized refusal that renders as one composer notice with draft and images retained; the claimed pre-gate applies the same copy. An accepting claim serializes the draft images, forwards them to commands.execute, and clears plus releases them only on a success outcome. The assembled web test roster gains the ui-input-trigger and ui-commands plugins, mirroring the shipped composition, so slash submissions exercise the command plane; a new keyless snapshot pins the refusal banner and the accepting /goal flow over the built client graph.
2026-08-17 18:57:55 +08:00
// @vitest-environment jsdom
// The command image-attachment envelope over the BUILT client graph (real
// bundles via AppWebEntry, keyless FixtureApiClient transport): an enter
// submission carrying composer images resolves only through a command whose
// descriptor declares `input.images`. A non-declaring command refuses with
// one composer error banner and everything retained; a declaring command
// consumes the images — serialized through the real draft-image chain into
// the commands/execute payload — and clears the composer on success, including
// when the image is the whole `/plan` task.
feat(commands): route composer image attachments through slash commands A claimed slash command consumed only the text half of the composer submission: /goal with reference images executed, cleared the draft, and silently stranded the images in the rail. Model-visible attachment intent had no route through the command plane. The submission envelope is now modeled end to end. CommandDefinition input.images declares acceptance; the declaration rides the descriptor to every client, onto the minted CommandClaim, and into the input machine's claim snapshot. commands.execute carries the submission's base64 images and enforces the declaration in the executor: non-declaring commands, a missing attachment store, and exceeded batch limits settle as logged error results before the handler runs. Admission reuses the attachment package's new admitEncodedImages, extracted from api-proxy's prompt path so both wire endpoints share one limits/validation/commit sequence. Producers own model visibility: /goal submits one user followup (image blocks + a fixed reference line) after a successful create/edit so goal rounds read the images from session history; /plan folds them into its steered message. Grammar misfits (/goal pause, bare /plan, /plan off) return direct errors and the composer keeps the images. On the client, enter adjudication carries a SubmitEnvelope and every command route that cannot consume images throws a localized refusal that renders as one composer notice with draft and images retained; the claimed pre-gate applies the same copy. An accepting claim serializes the draft images, forwards them to commands.execute, and clears plus releases them only on a success outcome. The assembled web test roster gains the ui-input-trigger and ui-commands plugins, mirroring the shipped composition, so slash submissions exercise the command plane; a new keyless snapshot pins the refusal banner and the accepting /goal flow over the built client graph.
2026-08-17 18:57:55 +08:00
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { expect, it } from 'vitest'
import { installAssembledBootEnv, mountAssembledApp } from './assembled-boot.ts'
installAssembledBootEnv()
/** Open a fresh fixture session and return its composer textarea. */
async function freshComposer(): Promise<HTMLTextAreaElement> {
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
const start = tree.querySelector<HTMLButtonElement>('button[aria-label="New session in fixture"]')
if (start === null) throw new Error('fixture Workspace new-session action missing')
fireEvent.click(start)
return await screen.findByPlaceholderText('Describe what you want to build', {}, { timeout: 10_000 }) as HTMLTextAreaElement
}
/** Paste one tiny PNG into the composer and wait for its rail thumbnail. */
async function pasteImage(textarea: HTMLTextAreaElement, name: string): Promise<void> {
const image = new File([new Uint8Array([137, 80, 78, 71])], name, { type: 'image/png' })
fireEvent.paste(textarea, {
clipboardData: {
items: [{ kind: 'file', type: 'image/png', getAsFile: () => image }],
getData: () => '',
},
})
await waitFor(() => {
const rail = document.querySelector('[role="group"][aria-label="Pending images"]')
if (rail === null) throw new Error('attachment rail missing')
expect([...rail.querySelectorAll('img')].map(img => img.getAttribute('alt'))).toContain(name)
}, { timeout: 5_000 })
}
it('refuses an image-carrying submit to a non-declaring command and keeps draft and images', async () => {
mountAssembledApp()
const textarea = await freshComposer()
await pasteImage(textarea, 'ref.png')
// /echo is a leadingInput fixture command without `input.images`.
fireEvent.change(textarea, { target: { value: '/echo hello' } })
fireEvent.keyDown(textarea, { key: 'Enter' })
// The refusal rides the same transient error banner as other composer
// failures; session activity remains on its separate status live region.
feat(commands): route composer image attachments through slash commands A claimed slash command consumed only the text half of the composer submission: /goal with reference images executed, cleared the draft, and silently stranded the images in the rail. Model-visible attachment intent had no route through the command plane. The submission envelope is now modeled end to end. CommandDefinition input.images declares acceptance; the declaration rides the descriptor to every client, onto the minted CommandClaim, and into the input machine's claim snapshot. commands.execute carries the submission's base64 images and enforces the declaration in the executor: non-declaring commands, a missing attachment store, and exceeded batch limits settle as logged error results before the handler runs. Admission reuses the attachment package's new admitEncodedImages, extracted from api-proxy's prompt path so both wire endpoints share one limits/validation/commit sequence. Producers own model visibility: /goal submits one user followup (image blocks + a fixed reference line) after a successful create/edit so goal rounds read the images from session history; /plan folds them into its steered message. Grammar misfits (/goal pause, bare /plan, /plan off) return direct errors and the composer keeps the images. On the client, enter adjudication carries a SubmitEnvelope and every command route that cannot consume images throws a localized refusal that renders as one composer notice with draft and images retained; the claimed pre-gate applies the same copy. An accepting claim serializes the draft images, forwards them to commands.execute, and clears plus releases them only on a success outcome. The assembled web test roster gains the ui-input-trigger and ui-commands plugins, mirroring the shipped composition, so slash submissions exercise the command plane; a new keyless snapshot pins the refusal banner and the accepting /goal flow over the built client graph.
2026-08-17 18:57:55 +08:00
const notice = await waitFor(() => {
const el = [...document.querySelectorAll('[role="alert"]')]
feat(commands): route composer image attachments through slash commands A claimed slash command consumed only the text half of the composer submission: /goal with reference images executed, cleared the draft, and silently stranded the images in the rail. Model-visible attachment intent had no route through the command plane. The submission envelope is now modeled end to end. CommandDefinition input.images declares acceptance; the declaration rides the descriptor to every client, onto the minted CommandClaim, and into the input machine's claim snapshot. commands.execute carries the submission's base64 images and enforces the declaration in the executor: non-declaring commands, a missing attachment store, and exceeded batch limits settle as logged error results before the handler runs. Admission reuses the attachment package's new admitEncodedImages, extracted from api-proxy's prompt path so both wire endpoints share one limits/validation/commit sequence. Producers own model visibility: /goal submits one user followup (image blocks + a fixed reference line) after a successful create/edit so goal rounds read the images from session history; /plan folds them into its steered message. Grammar misfits (/goal pause, bare /plan, /plan off) return direct errors and the composer keeps the images. On the client, enter adjudication carries a SubmitEnvelope and every command route that cannot consume images throws a localized refusal that renders as one composer notice with draft and images retained; the claimed pre-gate applies the same copy. An accepting claim serializes the draft images, forwards them to commands.execute, and clears plus releases them only on a success outcome. The assembled web test roster gains the ui-input-trigger and ui-commands plugins, mirroring the shipped composition, so slash submissions exercise the command plane; a new keyless snapshot pins the refusal banner and the accepting /goal flow over the built client graph.
2026-08-17 18:57:55 +08:00
.find(candidate => candidate.textContent?.includes('image attachments') ?? false)
if (el === undefined) throw new Error('composer refusal banner missing')
feat(commands): route composer image attachments through slash commands A claimed slash command consumed only the text half of the composer submission: /goal with reference images executed, cleared the draft, and silently stranded the images in the rail. Model-visible attachment intent had no route through the command plane. The submission envelope is now modeled end to end. CommandDefinition input.images declares acceptance; the declaration rides the descriptor to every client, onto the minted CommandClaim, and into the input machine's claim snapshot. commands.execute carries the submission's base64 images and enforces the declaration in the executor: non-declaring commands, a missing attachment store, and exceeded batch limits settle as logged error results before the handler runs. Admission reuses the attachment package's new admitEncodedImages, extracted from api-proxy's prompt path so both wire endpoints share one limits/validation/commit sequence. Producers own model visibility: /goal submits one user followup (image blocks + a fixed reference line) after a successful create/edit so goal rounds read the images from session history; /plan folds them into its steered message. Grammar misfits (/goal pause, bare /plan, /plan off) return direct errors and the composer keeps the images. On the client, enter adjudication carries a SubmitEnvelope and every command route that cannot consume images throws a localized refusal that renders as one composer notice with draft and images retained; the claimed pre-gate applies the same copy. An accepting claim serializes the draft images, forwards them to commands.execute, and clears plus releases them only on a success outcome. The assembled web test roster gains the ui-input-trigger and ui-commands plugins, mirroring the shipped composition, so slash submissions exercise the command plane; a new keyless snapshot pins the refusal banner and the accepting /goal flow over the built client graph.
2026-08-17 18:57:55 +08:00
return el
}, { timeout: 5_000 })
expect(notice.textContent).toBe('/echo does not accept image attachments; remove them first')
expect([...document.querySelectorAll('[role="status"]')]
.some(candidate => candidate.textContent?.includes('image attachments') ?? false)).toBe(false)
feat(commands): route composer image attachments through slash commands A claimed slash command consumed only the text half of the composer submission: /goal with reference images executed, cleared the draft, and silently stranded the images in the rail. Model-visible attachment intent had no route through the command plane. The submission envelope is now modeled end to end. CommandDefinition input.images declares acceptance; the declaration rides the descriptor to every client, onto the minted CommandClaim, and into the input machine's claim snapshot. commands.execute carries the submission's base64 images and enforces the declaration in the executor: non-declaring commands, a missing attachment store, and exceeded batch limits settle as logged error results before the handler runs. Admission reuses the attachment package's new admitEncodedImages, extracted from api-proxy's prompt path so both wire endpoints share one limits/validation/commit sequence. Producers own model visibility: /goal submits one user followup (image blocks + a fixed reference line) after a successful create/edit so goal rounds read the images from session history; /plan folds them into its steered message. Grammar misfits (/goal pause, bare /plan, /plan off) return direct errors and the composer keeps the images. On the client, enter adjudication carries a SubmitEnvelope and every command route that cannot consume images throws a localized refusal that renders as one composer notice with draft and images retained; the claimed pre-gate applies the same copy. An accepting claim serializes the draft images, forwards them to commands.execute, and clears plus releases them only on a success outcome. The assembled web test roster gains the ui-input-trigger and ui-commands plugins, mirroring the shipped composition, so slash submissions exercise the command plane; a new keyless snapshot pins the refusal banner and the accepting /goal flow over the built client graph.
2026-08-17 18:57:55 +08:00
// The whole envelope is retained: draft text and the rail thumbnail.
expect(textarea.value).toBe('/echo hello')
const rail = document.querySelector('[role="group"][aria-label="Pending images"]')
expect([...(rail?.querySelectorAll('img') ?? [])].map(img => img.getAttribute('alt'))).toEqual(['ref.png'])
})
it('consumes images through a declaring command and clears the composer on success', async () => {
mountAssembledApp()
const textarea = await freshComposer()
await pasteImage(textarea, 'goal-ref.png')
// /goal declares `input.images` in the fixture catalog; the claim submit
// serializes the pasted bytes and the fixture executor admits them.
fireEvent.change(textarea, { target: { value: '/goal rebuild the cathedral' } })
fireEvent.keyDown(textarea, { key: 'Enter' })
await waitFor(() => {
expect(textarea.value).toBe('')
expect(document.querySelector('[role="group"][aria-label="Pending images"]')).toBeNull()
}, { timeout: 5_000 })
})
it('submits a bare /plan with an image as an image-only plan request', async () => {
mountAssembledApp()
const textarea = await freshComposer()
await pasteImage(textarea, 'plan-task.png')
fireEvent.change(textarea, { target: { value: '/plan' } })
fireEvent.keyDown(textarea, { key: 'Enter' })
await waitFor(() => {
expect(textarea.value).toBe('')
expect(document.querySelector('[role="group"][aria-label="Pending images"]')).toBeNull()
}, { timeout: 5_000 })
expect([...document.querySelectorAll('[role="alert"]')]
.some(candidate => candidate.textContent?.includes('/plan') ?? false)).toBe(false)
})