diff --git a/tests/e2e/runner-task-creation.spec.ts b/tests/e2e/runner-task-creation.spec.ts new file mode 100644 index 00000000000..14ecad24e98 --- /dev/null +++ b/tests/e2e/runner-task-creation.spec.ts @@ -0,0 +1,30 @@ +import { randomUUID } from "node:crypto"; +import { expect, test } from "@playwright/test"; +import { createTaskThroughUi } from "../runner-e2e/user-actions.js"; + +for (const workMode of ["standard", "planning", "ask"] as const) { + test(`runner fixture creates a prompt-only ${workMode} task without starting a provider`, async ({ page, request }) => { + const companyResponse = await request.post("/api/companies", { data: { name: `Runner composer ${randomUUID()}` } }); + expect(companyResponse.ok()).toBe(true); + const company = await companyResponse.json(); + const agentResponse = await request.post(`/api/companies/${company.id}/agents`, { data: { + name: "Paused composer fixture", role: "engineer", adapterType: "codex_local", + runtimeConfig: { heartbeat: { enabled: false } }, + } }); + expect(agentResponse.ok()).toBe(true); + const agent = await agentResponse.json(); + expect((await request.post(`/api/agents/${agent.id}/pause`)).ok()).toBe(true); + const projectResponse = await request.post(`/api/companies/${company.id}/projects`, { data: { name: "Composer project" } }); + expect(projectResponse.ok()).toBe(true); + const project = await projectResponse.json(); + const prompt = `Preserve this exact prompt for ${workMode}.`; + const created = await createTaskThroughUi({ page, issuePrefix: company.issuePrefix, agentName: agent.name, + title: "Fixture label distinct from the generated title", prompt, workMode, projectName: project.name }); + const issue = await (await request.get(`/api/issues/${created.issueId}`)).json(); + expect(issue).toMatchObject({ id: created.issueId, companyId: company.id, assigneeAgentId: agent.id, + projectId: project.id, description: prompt, workMode }); + expect(issue.title).not.toBe("Fixture label distinct from the generated title"); + expect(created.submittedAtMs).toBeGreaterThan(0); + expect(await (await request.get(`/api/companies/${company.id}/heartbeat-runs`)).json()).toEqual([]); + }); +} diff --git a/tests/runner-e2e/README.md b/tests/runner-e2e/README.md index 8e6516cf4a4..9245e03193e 100644 --- a/tests/runner-e2e/README.md +++ b/tests/runner-e2e/README.md @@ -1629,6 +1629,13 @@ Recovery-state retention uses raw cleanup results before evidence publication. A ## Public installed release smoke +The shared task-creation helper uses the current prompt-only composer and binds +each task to its actual HTTP creation response ID, since its generated title may +change during execution. The provider-free `tests/e2e/runner-task-creation.spec.ts` +regression verifies assignee, project, and all three work modes with paused agents. +Explicit title-preservation cases still require a visible title input and fail +before provider execution when that surface is unavailable. + Set `PAPERCLIP_RUNNER_E2E_INSTALLED_CLI` to the absolute public consumer's `paperclipai/dist/index.js` for an installed-product acceptance run. Install the public package graph and run its ordinary `runtime setup cursor` first. The diff --git a/tests/runner-e2e/accounting-flow.ts b/tests/runner-e2e/accounting-flow.ts index 67b13ee94bc..ec05cd559e1 100644 --- a/tests/runner-e2e/accounting-flow.ts +++ b/tests/runner-e2e/accounting-flow.ts @@ -63,10 +63,10 @@ export async function runAccountingFlow(input: { try { if (execution.profile.generation === "legacy") await prepareLegacyContinuationSkill(api, fixtures.company.id, fixtures.agent.id); await api.patch("/api/instance/settings/experimental", { enableClassicTaskInterface: false }); - await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, + const createdTask = await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title: execution.task.buildTitle(nonce), prompt: execution.task.buildPrompt(nonce), workMode: "standard" }); issue = await pollUntil({ label: "accounting task creation", deadlineAt: input.deadlineAt, - load: async () => (await api.get(`/api/companies/${fixtures.company.id}/issues?limit=100`)).find(i => i.title === execution.task.buildTitle(nonce)), accept: Boolean }); + load: async () => (await api.get(`/api/companies/${fixtures.company.id}/issues?limit=100`)).find(i => i.id === createdTask.issueId), accept: Boolean }); if (!issue) throw new Error("Missing created accounting task"); if (probe.kind === "productive") { for (let step = 1; step <= 5; step++) { diff --git a/tests/runner-e2e/blocker-flow.ts b/tests/runner-e2e/blocker-flow.ts index 68f3173731b..7cb15788b15 100644 --- a/tests/runner-e2e/blocker-flow.ts +++ b/tests/runner-e2e/blocker-flow.ts @@ -88,10 +88,10 @@ export async function runBlockerFlow(input: { if (hash !== hashes[file]) throw new Error(`Bundled operational skill differs from evaluated source: ${file}`); } await api.patch("/api/instance/settings/experimental", { enableClassicTaskInterface: false }); - await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, + const createdTask = await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title: execution.task.buildTitle(input.nonce), prompt: scenario.prompt, workMode: "standard" }); issue = await pollUntil({ label: "browser-created blocker task", deadlineAt: input.deadlineAt, - load: async () => (await api.get(`${company}/issues`)).find(i => i.title === execution.task.buildTitle(input.nonce)), accept: Boolean }); + load: async () => (await api.get(`${company}/issues`)).find(i => i.id === createdTask.issueId), accept: Boolean }); if (!issue) throw new Error("No browser-created task"); checkpoints.push(await settle("waiting")); await open(); diff --git a/tests/runner-e2e/context-integrity-flow.ts b/tests/runner-e2e/context-integrity-flow.ts index ad1e240111c..1fe6a9d4c32 100644 --- a/tests/runner-e2e/context-integrity-flow.ts +++ b/tests/runner-e2e/context-integrity-flow.ts @@ -179,8 +179,8 @@ export async function runContextIntegrityFlow(input: { ? `${scenario.prompt}\n\nUse /${scenario.skillKey} for this request.` : scenario.prompt; skillRequestText = taskPrompt; - await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title: execution.task.buildTitle(nonce), prompt: taskPrompt, workMode: "standard" }); - issue = await pollUntil({ label: "context-integrity task created", deadlineAt: input.deadlineAt, load: async () => (await api.get(`${companyPath}/issues?limit=100`)).find((row) => row.title === execution.task.buildTitle(nonce)), accept: Boolean }); + const createdTask = await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title: execution.task.buildTitle(nonce), prompt: taskPrompt, workMode: "standard" }); + issue = await pollUntil({ label: "context-integrity task created", deadlineAt: input.deadlineAt, load: async () => (await api.get(`${companyPath}/issues?limit=100`)).find((row) => row.id === createdTask.issueId), accept: Boolean }); if (!issue) throw new Error("Missing context-integrity task"); if (scenario.id === "ordered-comment-continuation") { await pollUntil({ diff --git a/tests/runner-e2e/continuation-flow.ts b/tests/runner-e2e/continuation-flow.ts index 3a2c06a1ce5..a1e122bdc5b 100644 --- a/tests/runner-e2e/continuation-flow.ts +++ b/tests/runner-e2e/continuation-flow.ts @@ -223,7 +223,7 @@ export async function runContinuationFlow(input: { await api.patch("/api/instance/settings/experimental", { enableClassicTaskInterface: false, }); - await createTaskThroughUi({ + const createdTask = await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, @@ -236,7 +236,7 @@ export async function runContinuationFlow(input: { deadlineAt: input.deadlineAt, load: async () => (await api.get(tasksPath)).find( - (t) => t.title === execution.task.buildTitle(input.nonce), + (t) => t.id === createdTask.issueId, ), accept: Boolean, }); diff --git a/tests/runner-e2e/cursor-native-flow.ts b/tests/runner-e2e/cursor-native-flow.ts index 284830cd86c..bb1a0dd1900 100644 --- a/tests/runner-e2e/cursor-native-flow.ts +++ b/tests/runner-e2e/cursor-native-flow.ts @@ -263,8 +263,8 @@ export async function runCursorNativeFlow(input: { let expectedMarker = execution.task.buildVisibleMarker(nonce); try { if (!remote && design.id === "native-write-deny-reconnect") await sampleDenied("before-request"); - await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title: execution.task.buildTitle(nonce), prompt: remote ? input.remoteBootstrap!.prompt(nonce) : execution.task.buildPrompt(nonce) + (watch ? `\nExact native shell command (copy verbatim):\n${deniedCommand!.command}` : ""), workMode: "standard", projectName: project.name }); - issue = await pollUntil({ label: "browser-created Cursor task", deadlineAt: input.deadlineAt, load: async () => (await api.get(`/api/companies/${fixtures.company.id}/issues?limit=100`)).find(row => row.title === execution.task.buildTitle(nonce)), accept: Boolean }) ?? {}; + const createdTask = await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title: execution.task.buildTitle(nonce), prompt: remote ? input.remoteBootstrap!.prompt(nonce) : execution.task.buildPrompt(nonce) + (watch ? `\nExact native shell command (copy verbatim):\n${deniedCommand!.command}` : ""), workMode: "standard", projectName: project.name }); + issue = await pollUntil({ label: "browser-created Cursor task", deadlineAt: input.deadlineAt, load: async () => (await api.get(`/api/companies/${fixtures.company.id}/issues?limit=100`)).find(row => row.id === createdTask.issueId), accept: Boolean }) ?? {}; if (!issue.id) throw new Error("Browser-created Cursor task is absent"); if (remote) { const started = await pollUntil({ label: "exact Cursor bootstrap run", deadlineAt: input.deadlineAt, load, reject, diff --git a/tests/runner-e2e/everyday-flow.ts b/tests/runner-e2e/everyday-flow.ts index b9f23d1f474..3e72615b6e7 100644 --- a/tests/runner-e2e/everyday-flow.ts +++ b/tests/runner-e2e/everyday-flow.ts @@ -667,7 +667,7 @@ export async function runEverydayFlow(input: Input) { if (state.connections.length) throw new Error("New-connection story requires an unconnected company"); } - await createTaskThroughUi({ + const createdTask = await createTaskThroughUi({ page, issuePrefix: prefix, agentName: fixtures.agent.name, @@ -682,9 +682,9 @@ export async function runEverydayFlow(input: Input) { load: () => api.get(`/api/companies/${fixtures.company.id}/issues`), accept: (rows) => - rows.some((i) => i.title === execution.task.buildTitle(nonce)), + rows.some((i) => i.id === createdTask.issueId), }).then((rows) => - rows.find((i) => i.title === execution.task.buildTitle(nonce))!, + rows.find((i) => i.id === createdTask.issueId)!, ); input.observe(parent!, []); note("task-submitted", { issueId: parent!.id }); diff --git a/tests/runner-e2e/instruction-persistence.ts b/tests/runner-e2e/instruction-persistence.ts index 6e60b304dd5..baae39b1216 100644 --- a/tests/runner-e2e/instruction-persistence.ts +++ b/tests/runner-e2e/instruction-persistence.ts @@ -68,9 +68,9 @@ export async function runInstructionPersistenceFlow(input: { let issue: Row = {}; let runs: Row[] = []; async function create(title: string, prompt: string) { - await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title, prompt, workMode: "standard", projectName: fixtures.project?.name }); + const createdTask = await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title, prompt, workMode: "standard", projectName: fixtures.project?.name }); const found = await pollUntil({ label: `instruction task ${title}`, deadlineAt: input.deadlineAt, - load: async () => (await api.get(`/api/companies/${fixtures.company.id}/issues?limit=100`)).find(row => row.title === title), accept: row => Boolean(row) }); + load: async () => (await api.get(`/api/companies/${fixtures.company.id}/issues?limit=100`)).find(row => row.id === createdTask.issueId), accept: row => Boolean(row) }); if (!found) throw new Error("Browser-created instruction task missing"); issue = found; input.observe(issue, runs); diff --git a/tests/runner-e2e/native-active-stop-flow.ts b/tests/runner-e2e/native-active-stop-flow.ts index 687f3408a95..6d57a77c458 100644 --- a/tests/runner-e2e/native-active-stop-flow.ts +++ b/tests/runner-e2e/native-active-stop-flow.ts @@ -136,8 +136,8 @@ export async function runNativeActiveStopFlow(input: { check("explicit-per-turn-policy", configured.adapterConfig?.acpxPermissionMode === "approve-reads" && configured.adapterConfig?.lifecycleMode === "per_turn" && (provider !== "cursor" || configured.adapterConfig?.acpxSessionMode === "agent"), "Agent mode and per-turn restrictive permission policy selected before startup"); const project = await api.post(`/api/companies/${fixtures.company.id}/projects`, { name: `Native active Stop ${nonce}`, executionWorkspacePolicy: { enabled: true, defaultMode: "shared_workspace", sharedWorkspaceConcurrency: "serialize", allowIssueOverride: false, environmentId: fixtures.environment.id, workspaceStrategy: { type: "project_primary" } }, workspace: { name: "Primary", sourceType: "local_path", cwd: input.workspacePath, isPrimary: true } }); if (!remote) await sample("before-request"); - await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title: execution.task.buildTitle(nonce), prompt: remote ? input.remoteBootstrap!.prompt(nonce) : prompt(), workMode: "standard", projectName: project.name }); - issue = (await pollUntil({ label: "browser-created active Stop task", deadlineAt: input.deadlineAt, load: async () => (await api.get(`/api/companies/${fixtures.company.id}/issues?limit=100`)).find(i => i.title === execution.task.buildTitle(nonce)), accept: Boolean }))!; + const createdTask = await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title: execution.task.buildTitle(nonce), prompt: remote ? input.remoteBootstrap!.prompt(nonce) : prompt(), workMode: "standard", projectName: project.name }); + issue = (await pollUntil({ label: "browser-created active Stop task", deadlineAt: input.deadlineAt, load: async () => (await api.get(`/api/companies/${fixtures.company.id}/issues?limit=100`)).find(i => i.id === createdTask.issueId), accept: Boolean }))!; if (remote) { await pollUntil({ label: "active Stop remote bootstrap", deadlineAt: input.deadlineAt, load, accept: state => state.run.status === "running" }); const bound = await input.remoteBootstrap!.bindAndRelease({ issueId: issue.id, runId: runs[0]!.id, targets: [target], actionPrompt: async value => { diff --git a/tests/runner-e2e/native-provider-loss-flow.ts b/tests/runner-e2e/native-provider-loss-flow.ts index 1652179b2fe..d9a63e36325 100644 --- a/tests/runner-e2e/native-provider-loss-flow.ts +++ b/tests/runner-e2e/native-provider-loss-flow.ts @@ -104,9 +104,9 @@ export async function runNativeProviderLossFlow(input: { await api.patch(`/api/agents/${fixtures.agent.id}`, { adapterConfig: { ...agent.adapterConfig, acpxPermissionMode: "approve-reads", acpxSessionMode: "agent", timeoutSec: 120, lifecycleMode: "per_turn" } }); const project = await api.post(`/api/companies/${fixtures.company.id}/projects`, { name: `Native provider loss ${nonce}`, executionWorkspacePolicy: { enabled: true, defaultMode: "shared_workspace", sharedWorkspaceConcurrency: "serialize", allowIssueOverride: false, environmentId: fixtures.environment.id, workspaceStrategy: { type: "project_primary" } }, workspace: { name: "Primary", sourceType: "local_path", cwd: input.workspacePath, isPrimary: true } }); if (!remote) await sample("before-request"); - await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title: execution.task.buildTitle(nonce), prompt: remote ? input.remoteBootstrap!.prompt(nonce) : prompt(), workMode: "standard", projectName: project.name }); + const createdTask = await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title: execution.task.buildTitle(nonce), prompt: remote ? input.remoteBootstrap!.prompt(nonce) : prompt(), workMode: "standard", projectName: project.name }); issue = (await pollUntil({ label: "browser-created provider-loss task", deadlineAt: input.deadlineAt, - load: async () => (await api.get(`/api/companies/${fixtures.company.id}/issues?limit=100`)).find(row => row.title === execution.task.buildTitle(nonce)), accept: Boolean }))!; + load: async () => (await api.get(`/api/companies/${fixtures.company.id}/issues?limit=100`)).find(row => row.id === createdTask.issueId), accept: Boolean }))!; if (remote) { await pollUntil({ label: "provider-loss bootstrap", deadlineAt: input.deadlineAt, load, accept: value => value.run.status === "running" }); fixture = await input.remoteBootstrap!.bindAndRelease({ issueId: issue.id, runId: runs[0]!.id, targets: [target], actionPrompt: async value => { diff --git a/tests/runner-e2e/runner.spec.ts b/tests/runner-e2e/runner.spec.ts index f5f625ac7e3..2a017a32ec3 100644 --- a/tests/runner-e2e/runner.spec.ts +++ b/tests/runner-e2e/runner.spec.ts @@ -1079,9 +1079,9 @@ for (const execution of executions) { matcherResults = evidence.checks.map(check => ({ matcher: { kind: "json_path" as const, path: `firstTask.checks.${check.id}`, expected: true }, passed: check.passed, detail: check.detail })); }, createOrdinary: async (taskTitle, taskPrompt) => { - await createTaskThroughUi({ page, issuePrefix: fixtures!.company.issuePrefix!, agentName: fixtures!.agent.name, title: taskTitle, prompt: taskPrompt, workMode: "standard" }); + const createdTask = await createTaskThroughUi({ page, issuePrefix: fixtures!.company.issuePrefix!, agentName: fixtures!.agent.name, title: taskTitle, prompt: taskPrompt, workMode: "standard" }); const created = await pollUntil({ label: "ordinary UI-created task", deadlineAt: startedAtMs + deadlineMs, - load: async () => (await api.get(`/api/companies/${fixtures!.company.id}/issues?limit=100`)).find(row => row.title === taskTitle), accept: row => Boolean(row) }); + load: async () => (await api.get(`/api/companies/${fixtures!.company.id}/issues?limit=100`)).find(row => row.id === createdTask.issueId), accept: row => Boolean(row) }); if (!created) throw new Error("Missing ordinary task"); return created; }, @@ -1106,8 +1106,7 @@ for (const execution of executions) { { timeout: Math.max(1, startedAtMs + deadlineMs - Date.now()) }) .catch((cause: unknown) => new Error("Could not capture task creation response", { cause })) : null; - turnSubmissionTimesMs.push( - await createTaskThroughUi({ + const createdTask = await createTaskThroughUi({ page, issuePrefix, agentName: fixtures.agent.name, @@ -1115,8 +1114,9 @@ for (const execution of executions) { prompt, workMode: execution.task.workMode, projectName: fixtures.project?.name, - }), - ); + requireExplicitTitle: execution.suite.id === "task-titles" && Boolean(title), + }); + turnSubmissionTimesMs.push(createdTask.submittedAtMs); const deadlineAt = startedAtMs + deadlineMs; if (titleCreationResponse) { @@ -1131,9 +1131,9 @@ for (const execution of executions) { deadlineAt, load: async () => { const issues = await api.get( - `/api/companies/${fixtures!.company.id}/issues?q=${encodeURIComponent(title)}&limit=50`, + `/api/companies/${fixtures!.company.id}/issues?limit=50`, ); - return issues.find((candidate) => candidate.title === title); + return issues.find((candidate) => candidate.id === createdTask.issueId); }, accept: (candidate): candidate is IssueRecord => Boolean(candidate), }); diff --git a/tests/runner-e2e/user-actions.ts b/tests/runner-e2e/user-actions.ts index 70c88b41271..812d24cce00 100644 --- a/tests/runner-e2e/user-actions.ts +++ b/tests/runner-e2e/user-actions.ts @@ -8,6 +8,7 @@ export async function createTaskThroughUi(input: { prompt: string; workMode: "standard" | "planning" | "ask"; projectName?: string; + requireExplicitTitle?: boolean; }) { const issuesUrl = `/${encodeURIComponent(input.issuePrefix)}/issues`; const newTask = input.page.getByRole("button", { name: "New Task" }).first(); @@ -33,41 +34,42 @@ export async function createTaskThroughUi(input: { ); } await newTask.click(); - await input.page.getByPlaceholder("Task title").fill(input.title); - await input.page - .getByRole("dialog") + const dialog = input.page.getByRole("dialog", { name: "New task", exact: true }); + const titleInput = dialog.getByRole("textbox", { name: "Task title", exact: true }); + if (await titleInput.isVisible()) await titleInput.fill(input.title); + else if (input.requireExplicitTitle) throw new Error("This title-preservation case requires a visible explicit title input"); + await dialog .getByRole("textbox", { name: "editable markdown", exact: true }) .fill(input.prompt); if (input.workMode !== "standard") { - await input.page - .getByRole("dialog") - .locator(`[data-issue-work-mode-chip="standard"]`) - .click(); - await input.page - .locator(`[data-issue-work-mode="${input.workMode}"]`) - .click(); + await dialog.getByRole("button", { name: "Add to composer", exact: true }).click(); + await input.page.getByTestId(input.workMode === "planning" ? "composer-add-plan" : "composer-add-ask").click(); } - await input.page - .getByRole("button", { name: "Assignee", exact: true }) + await dialog + .getByRole("button", { name: "Select assignee", exact: true }) .click(); - await input.page - .getByPlaceholder("Search assignees...") - .fill(input.agentName); - await input.page.getByText(input.agentName, { exact: true }).last().click(); + await input.page.getByRole("searchbox", { name: "Search assignees", exact: true }).fill(input.agentName); + await input.page.getByRole("option").filter({ has: input.page.getByText(input.agentName, { exact: true }) }).click(); + await expect(input.page.getByRole("searchbox", { name: "Search assignees", exact: true })).toBeHidden(); if (input.projectName) { - const dialog = input.page.getByRole("dialog"); - // Selecting the assignee advances focus to this selector and opens it. - // Focus is idempotent here; clicking would toggle an already-open popover - // closed before the search field can be filled. - await dialog.getByRole("button", { name: "Project", exact: true }).focus(); - await dialog.getByPlaceholder("Search projects...").fill(input.projectName); - await dialog.getByText(input.projectName, { exact: true }).last().click(); + await dialog.getByRole("button", { name: "Project", exact: true }).click(); + await input.page.getByPlaceholder("Search projects...").fill(input.projectName); + await input.page.getByText(input.projectName, { exact: true }).last().click(); } const submittedAtMs = Date.now(); - await input.page - .getByRole("button", { name: "Create Task", exact: true }) - .click(); - return submittedAtMs; + // The prompt-only composer generates a provisional title which the provider + // can rename immediately. Bind the fixture to the actual creation response. + const [response] = await Promise.all([ + input.page.waitForResponse(response => response.request().method() === "POST" + && /^\/api\/companies\/[^/]+\/issues$/.test(new URL(response.url()).pathname)), + dialog.getByRole("button", { name: "Create task", exact: true }).click(), + ]); + expect(response.status()).toBe(201); + const issue = await response.json() as { id: string; companyId: string }; + expect(new URL(response.url()).pathname).toBe(`/api/companies/${issue.companyId}/issues`); + expect(issue.id).toEqual(expect.any(String)); + expect(issue.id.length).toBeGreaterThan(0); + return { submittedAtMs, issueId: issue.id }; } export async function submitTaskReply(