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>
44 lines
2.0 KiB
TypeScript
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");
|
|
});
|
|
});
|