chore(FN-4350): import dependency content from fusion/fn-4326
Squash-imported the working tree of fusion/fn-4326 as a single commit so this branch carries the dep's content without inheriting its individual commits. If the dep is later squash-merged to main, this commit's patch-id should match the merge and rebase cleanly. Fusion-Task-Id: FN-4350 Fusion-Task-Lineage: 11fff2c2-1cd1-4562-9b01-032a15217479
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { createTaskStoreTestHarness } from "./store-test-helpers.js";
|
||||
|
||||
describe("TaskStore moveTask preserveStatus", () => {
|
||||
const harness = createTaskStoreTestHarness();
|
||||
|
||||
beforeEach(harness.beforeEach);
|
||||
afterEach(harness.afterEach);
|
||||
|
||||
it("clears status/error by default when moving in-progress to todo", async () => {
|
||||
const task = await harness.store().createTask({ description: "preserveStatus default clear" });
|
||||
await harness.store().moveTask(task.id, "todo");
|
||||
await harness.store().moveTask(task.id, "in-progress");
|
||||
await harness.store().updateTask(task.id, {
|
||||
status: "failed",
|
||||
error: "boom",
|
||||
});
|
||||
|
||||
const moved = await harness.store().moveTask(task.id, "todo");
|
||||
expect(moved.status).toBeUndefined();
|
||||
expect(moved.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves status/error when preserveStatus is true on in-progress to todo", async () => {
|
||||
const task = await harness.store().createTask({ description: "preserveStatus true in-progress" });
|
||||
await harness.store().moveTask(task.id, "todo");
|
||||
await harness.store().moveTask(task.id, "in-progress");
|
||||
await harness.store().updateTask(task.id, {
|
||||
status: "failed",
|
||||
error: "branch conflict",
|
||||
});
|
||||
|
||||
const moved = await harness.store().moveTask(task.id, "todo", { preserveStatus: true });
|
||||
expect(moved.status).toBe("failed");
|
||||
expect(moved.error).toBe("branch conflict");
|
||||
});
|
||||
|
||||
it("preserves status/error on in-review to todo when preserveStatus is true", async () => {
|
||||
const task = await harness.store().createTask({ description: "preserveStatus true in-review" });
|
||||
await harness.store().moveTask(task.id, "todo");
|
||||
await harness.store().moveTask(task.id, "in-progress");
|
||||
await harness.store().moveTask(task.id, "in-review");
|
||||
await harness.store().updateTask(task.id, {
|
||||
status: "failed",
|
||||
error: "recovery exhausted",
|
||||
});
|
||||
|
||||
const moved = await harness.store().moveTask(task.id, "todo", { preserveStatus: true });
|
||||
expect(moved.status).toBe("failed");
|
||||
expect(moved.error).toBe("recovery exhausted");
|
||||
});
|
||||
});
|
||||
@@ -3461,6 +3461,12 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
* clear the worktree.
|
||||
*/
|
||||
preserveWorktree?: boolean;
|
||||
/**
|
||||
* When true, do not clear task.status/task.error/task.pausedReason on
|
||||
* reopen-to-todo/triage transitions. Required so recovery handlers that
|
||||
* bounce through todo can keep sticky failed state context.
|
||||
*/
|
||||
preserveStatus?: boolean;
|
||||
/**
|
||||
* When transitioning to in-progress on a task that has no worktree
|
||||
* assigned, invoke this allocator to pick a path. The store calls
|
||||
@@ -3541,8 +3547,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
&& (toColumn === "todo" || toColumn === "triage");
|
||||
|
||||
if (isReopenToTodoOrTriage) {
|
||||
task.status = undefined;
|
||||
task.error = undefined;
|
||||
if (!options?.preserveStatus) {
|
||||
task.status = undefined;
|
||||
task.error = undefined;
|
||||
task.pausedReason = undefined;
|
||||
}
|
||||
task.blockedBy = undefined;
|
||||
task.paused = undefined;
|
||||
task.pausedByAgentId = undefined;
|
||||
@@ -3675,7 +3684,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
async updateTask(
|
||||
id: string,
|
||||
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; currentStep?: number; blockedBy?: string | null; assignedAgentId?: string | null; pausedByAgentId?: string | null; assigneeUserId?: string | null; scopeOverride?: boolean | null; scopeOverrideReason?: string | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; checkoutNodeId?: string | null; checkoutRunId?: string | null; checkoutLeaseRenewedAt?: string | null; checkoutLeaseEpoch?: number | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; executionStartBranch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; postReviewFixCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; verificationFailureCount?: number | null; mergeConflictBounceCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; executionStartedAt?: string | null; executionCompletedAt?: string | null; review?: import("./types.js").TaskReview | null; reviewState?: import("./types.js").TaskReviewState | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; githubTracking?: import("./types.js").TaskGithubTracking | null; tokenUsage?: import("./types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
|
||||
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; currentStep?: number; blockedBy?: string | null; assignedAgentId?: string | null; pausedByAgentId?: string | null; pausedReason?: string | null; dispatchStormCount?: number | null; lastDispatchAt?: string | null; assigneeUserId?: string | null; scopeOverride?: boolean | null; scopeOverrideReason?: string | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; checkoutNodeId?: string | null; checkoutRunId?: string | null; checkoutLeaseRenewedAt?: string | null; checkoutLeaseEpoch?: number | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; executionStartBranch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; postReviewFixCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; verificationFailureCount?: number | null; mergeConflictBounceCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; executionStartedAt?: string | null; executionCompletedAt?: string | null; review?: import("./types.js").TaskReview | null; reviewState?: import("./types.js").TaskReviewState | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; githubTracking?: import("./types.js").TaskGithubTracking | null; tokenUsage?: import("./types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
|
||||
runContext?: RunMutationContext,
|
||||
): Promise<Task> {
|
||||
return this.withTaskLock(id, async () => {
|
||||
@@ -3803,6 +3812,21 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
} else if (updates.pausedByAgentId !== undefined) {
|
||||
task.pausedByAgentId = updates.pausedByAgentId;
|
||||
}
|
||||
if (updates.pausedReason === null) {
|
||||
task.pausedReason = undefined;
|
||||
} else if (updates.pausedReason !== undefined) {
|
||||
task.pausedReason = updates.pausedReason;
|
||||
}
|
||||
if (updates.dispatchStormCount === null) {
|
||||
task.dispatchStormCount = undefined;
|
||||
} else if (updates.dispatchStormCount !== undefined) {
|
||||
task.dispatchStormCount = updates.dispatchStormCount;
|
||||
}
|
||||
if (updates.lastDispatchAt === null) {
|
||||
task.lastDispatchAt = undefined;
|
||||
} else if (updates.lastDispatchAt !== undefined) {
|
||||
task.lastDispatchAt = updates.lastDispatchAt;
|
||||
}
|
||||
if (updates.assigneeUserId === null) {
|
||||
task.assigneeUserId = undefined;
|
||||
} else if (updates.assigneeUserId !== undefined) {
|
||||
|
||||
@@ -1162,6 +1162,12 @@ export interface Task {
|
||||
blockedBy?: string;
|
||||
/** When true, all automated agent and scheduler interaction is suspended. */
|
||||
paused?: boolean;
|
||||
/** Optional machine-readable reason for automated pauses (for example dispatch-storm). */
|
||||
pausedReason?: string;
|
||||
/** Dispatch-storm cycle counter tracked by scheduler for todo↔in-progress loop detection. */
|
||||
dispatchStormCount?: number;
|
||||
/** ISO timestamp of the most recent dispatch-storm cycle increment. */
|
||||
lastDispatchAt?: string;
|
||||
/** When set, this task was paused because the agent with this ID was paused. Cleared when the agent resumes. Distinct from user-initiated pause. */
|
||||
pausedByAgentId?: string;
|
||||
/** Configured merge target/base branch for this task (task intent).
|
||||
|
||||
Reference in New Issue
Block a user