FN-6327: add engineer backlog auto-claim controls
Expose engineer backlog auto-claim configuration in dashboard settings and agent heartbeat settings. - Add a project-level Scheduling & Capacity checkbox for engineer backlog auto-claim. - Add a per-agent Heartbeat Settings checkbox that persists runtimeConfig.engineerBacklogAutoClaim. - Cover the new settings UI with dashboard component tests. - Document the project default and per-agent override locations and add a release changeset. Files changed: .../FN-6327-engineer-backlog-auto-claim-ui.md | 5 + docs/settings-reference.md | 4 +- .../dashboard/app/components/AgentDetailView.tsx | 25 ++++- .../AgentDetailView.advanced-settings.test.tsx | 104 +++++++++++++++++++++ .../components/__tests__/SettingsModal.test.tsx | 66 +++++++++++++ .../settings/sections/SchedulingSection.tsx | 14 +++ 6 files changed, 215 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-6327 Fusion-Task-Lineage: fddc39bc-f50e-42fa-ae67-7c3eca59cbeb
This commit is contained in:
5
.changeset/FN-6327-engineer-backlog-auto-claim-ui.md
Normal file
5
.changeset/FN-6327-engineer-backlog-auto-claim-ui.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add dashboard controls for the engineer backlog auto-claim opt-in at project scope and per-agent heartbeat settings.
|
||||||
@@ -295,7 +295,7 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
|
|||||||
| `heartbeatScopeDiscipline` | `"strict" \| "lite" \| "off"` | `"strict"` | Heartbeat prompt procedure mode. `strict` keeps coordination-heavy scope discipline, `lite` restores pre-2026-05-11 wording, and `off` uses a minimal procedure. Per-agent `runtimeConfig.heartbeatScopeDiscipline` can override this default. |
|
| `heartbeatScopeDiscipline` | `"strict" \| "lite" \| "off"` | `"strict"` | Heartbeat prompt procedure mode. `strict` keeps coordination-heavy scope discipline, `lite` restores pre-2026-05-11 wording, and `off` uses a minimal procedure. Per-agent `runtimeConfig.heartbeatScopeDiscipline` can override this default. |
|
||||||
| `heartbeatPromptTemplate` | `"default" \| "compact"` | `"default"` | Heartbeat execution-prompt trim template default. Per-agent `runtimeConfig.heartbeatPromptTemplate` overrides this value. Role fallback when unset everywhere is `executor`→`default`, non-executor coordination roles→`compact`. |
|
| `heartbeatPromptTemplate` | `"default" \| "compact"` | `"default"` | Heartbeat execution-prompt trim template default. Per-agent `runtimeConfig.heartbeatPromptTemplate` overrides this value. Role fallback when unset everywhere is `executor`→`default`, non-executor coordination roles→`compact`. |
|
||||||
| `autoClaimCandidatesInPrompt` | `number` | `5` | Default no-task heartbeat candidate list length. Integer range `0-10`; `0` suppresses candidate prompt injection. |
|
| `autoClaimCandidatesInPrompt` | `number` | `5` | Default no-task heartbeat candidate list length. Integer range `0-10`; `0` suppresses candidate prompt injection. |
|
||||||
| `engineerBacklogAutoClaim` | `boolean` | `false` | Opt engineer-role agents into no-task backlog auto-claim for implementation tasks. The default remains executor-only; per-agent `runtimeConfig.engineerBacklogAutoClaim` overrides this project default, and explicit routing/delegation is unchanged. |
|
| `engineerBacklogAutoClaim` | `boolean` | `false` | Opt engineer-role agents into no-task backlog auto-claim for implementation tasks. The default remains executor-only; per-agent `runtimeConfig.engineerBacklogAutoClaim` overrides this project default, and explicit routing/delegation is unchanged. Configure the project default in **Settings → Scheduling & Capacity → Let engineer agents auto-claim backlog tasks**; configure the per-agent override in **Agents → Agent Detail → Settings → Heartbeat Settings → Engineer Backlog Auto-Claim**. |
|
||||||
| `defaultNodeId` | `string` | `undefined` | Optional project default execution node for task dispatch. When set, tasks without a per-task `nodeId` override resolve to this node (`routing source: project-default`). See [Task Management → Node Routing](./task-management.md#node-routing). |
|
| `defaultNodeId` | `string` | `undefined` | Optional project default execution node for task dispatch. When set, tasks without a per-task `nodeId` override resolve to this node (`routing source: project-default`). See [Task Management → Node Routing](./task-management.md#node-routing). |
|
||||||
| `unavailableNodePolicy` | `"block" \| "fallback-local"` | `"block"` | Project routing policy used during scheduler dispatch when a task resolves to a remote node and node health is known. `"block"` keeps the task in `todo` if the node is unhealthy; `"fallback-local"` reroutes dispatch to local execution. See [Architecture → Task Routing Architecture](./architecture.md#task-routing-architecture). |
|
| `unavailableNodePolicy` | `"block" \| "fallback-local"` | `"block"` | Project routing policy used during scheduler dispatch when a task resolves to a remote node and node health is known. `"block"` keeps the task in `todo` if the node is unhealthy; `"fallback-local"` reroutes dispatch to local execution. See [Architecture → Task Routing Architecture](./architecture.md#task-routing-architecture). |
|
||||||
| `secretsAccessPolicy` | `"auto" \| "prompt" \| "deny"` | `undefined` | Project-level default secret access policy (overrides global default when present). |
|
| `secretsAccessPolicy` | `"auto" \| "prompt" \| "deny"` | `undefined` | Project-level default secret access policy (overrides global default when present). |
|
||||||
@@ -1173,7 +1173,7 @@ Common heartbeat/runtime keys on `runtimeConfig` include:
|
|||||||
| `selfImproveIntervalMs` | `number` | Delay between self-improvement cycles (default 4h, minimum 1h) |
|
| `selfImproveIntervalMs` | `number` | Delay between self-improvement cycles (default 4h, minimum 1h) |
|
||||||
| `lastSelfImproveAt` | `string` | Last self-improvement checkpoint timestamp (managed by heartbeat monitor) |
|
| `lastSelfImproveAt` | `string` | Last self-improvement checkpoint timestamp (managed by heartbeat monitor) |
|
||||||
|
|
||||||
Configure these per agent in **Agents → Agent Detail → Settings → Heartbeat Settings** (dashboard), or by updating agent `runtimeConfig` via the Agents API/CLI config flows.
|
Configure these per agent in **Agents → Agent Detail → Settings → Heartbeat Settings** (dashboard), or by updating agent `runtimeConfig` via the Agents API/CLI config flows. The **Engineer Backlog Auto-Claim** checkbox in this card controls `runtimeConfig.engineerBacklogAutoClaim` for that agent and only affects no-task backlog pickup; explicit assignment and delegation behavior are unchanged.
|
||||||
|
|
||||||
These examples show agents configured to use Paperclip, Hermes, and OpenClaw runtime hints:
|
These examples show agents configured to use Paperclip, Hermes, and OpenClaw runtime hints:
|
||||||
|
|
||||||
|
|||||||
@@ -3310,6 +3310,10 @@ function deriveAutoClaimRelevantTasksEnabled(runtimeConfig: AgentDetail["runtime
|
|||||||
return runtimeConfig?.autoClaimRelevantTasks !== false;
|
return runtimeConfig?.autoClaimRelevantTasks !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deriveEngineerBacklogAutoClaim(runtimeConfig: AgentDetail["runtimeConfig"] | undefined): boolean {
|
||||||
|
return runtimeConfig?.engineerBacklogAutoClaim === true;
|
||||||
|
}
|
||||||
|
|
||||||
function deriveRunMissedHeartbeatOnStartup(runtimeConfig: AgentDetail["runtimeConfig"] | undefined): boolean {
|
function deriveRunMissedHeartbeatOnStartup(runtimeConfig: AgentDetail["runtimeConfig"] | undefined): boolean {
|
||||||
return runtimeConfig?.runMissedHeartbeatOnStartup === true;
|
return runtimeConfig?.runMissedHeartbeatOnStartup === true;
|
||||||
}
|
}
|
||||||
@@ -3692,6 +3696,9 @@ function ConfigTab({
|
|||||||
const [autoClaimRelevantTasksEnabled, setAutoClaimRelevantTasksEnabled] = useState<boolean>(
|
const [autoClaimRelevantTasksEnabled, setAutoClaimRelevantTasksEnabled] = useState<boolean>(
|
||||||
() => deriveAutoClaimRelevantTasksEnabled(agent.runtimeConfig),
|
() => deriveAutoClaimRelevantTasksEnabled(agent.runtimeConfig),
|
||||||
);
|
);
|
||||||
|
const [engineerBacklogAutoClaimEnabled, setEngineerBacklogAutoClaimEnabled] = useState<boolean>(
|
||||||
|
() => deriveEngineerBacklogAutoClaim(agent.runtimeConfig),
|
||||||
|
);
|
||||||
const [runMissedHeartbeatOnStartup, setRunMissedHeartbeatOnStartup] = useState<boolean>(
|
const [runMissedHeartbeatOnStartup, setRunMissedHeartbeatOnStartup] = useState<boolean>(
|
||||||
() => deriveRunMissedHeartbeatOnStartup(agent.runtimeConfig),
|
() => deriveRunMissedHeartbeatOnStartup(agent.runtimeConfig),
|
||||||
);
|
);
|
||||||
@@ -3979,6 +3986,7 @@ function ConfigTab({
|
|||||||
const rc = agent.runtimeConfig ?? {};
|
const rc = agent.runtimeConfig ?? {};
|
||||||
if (heartbeatEnabled !== deriveHeartbeatEnabled(agent.runtimeConfig)) return true;
|
if (heartbeatEnabled !== deriveHeartbeatEnabled(agent.runtimeConfig)) return true;
|
||||||
if (autoClaimRelevantTasksEnabled !== deriveAutoClaimRelevantTasksEnabled(agent.runtimeConfig)) return true;
|
if (autoClaimRelevantTasksEnabled !== deriveAutoClaimRelevantTasksEnabled(agent.runtimeConfig)) return true;
|
||||||
|
if (engineerBacklogAutoClaimEnabled !== deriveEngineerBacklogAutoClaim(agent.runtimeConfig)) return true;
|
||||||
if (runMissedHeartbeatOnStartup !== deriveRunMissedHeartbeatOnStartup(agent.runtimeConfig)) return true;
|
if (runMissedHeartbeatOnStartup !== deriveRunMissedHeartbeatOnStartup(agent.runtimeConfig)) return true;
|
||||||
if (allowParallelExecution !== deriveAllowParallelExecution(agent.runtimeConfig)) return true;
|
if (allowParallelExecution !== deriveAllowParallelExecution(agent.runtimeConfig)) return true;
|
||||||
if (skipHeartbeatWhenIdle !== deriveSkipHeartbeatWhenIdle(agent.runtimeConfig)) return true;
|
if (skipHeartbeatWhenIdle !== deriveSkipHeartbeatWhenIdle(agent.runtimeConfig)) return true;
|
||||||
@@ -4058,6 +4066,7 @@ function ConfigTab({
|
|||||||
setHeartbeatValues(deriveHeartbeatValues(agent.runtimeConfig));
|
setHeartbeatValues(deriveHeartbeatValues(agent.runtimeConfig));
|
||||||
setHeartbeatEnabled(deriveHeartbeatEnabled(agent.runtimeConfig));
|
setHeartbeatEnabled(deriveHeartbeatEnabled(agent.runtimeConfig));
|
||||||
setAutoClaimRelevantTasksEnabled(deriveAutoClaimRelevantTasksEnabled(agent.runtimeConfig));
|
setAutoClaimRelevantTasksEnabled(deriveAutoClaimRelevantTasksEnabled(agent.runtimeConfig));
|
||||||
|
setEngineerBacklogAutoClaimEnabled(deriveEngineerBacklogAutoClaim(agent.runtimeConfig));
|
||||||
setRunMissedHeartbeatOnStartup(deriveRunMissedHeartbeatOnStartup(agent.runtimeConfig));
|
setRunMissedHeartbeatOnStartup(deriveRunMissedHeartbeatOnStartup(agent.runtimeConfig));
|
||||||
setAllowParallelExecution(deriveAllowParallelExecution(agent.runtimeConfig));
|
setAllowParallelExecution(deriveAllowParallelExecution(agent.runtimeConfig));
|
||||||
setSkipHeartbeatWhenIdle(deriveSkipHeartbeatWhenIdle(agent.runtimeConfig));
|
setSkipHeartbeatWhenIdle(deriveSkipHeartbeatWhenIdle(agent.runtimeConfig));
|
||||||
@@ -4216,6 +4225,7 @@ function ConfigTab({
|
|||||||
const newRuntimeConfig: Record<string, unknown> = { ...agent.runtimeConfig };
|
const newRuntimeConfig: Record<string, unknown> = { ...agent.runtimeConfig };
|
||||||
newRuntimeConfig.enabled = heartbeatEnabled;
|
newRuntimeConfig.enabled = heartbeatEnabled;
|
||||||
newRuntimeConfig.autoClaimRelevantTasks = autoClaimRelevantTasksEnabled;
|
newRuntimeConfig.autoClaimRelevantTasks = autoClaimRelevantTasksEnabled;
|
||||||
|
newRuntimeConfig.engineerBacklogAutoClaim = engineerBacklogAutoClaimEnabled;
|
||||||
newRuntimeConfig.runMissedHeartbeatOnStartup = runMissedHeartbeatOnStartup;
|
newRuntimeConfig.runMissedHeartbeatOnStartup = runMissedHeartbeatOnStartup;
|
||||||
newRuntimeConfig.allowParallelExecution = allowParallelExecution;
|
newRuntimeConfig.allowParallelExecution = allowParallelExecution;
|
||||||
newRuntimeConfig.skipHeartbeatWhenIdle = skipHeartbeatWhenIdle;
|
newRuntimeConfig.skipHeartbeatWhenIdle = skipHeartbeatWhenIdle;
|
||||||
@@ -4326,7 +4336,7 @@ function ConfigTab({
|
|||||||
runtimeConfig: newRuntimeConfig,
|
runtimeConfig: newRuntimeConfig,
|
||||||
bundleConfig: newBundleConfig,
|
bundleConfig: newBundleConfig,
|
||||||
};
|
};
|
||||||
}, [agent.metadata, agent.runtimeConfig, allowParallelExecution, autoClaimRelevantTasksEnabled, budgetValues, bundleEntryFile, bundleExternalPath, bundleFiles, bundleMode, formValues, heartbeatEnabled, heartbeatPromptTemplate, heartbeatScopeDiscipline, heartbeatValues, iconValue, modelValue, nameValue, reportsToValue, roleValue, runMissedHeartbeatOnStartup, runtimeMode, selectedRuntimeId, selectedSkills, skipHeartbeatWhenIdle, titleValue, validationErrors]);
|
}, [agent.metadata, agent.runtimeConfig, allowParallelExecution, autoClaimRelevantTasksEnabled, budgetValues, bundleEntryFile, bundleExternalPath, bundleFiles, bundleMode, engineerBacklogAutoClaimEnabled, formValues, heartbeatEnabled, heartbeatPromptTemplate, heartbeatScopeDiscipline, heartbeatValues, iconValue, modelValue, nameValue, reportsToValue, roleValue, runMissedHeartbeatOnStartup, runtimeMode, selectedRuntimeId, selectedSkills, skipHeartbeatWhenIdle, titleValue, validationErrors]);
|
||||||
|
|
||||||
const persistSettings = useCallback(async (showValidationToast: boolean, source: "auto" | "manual") => {
|
const persistSettings = useCallback(async (showValidationToast: boolean, source: "auto" | "manual") => {
|
||||||
const payload = buildSavePayload();
|
const payload = buildSavePayload();
|
||||||
@@ -4769,6 +4779,19 @@ function ConfigTab({
|
|||||||
{t("agents.autoClaimRelevantTasks", "Auto-Claim Relevant Tasks")}
|
{t("agents.autoClaimRelevantTasks", "Auto-Claim Relevant Tasks")}
|
||||||
</label>
|
</label>
|
||||||
<span className="config-hint">{t("agents.autoClaimHint", "When enabled (default), no-task heartbeats scan open unowned work and auto-claim tasks aligned with this agent's role and soul.")}</span>
|
<span className="config-hint">{t("agents.autoClaimHint", "When enabled (default), no-task heartbeats scan open unowned work and auto-claim tasks aligned with this agent's role and soul.")}</span>
|
||||||
|
<label className="checkbox-label" htmlFor="hb-engineerBacklogAutoClaim">
|
||||||
|
<input
|
||||||
|
id="hb-engineerBacklogAutoClaim"
|
||||||
|
type="checkbox"
|
||||||
|
checked={engineerBacklogAutoClaimEnabled}
|
||||||
|
onChange={(e) => {
|
||||||
|
setEngineerBacklogAutoClaimEnabled(e.target.checked);
|
||||||
|
void scheduleAutoSave();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{t("agents.engineerBacklogAutoClaim", "Engineer Backlog Auto-Claim")}
|
||||||
|
</label>
|
||||||
|
<span className="config-hint">{t("agents.engineerBacklogAutoClaimHint", "Per-agent override of the project default. Allows this engineer-role agent to auto-claim unowned backlog tasks; explicit assignment and delegation are unchanged.")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="config-field">
|
<div className="config-field">
|
||||||
|
|||||||
@@ -709,6 +709,35 @@ describe("Advanced Settings", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["undefined", undefined, false],
|
||||||
|
["false", false, false],
|
||||||
|
["true", true, true],
|
||||||
|
] as const)("renders engineer backlog auto-claim unchecked by default and reflects %s runtimeConfig", async (_label, engineerBacklogAutoClaim, expectedChecked) => {
|
||||||
|
mockFetchAgent.mockResolvedValue(createMockAgent({
|
||||||
|
role: "engineer",
|
||||||
|
runtimeConfig: {
|
||||||
|
heartbeatIntervalMs: 30000,
|
||||||
|
...(engineerBacklogAutoClaim === undefined ? {} : { engineerBacklogAutoClaim }),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(
|
||||||
|
<AgentDetailView
|
||||||
|
agentId="agent-001"
|
||||||
|
onClose={vi.fn()}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await navigateToSettings(user);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect((screen.getByLabelText("Engineer Backlog Auto-Claim") as HTMLInputElement).checked).toBe(expectedChecked);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("shows Save Settings button disabled when no changes", async () => {
|
it("shows Save Settings button disabled when no changes", async () => {
|
||||||
mockFetchAgent.mockResolvedValue(createMockAgent({ metadata: {} }));
|
mockFetchAgent.mockResolvedValue(createMockAgent({ metadata: {} }));
|
||||||
|
|
||||||
@@ -1018,6 +1047,81 @@ describe("Advanced Settings", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("persists engineer backlog auto-claim enabled override on save", async () => {
|
||||||
|
mockFetchAgent.mockResolvedValue(createMockAgent({
|
||||||
|
role: "engineer",
|
||||||
|
runtimeConfig: {
|
||||||
|
enabled: true,
|
||||||
|
heartbeatIntervalMs: 30000,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
mockUpdateAgent.mockResolvedValue(createMockAgent() as any);
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(
|
||||||
|
<AgentDetailView
|
||||||
|
agentId="agent-001"
|
||||||
|
onClose={vi.fn()}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await navigateToSettings(user);
|
||||||
|
|
||||||
|
const toggle = await screen.findByLabelText("Engineer Backlog Auto-Claim");
|
||||||
|
expect((toggle as HTMLInputElement).checked).toBe(false);
|
||||||
|
await user.click(toggle);
|
||||||
|
await user.click(screen.getByText("Save Settings"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockUpdateAgent).toHaveBeenCalledWith(
|
||||||
|
"agent-001",
|
||||||
|
expect.objectContaining({
|
||||||
|
runtimeConfig: expect.objectContaining({ engineerBacklogAutoClaim: true }),
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("persists engineer backlog auto-claim disabled override on save", async () => {
|
||||||
|
mockFetchAgent.mockResolvedValue(createMockAgent({
|
||||||
|
role: "engineer",
|
||||||
|
runtimeConfig: {
|
||||||
|
enabled: true,
|
||||||
|
engineerBacklogAutoClaim: true,
|
||||||
|
heartbeatIntervalMs: 30000,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
mockUpdateAgent.mockResolvedValue(createMockAgent() as any);
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(
|
||||||
|
<AgentDetailView
|
||||||
|
agentId="agent-001"
|
||||||
|
onClose={vi.fn()}
|
||||||
|
addToast={vi.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await navigateToSettings(user);
|
||||||
|
|
||||||
|
const toggle = await screen.findByLabelText("Engineer Backlog Auto-Claim");
|
||||||
|
expect((toggle as HTMLInputElement).checked).toBe(true);
|
||||||
|
await user.click(toggle);
|
||||||
|
await user.click(screen.getByText("Save Settings"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockUpdateAgent).toHaveBeenCalledWith(
|
||||||
|
"agent-001",
|
||||||
|
expect.objectContaining({
|
||||||
|
runtimeConfig: expect.objectContaining({ engineerBacklogAutoClaim: false }),
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("applies coordination-only preset and persists disabled auto-claim", async () => {
|
it("applies coordination-only preset and persists disabled auto-claim", async () => {
|
||||||
mockFetchAgent.mockResolvedValue(createMockAgent({
|
mockFetchAgent.mockResolvedValue(createMockAgent({
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
|
|||||||
@@ -2648,6 +2648,72 @@ describe("SettingsModal", () => {
|
|||||||
const payload = mockUpdateSettings.mock.calls[0][0] as Record<string, unknown>;
|
const payload = mockUpdateSettings.mock.calls[0][0] as Record<string, unknown>;
|
||||||
expect(payload.heartbeatScopeDiscipline).toBe("off");
|
expect(payload.heartbeatScopeDiscipline).toBe("off");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["undefined", undefined, false],
|
||||||
|
["false", false, false],
|
||||||
|
["true", true, true],
|
||||||
|
] as const)("renders engineer backlog auto-claim from %s project setting", async (_label, engineerBacklogAutoClaim, expectedChecked) => {
|
||||||
|
mockFetchSettings.mockResolvedValue({
|
||||||
|
...defaultSettings,
|
||||||
|
...(engineerBacklogAutoClaim === undefined ? {} : { engineerBacklogAutoClaim }),
|
||||||
|
});
|
||||||
|
|
||||||
|
renderModal();
|
||||||
|
await waitFor(() => expect(mockFetchSettings).toHaveBeenCalled());
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Scheduling & Capacity"));
|
||||||
|
|
||||||
|
expect((screen.getByLabelText("Let engineer agents auto-claim backlog tasks") as HTMLInputElement).checked).toBe(expectedChecked);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes enabled engineer backlog auto-claim through the project settings save payload", async () => {
|
||||||
|
mockFetchSettings.mockResolvedValue({
|
||||||
|
...defaultSettings,
|
||||||
|
engineerBacklogAutoClaim: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
renderModal();
|
||||||
|
await waitFor(() => expect(mockFetchSettings).toHaveBeenCalled());
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Scheduling & Capacity"));
|
||||||
|
|
||||||
|
const toggle = screen.getByLabelText("Let engineer agents auto-claim backlog tasks") as HTMLInputElement;
|
||||||
|
expect(toggle.checked).toBe(false);
|
||||||
|
await userEvent.click(toggle);
|
||||||
|
await userEvent.click(screen.getByText("Save"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockUpdateSettings).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = mockUpdateSettings.mock.calls[0][0] as Record<string, unknown>;
|
||||||
|
expect(payload.engineerBacklogAutoClaim).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes disabled engineer backlog auto-claim through the project settings save payload", async () => {
|
||||||
|
mockFetchSettings.mockResolvedValue({
|
||||||
|
...defaultSettings,
|
||||||
|
engineerBacklogAutoClaim: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
renderModal();
|
||||||
|
await waitFor(() => expect(mockFetchSettings).toHaveBeenCalled());
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Scheduling & Capacity"));
|
||||||
|
|
||||||
|
const toggle = screen.getByLabelText("Let engineer agents auto-claim backlog tasks") as HTMLInputElement;
|
||||||
|
expect(toggle.checked).toBe(true);
|
||||||
|
await userEvent.click(toggle);
|
||||||
|
await userEvent.click(screen.getByText("Save"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockUpdateSettings).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = mockUpdateSettings.mock.calls[0][0] as Record<string, unknown>;
|
||||||
|
expect(payload.engineerBacklogAutoClaim).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Number input clearing", () => {
|
describe("Number input clearing", () => {
|
||||||
|
|||||||
@@ -126,6 +126,20 @@ export function SchedulingSection({
|
|||||||
</select>
|
</select>
|
||||||
<small>Strict — coordination-focused; higher per-tick tokens. Lite — pre-2026-05-11 behavior. Off — minimal procedure.</small>
|
<small>Strict — coordination-focused; higher per-tick tokens. Lite — pre-2026-05-11 behavior. Off — minimal procedure.</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="engineerBacklogAutoClaim" className="checkbox-label">
|
||||||
|
<input
|
||||||
|
id="engineerBacklogAutoClaim"
|
||||||
|
type="checkbox"
|
||||||
|
checked={form.engineerBacklogAutoClaim === true}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((f) => ({ ...f, engineerBacklogAutoClaim: e.target.checked }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Let engineer agents auto-claim backlog tasks
|
||||||
|
</label>
|
||||||
|
<small>Backlog/no-task auto-claim is executor-only by default. Enable to let engineer-role agents auto-claim unowned backlog tasks; explicit routing and delegation are unchanged. Default: off.</small>
|
||||||
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="taskStuckTimeoutMs">Stuck Task Timeout (minutes)</label>
|
<label htmlFor="taskStuckTimeoutMs">Stuck Task Timeout (minutes)</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
Reference in New Issue
Block a user