feat(HAI-045): normalize queued status for file-scope-deferred tasks
- Set status to queued in scheduler for file-scope-deferred tasks - Normalize queued badge display in TaskCard component - Add TaskCard unit tests for queued status rendering - Add scheduler tests for file-scope-deferred task queuing - Move logo.svg to correct public directory
This commit is contained in:
@@ -65,7 +65,7 @@ export function TaskCard({ task, queued, onOpenDetail, addToast }: TaskCardProps
|
||||
>
|
||||
<div className="card-header">
|
||||
<span className="card-id">{task.id}</span>
|
||||
{task.status && (
|
||||
{task.status && task.status !== "queued" && (
|
||||
<span
|
||||
className={`card-status-badge${ACTIVE_STATUSES.has(task.status) ? " pulsing" : ""}`}
|
||||
style={{
|
||||
@@ -98,14 +98,14 @@ export function TaskCard({ task, queued, onOpenDetail, addToast }: TaskCardProps
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{((task.dependencies && task.dependencies.length > 0) || queued) && (
|
||||
{((task.dependencies && task.dependencies.length > 0) || queued || task.status === "queued") && (
|
||||
<div className="card-meta">
|
||||
{task.dependencies && task.dependencies.length > 0 && (
|
||||
<span className="card-dep-badge">
|
||||
<Link size={12} style={{ verticalAlign: 'middle' }} /> {task.dependencies.length} dep{task.dependencies.length > 1 ? "s" : ""}
|
||||
</span>
|
||||
)}
|
||||
{queued && <span className="queued-badge"><Clock size={12} style={{ verticalAlign: 'middle' }} /> Queued</span>}
|
||||
{(queued || task.status === "queued") && <span className="queued-badge"><Clock size={12} style={{ verticalAlign: 'middle' }} /> Queued</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -90,3 +90,46 @@ describe("TaskCard agent-active class", () => {
|
||||
expect(cls).toContain("queued");
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskCard queued badge logic", () => {
|
||||
/** Mirrors the card-status-badge visibility condition from TaskCard.tsx */
|
||||
function shouldShowStatusBadge(status?: string | null): boolean {
|
||||
return !!status && status !== "queued";
|
||||
}
|
||||
|
||||
/** Mirrors the queued-badge visibility condition from TaskCard.tsx */
|
||||
function shouldShowQueuedBadge(opts: { queued?: boolean; status?: string | null }): boolean {
|
||||
return !!(opts.queued || opts.status === "queued");
|
||||
}
|
||||
|
||||
it("shows queued-badge when queued prop is true", () => {
|
||||
expect(shouldShowQueuedBadge({ queued: true })).toBe(true);
|
||||
});
|
||||
|
||||
it("shows queued-badge when task.status is 'queued'", () => {
|
||||
expect(shouldShowQueuedBadge({ status: "queued" })).toBe(true);
|
||||
});
|
||||
|
||||
it("shows queued-badge when both queued prop and status are set", () => {
|
||||
expect(shouldShowQueuedBadge({ queued: true, status: "queued" })).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT show queued-badge when neither queued prop nor status is 'queued'", () => {
|
||||
expect(shouldShowQueuedBadge({ queued: false, status: "executing" })).toBe(false);
|
||||
expect(shouldShowQueuedBadge({})).toBe(false);
|
||||
});
|
||||
|
||||
it("does NOT show card-status-badge when status is 'queued'", () => {
|
||||
expect(shouldShowStatusBadge("queued")).toBe(false);
|
||||
});
|
||||
|
||||
it("shows card-status-badge for non-queued statuses", () => {
|
||||
expect(shouldShowStatusBadge("executing")).toBe(true);
|
||||
expect(shouldShowStatusBadge("planning")).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT show card-status-badge when status is null/undefined", () => {
|
||||
expect(shouldShowStatusBadge(null)).toBe(false);
|
||||
expect(shouldShowStatusBadge(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,3 +122,70 @@ describe("Scheduler concurrency", () => {
|
||||
expect(store.moveTask).toHaveBeenCalledWith("HAI-003", "in-progress");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Scheduler file-scope overlap", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
async function runSchedule(scheduler: Scheduler): Promise<void> {
|
||||
(scheduler as any).running = true;
|
||||
await scheduler.schedule();
|
||||
}
|
||||
|
||||
it("sets status 'queued' for a todo task deferred due to file scope overlap", async () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-002", column: "todo" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
// Enable file scope grouping
|
||||
store.getSettings.mockResolvedValue({
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 15000,
|
||||
groupOverlappingFiles: true,
|
||||
autoMerge: false,
|
||||
});
|
||||
// Both tasks share overlapping file scopes
|
||||
store.parseFileScopeFromPrompt.mockImplementation(async (id: string) => {
|
||||
if (id === "HAI-001") return ["packages/shared/utils.ts"];
|
||||
if (id === "HAI-002") return ["packages/shared/utils.ts"];
|
||||
return [];
|
||||
});
|
||||
|
||||
const scheduler = new Scheduler(store, { maxConcurrent: 3 });
|
||||
await runSchedule(scheduler);
|
||||
|
||||
// HAI-002 should NOT be moved to in-progress (deferred)
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
// HAI-002 should have status set to "queued"
|
||||
expect(store.updateTask).toHaveBeenCalledWith("HAI-002", { status: "queued" });
|
||||
});
|
||||
|
||||
it("does not set status 'queued' when file scopes do not overlap", async () => {
|
||||
const tasks = [
|
||||
makeTask({ id: "HAI-001", column: "in-progress" }),
|
||||
makeTask({ id: "HAI-002", column: "todo" }),
|
||||
];
|
||||
const store = createMockStore(tasks);
|
||||
store.getSettings.mockResolvedValue({
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 15000,
|
||||
groupOverlappingFiles: true,
|
||||
autoMerge: false,
|
||||
});
|
||||
store.parseFileScopeFromPrompt.mockImplementation(async (id: string) => {
|
||||
if (id === "HAI-001") return ["packages/a/file.ts"];
|
||||
if (id === "HAI-002") return ["packages/b/file.ts"];
|
||||
return [];
|
||||
});
|
||||
|
||||
const scheduler = new Scheduler(store, { maxConcurrent: 3 });
|
||||
await runSchedule(scheduler);
|
||||
|
||||
// HAI-002 should be moved (no overlap)
|
||||
expect(store.moveTask).toHaveBeenCalledWith("HAI-002", "in-progress");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -215,6 +215,7 @@ export class Scheduler {
|
||||
console.log(
|
||||
`[scheduler] Deferring ${task.id}: file overlap with ${overlappingTaskId}`,
|
||||
);
|
||||
await this.store.updateTask(task.id, { status: "queued" });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user