Q130: every open task says why it is not moving, when it resumes, and who must act
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
271a83a7bc
commit
074a1fc8e3
14 files changed
+255
-7
No files matched your search
@@ -0,0 +1,108 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { DatabaseConnection } from "../../src/main/database/connection";
|
||||
import { OnboardingService } from "../../src/main/services/onboarding-service";
|
||||
import { ProviderService } from "../../src/main/services/provider-service";
|
||||
import { providerAdapters } from "../../src/main/services/provider-adapters";
|
||||
import { TaskExecutionService } from "../../src/main/services/task-execution-service";
|
||||
import { SnapshotService } from "../../src/main/services/snapshot-service";
|
||||
import { currentProviderUsageWindow } from "../../src/shared/provider-runtime-domain";
|
||||
import type { AgentTurnResult } from "../../src/main/services/provider-tool-turn";
|
||||
|
||||
const testVault = {
|
||||
encryptSecret: (secret: string) => Buffer.from(secret, "utf8"),
|
||||
decryptSecret: (ciphertext: Buffer) => ciphertext.toString("utf8"),
|
||||
};
|
||||
|
||||
/** A company whose kickoff task is open with no run in flight. */
|
||||
function company() {
|
||||
const turn = async (): Promise<AgentTurnResult> => ({ text: "Done.", toolCalls: [], rawResponseJson: "{}" });
|
||||
const connection = new DatabaseConnection(join(tmpdir(), `ac-task-holds-${randomUUID()}.db`));
|
||||
const providers = new ProviderService(connection.db, providerAdapters, testVault, turn);
|
||||
const onboarding = new OnboardingService({ db: connection.db, saveProviderCredential: (input) => providers.saveCredential(input) });
|
||||
const execution = new TaskExecutionService(connection.db, providers);
|
||||
let orgId = "";
|
||||
let ceoId = "";
|
||||
connection.transaction(() => {
|
||||
const summary = onboarding.bootstrap({
|
||||
companyName: "FlowPilot", companyDescription: "CSM analytics.", employeeName: "Yang", workspacePath: "",
|
||||
executionMode: "ai_ceo", autoApproveHires: true, reviewDeliverables: false,
|
||||
goalTitle: "Ship the MVP.", goalDescription: "A working product.",
|
||||
providerKey: "openai", providerSecret: "sk-test-key", providerModel: "gpt-5.4",
|
||||
});
|
||||
orgId = summary.orgId;
|
||||
ceoId = summary.ceoAgentId;
|
||||
});
|
||||
const run = connection.db.prepare("select id, task_id, provider_id from task_runs").get() as { id: string; task_id: string; provider_id: string };
|
||||
connection.db.prepare("update task_runs set state = 'failed' where id = ?").run(run.id);
|
||||
const hold = () => new SnapshotService(connection.db).load().tasks.find((task) => task.id === run.task_id)!.hold;
|
||||
return { connection, providers, execution, orgId, ceoId, taskId: run.task_id, providerId: run.provider_id, hold };
|
||||
}
|
||||
|
||||
describe("why an open task is not moving", () => {
|
||||
test("an open task with nothing in its way has no hold", () => {
|
||||
const h = company();
|
||||
expect(h.hold()).toBeNull();
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("a provider cooldown says when the task resumes on its own", () => {
|
||||
const h = company();
|
||||
const until = Date.now() + 90_000;
|
||||
h.connection.db.prepare("insert into provider_cooldowns(provider_id, label, until_ms, strikes) values (?, 'OpenAI', ?, 2)").run(h.providerId, until);
|
||||
expect(h.hold()).toEqual({ reason: "OpenAI reported a passing failure; its work waits out a cooldown.", resumesAt: new Date(until).toISOString(), waitingOn: "provider" });
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("a spent monthly request limit waits on the operator or the next window, and a zero limit only on the operator", () => {
|
||||
const h = company();
|
||||
const window = currentProviderUsageWindow();
|
||||
h.connection.db.prepare("update providers set monthly_request_limit = 2 where id = ?").run(h.providerId);
|
||||
h.connection.db.prepare(`
|
||||
insert into provider_usage_windows(id, org_id, provider_id, window_start, window_end, request_count, input_char_count, output_char_count, created_at, updated_at)
|
||||
values ('puw_t', ?, ?, ?, ?, 2, 0, 0, ?, ?)
|
||||
`).run(h.orgId, h.providerId, window.start, window.end, new Date().toISOString(), new Date().toISOString());
|
||||
expect(h.hold()).toEqual({ reason: "OpenAI used its monthly request limit (2). Raise the limit or wait for the next window.", resumesAt: window.end, waitingOn: "operator" });
|
||||
h.connection.db.prepare("update providers set monthly_request_limit = 0 where id = ?").run(h.providerId);
|
||||
expect(h.hold()).toEqual({ reason: "OpenAI has a monthly request limit of 0. Raise the limit to let its work run.", resumesAt: null, waitingOn: "operator" });
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("a spent money budget waits on the operator or next month, and a zero budget only on the operator", () => {
|
||||
const h = company();
|
||||
h.providers.setPrices({ orgId: h.orgId, providerId: h.providerId, inputDollarsPerMtok: 2.5, cachedInputDollarsPerMtok: null, outputDollarsPerMtok: 10 });
|
||||
h.connection.db.prepare("update agents set budget_cents = 100 where id = ?").run(h.ceoId);
|
||||
h.connection.db.prepare(`
|
||||
insert into provider_requests(id, org_id, provider_id, provider_key, agent_id, model, prompt_sha256, state, created_at, cost_cents)
|
||||
values ('preq_t', ?, ?, 'openai', ?, 'gpt-5.4', 'x', 'succeeded', ?, 150)
|
||||
`).run(h.orgId, h.providerId, h.ceoId, new Date().toISOString());
|
||||
expect(h.hold()).toEqual({
|
||||
reason: "CEO has spent its monthly budget ($1.50 of $1.00). Raise the budget or wait for next month.",
|
||||
resumesAt: currentProviderUsageWindow().end,
|
||||
waitingOn: "operator",
|
||||
});
|
||||
h.connection.db.prepare("update agents set budget_cents = 0 where id = ?").run(h.ceoId);
|
||||
expect(h.hold()).toEqual({ reason: "CEO has spent its monthly budget ($1.50 of $0.00). Raise the budget to let it run.", resumesAt: null, waitingOn: "operator" });
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("a heartbeat that could not queue the task records why, until it can", () => {
|
||||
const h = company();
|
||||
h.connection.db.prepare("update tasks set risk = 'high' where id = ?").run(h.taskId);
|
||||
expect(h.connection.transaction(() => h.execution.enqueueHeartbeatRuns())).toEqual([]);
|
||||
expect(h.hold()).toEqual({ reason: "Could not start: Approved task execution approval is required.", resumesAt: null, waitingOn: "operator" });
|
||||
h.connection.db.prepare("update tasks set risk = 'low' where id = ?").run(h.taskId);
|
||||
expect(h.connection.transaction(() => h.execution.enqueueHeartbeatRuns())).toHaveLength(1);
|
||||
expect(h.hold()).toBeNull();
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("a stalled task waits on the operator with no resume time", () => {
|
||||
const h = company();
|
||||
h.connection.db.prepare("update tasks set stalled_reason = 'Stalled: 3 sessions made no progress.' where id = ?").run(h.taskId);
|
||||
expect(h.hold()).toEqual({ reason: "Stalled: 3 sessions made no progress.", resumesAt: null, waitingOn: "operator" });
|
||||
h.connection.close();
|
||||
});
|
||||
});
|
||||
@@ -35,7 +35,7 @@ describe("domain services", () => {
|
||||
const connection = createConnection();
|
||||
|
||||
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92]);
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user