Files
fusion/packages/dashboard/app/components/__tests__/MailboxTaskProposal.test.tsx
gsxdsm 7c23771433 FN-8265: add task follow-up proposal creation
Enable configured ephemeral workers to propose and create follow-up tasks from mailbox messages.

- Add persisted task-proposal claim state, migrations, and async messaging APIs.
- Register task-proposal creation routes, SSE events, agent tool support, and CLI integration.
- Add mailbox creation controls, settings, documentation, localization, and regression coverage.

Files changed:
 .changeset/fn-8265-task-follow-up-policy.md        |   7 ++
 docs/dashboard-guide.md                            |   2 +-
 docs/settings-reference.md                         |  12 ++-
 packages/cli/src/extension.ts                      |  34 ++++---
 .../src/__tests__/postgres/sqlite-migrator.test.ts |  14 ++-
 .../postgres/task-proposal-claim.pg.test.ts        |  51 +++++++++++
 packages/core/src/async-message-store.ts           |  39 ++++++++
 packages/core/src/index.gate.ts                    |   4 +-
 packages/core/src/index.ts                         |   4 +-
 packages/core/src/message-store.ts                 |  46 ++++++++++
 .../core/src/postgres/migrations/0000_initial.sql  |   2 +
 .../migrations/0020_task_proposal_claim.sql        |   4 +
 packages/core/src/postgres/schema-applier.ts       |  13 ++-
 packages/core/src/postgres/schema/project.ts       |   3 +
 packages/core/src/settings-schema.ts               |  19 +++-
 packages/core/src/task-store/async-persistence.ts  |   2 +-
 packages/core/src/task-store/persistence.ts        |   4 +-
 packages/core/src/task-store/serialization.ts      |   1 +
 packages/core/src/task-store/task-creation.ts      |  51 +++++++++++
 packages/core/src/task-store/task-row-mappers.ts   |   2 +-
 packages/core/src/types.ts                         |  56 +++++++++++-
 packages/dashboard/app/api/legacy.ts               |   5 +
 packages/dashboard/app/components/MailboxModal.tsx |   4 +
 .../app/components/MailboxTaskProposal.css         |   3 +
 .../app/components/MailboxTaskProposal.tsx         |  33 +++++++
 packages/dashboard/app/components/MailboxView.tsx  |   4 +
 .../__tests__/MailboxTaskProposal.test.tsx         |  43 +++++++++
 .../app/components/settings/section-keys.ts        |   2 +-
 .../settings/sections/GeneralSection.search.ts     |  13 ++-
 .../settings/sections/GeneralSection.tsx           |  22 +++--
 .../settings-default-descriptions.test.tsx         |   4 +-
 .../routes/__tests__/task-proposal-routes.test.ts  |  99 ++++++++++++++++++++
 .../src/routes/register-messaging-scripts.ts       | 101 +++++++++++++++++++++
 packages/dashboard/src/sse.ts                      |   7 ++
 packages/engine/src/agent-tools.ts                 |  30 ++++--
 packages/engine/src/executor.ts                    |   9 +-
 packages/engine/src/step-session-executor.ts       |   8 +-
 packages/i18n/locales/en/app.json                  |   5 +
 38 files changed, 696 insertions(+), 66 deletions(-)

Fusion-Task-Id: FN-8265

Fusion-Task-Lineage: 4e864a2f-3485-4a54-8be7-1699b5479a94

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-18 03:48:38 -07:00

44 lines
2.0 KiB
TypeScript

import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import type { Message, MessageMetadata } from "@fusion/core";
import { describe, expect, it, vi } from "vitest";
import { createProposedTask } from "../../api";
import { MailboxTaskProposal } from "../MailboxTaskProposal";
vi.mock("../../api", () => ({ createProposedTask: vi.fn() }));
const proposalMetadata: MessageMetadata = {
kind: "task-proposal",
proposalStatus: "pending",
proposalIdempotencyKey: "proposal-key",
proposedTask: { title: "Follow up", description: "Implement the follow-up." },
};
function createdMessage(): Message {
return {
id: "message-1", fromId: "agent-1", fromType: "agent", toId: "dashboard-user", toType: "user",
content: "Proposal", type: "agent-to-user", read: false, metadata: {
...proposalMetadata, proposalStatus: "created", createdTaskId: "FN-8265",
}, createdAt: "2026-07-30T00:00:00.000Z", updatedAt: "2026-07-30T00:00:00.000Z",
};
}
describe("MailboxTaskProposal", () => {
it("renders nothing for non-proposal metadata", () => {
const { container } = render(<MailboxTaskProposal messageId="message-1" metadata={{}} />);
expect(container).toBeEmptyDOMElement();
});
it("replaces Create task with the created-task affordance from the successful response", async () => {
const onOpenTask = vi.fn();
vi.mocked(createProposedTask).mockResolvedValue({ task: { id: "FN-8265" } as never, proposal: createdMessage() });
render(<MailboxTaskProposal messageId="message-1" metadata={proposalMetadata} projectId="project-1" onOpenTask={onOpenTask} />);
fireEvent.click(screen.getByRole("button", { name: "Create task" }));
await waitFor(() => expect(screen.getByRole("button", { name: "Task FN-8265 created — View task" })).toBeInTheDocument());
expect(createProposedTask).toHaveBeenCalledWith("message-1", "project-1");
fireEvent.click(screen.getByRole("button", { name: "Task FN-8265 created — View task" }));
expect(onOpenTask).toHaveBeenCalledWith("FN-8265");
});
});