feat: add dispatch retry and import recovery flows

This commit is contained in:
kris
2026-03-31 22:10:03 +08:00
parent be31503d22
commit dcbff3cc7d
15 changed files with 776 additions and 23 deletions

View File

@@ -10,6 +10,7 @@ let postMessageRoute: (typeof import("../src/app/api/v1/projects/[projectId]/mes
let getDispatchPlansRoute: (typeof import("../src/app/api/v1/projects/[projectId]/dispatch-plans/route"))["GET"];
let confirmDispatchPlanRoute: (typeof import("../src/app/api/v1/projects/[projectId]/dispatch-plans/[planId]/confirm/route"))["POST"];
let rejectDispatchPlanRoute: (typeof import("../src/app/api/v1/projects/[projectId]/dispatch-plans/[planId]/reject/route"))["POST"];
let retryDispatchPlanRoute: (typeof import("../src/app/api/v1/projects/[projectId]/dispatch-plans/[planId]/retry/route"))["POST"];
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
let createProjectGroupChat: (typeof import("../src/lib/boss-data"))["createProjectGroupChat"];
let isDispatchableThreadProject: (typeof import("../src/lib/boss-data"))["isDispatchableThreadProject"];
@@ -26,11 +27,12 @@ async function setup() {
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const [messageModule, plansModule, confirmModule, rejectModule, data, auth] = await Promise.all([
const [messageModule, plansModule, confirmModule, rejectModule, retryModule, data, auth] = await Promise.all([
import("../src/app/api/v1/projects/[projectId]/messages/route.ts"),
import("../src/app/api/v1/projects/[projectId]/dispatch-plans/route.ts"),
import("../src/app/api/v1/projects/[projectId]/dispatch-plans/[planId]/confirm/route.ts"),
import("../src/app/api/v1/projects/[projectId]/dispatch-plans/[planId]/reject/route.ts"),
import("../src/app/api/v1/projects/[projectId]/dispatch-plans/[planId]/retry/route.ts"),
import("../src/lib/boss-data.ts"),
import("../src/lib/boss-auth.ts"),
]);
@@ -39,6 +41,7 @@ async function setup() {
getDispatchPlansRoute = plansModule.GET;
confirmDispatchPlanRoute = confirmModule.POST;
rejectDispatchPlanRoute = rejectModule.POST;
retryDispatchPlanRoute = retryModule.POST;
createAuthSession = data.createAuthSession;
createProjectGroupChat = data.createProjectGroupChat;
isDispatchableThreadProject = data.isDispatchableThreadProject;
@@ -317,3 +320,65 @@ test("rejecting a dispatch plan marks approval_required groups as rejected and w
);
assert.ok(notice, "expected rejection notice in group chat");
});
test("retrying a rejected dispatch plan creates a fresh pending recommendation and resets approval gate", async () => {
const { groupProject, dispatchPlan } = await createDispatchPlanForTest();
const state = await readState();
await writeState({
...state,
projects: state.projects.map((project) =>
project.id === groupProject.id
? {
...project,
collaborationMode: "approval_required" as const,
approvalState: "pending_user" as const,
}
: project,
),
});
const rejectResponse = await rejectDispatchPlanRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${groupProject.id}/dispatch-plans/${dispatchPlan.planId}/reject`,
"POST",
{},
),
{ params: Promise.resolve({ projectId: groupProject.id, planId: dispatchPlan.planId }) },
);
assert.equal(rejectResponse.status, 200);
const retryResponse = await retryDispatchPlanRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${groupProject.id}/dispatch-plans/${dispatchPlan.planId}/retry`,
"POST",
{},
),
{ params: Promise.resolve({ projectId: groupProject.id, planId: dispatchPlan.planId }) },
);
assert.equal(retryResponse.status, 200);
const retryPayload = (await retryResponse.json()) as {
ok: boolean;
dispatchPlan: { planId: string; status: string; requestMessageId: string } | null;
collaborationGate: {
approvalState: string;
requiresMasterAgentApproval: boolean;
collaborationMode: string;
};
};
assert.equal(retryPayload.ok, true);
assert.ok(retryPayload.dispatchPlan, "expected a fresh dispatch recommendation");
assert.notEqual(retryPayload.dispatchPlan?.planId, dispatchPlan.planId);
assert.equal(retryPayload.dispatchPlan?.status, "pending_user_confirmation");
assert.match(retryPayload.dispatchPlan?.requestMessageId ?? "", /:retry:/);
assert.equal(retryPayload.collaborationGate.collaborationMode, "approval_required");
assert.equal(retryPayload.collaborationGate.requiresMasterAgentApproval, true);
assert.equal(retryPayload.collaborationGate.approvalState, "pending_user");
const nextState = await readState();
const refreshedPlan = nextState.dispatchPlans.find((plan) => plan.planId === retryPayload.dispatchPlan?.planId);
assert.ok(refreshedPlan, "expected retried dispatch plan in state");
const nextGroupProject = nextState.projects.find((project) => project.id === groupProject.id);
assert.equal(nextGroupProject?.approvalState, "pending_user");
});