@desplega.ai/agent-swarm 1.113.0 → 1.115.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/{actions-q4n7cz6e.js → actions-17ya5wde.js} +8 -9
- package/dist/{app-es4nzc71.js → app-4f718yk0.js} +6 -6
- package/dist/{assistant-0x8ey0vs.js → assistant-ah7csnjz.js} +19 -18
- package/dist/{boot-reembed-69agkhv6.js → boot-reembed-4jpxhb25.js} +6 -6
- package/dist/{boot-reembed-p0ny05t8.js → boot-reembed-qata34xa.js} +5 -5
- package/dist/{boot-scrub-logs-yybrkmvr.js → boot-scrub-logs-tv48nn3z.js} +4 -4
- package/dist/{claude-adapter-mma9a1yr.js → claude-adapter-3csn5bks.js} +3 -3
- package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-kysmkz89.js} +2 -2
- package/dist/cli-0pygq9r1.js +429 -0
- package/dist/{cli-hsetkkd3.js → cli-12vb0xg6.js} +33 -9
- package/dist/{cli-ncfgsqbd.js → cli-15y7qhzp.js} +262 -33
- package/dist/{cli-7ga43spr.js → cli-1yjrbg1f.js} +1 -1
- package/dist/{cli-36ymmpmp.js → cli-3samkyap.js} +2 -2
- package/dist/cli-52fg7s7x.js +269 -0
- package/dist/{cli-hjhvekf4.js → cli-65ekje01.js} +11 -8
- package/dist/{cli-h3k622d0.js → cli-6aq8xhvm.js} +90 -3
- package/dist/{cli-f3qa069v.js → cli-7xje5amz.js} +1 -1
- package/dist/cli-86fdnerz.js +405 -0
- package/dist/{cli-9cy6nc5m.js → cli-8fjwym53.js} +1 -1
- package/dist/{cli-c3frqf65.js → cli-b23axgz4.js} +3 -3
- package/dist/{cli-a0t7vapf.js → cli-bc89my0z.js} +1 -1
- package/dist/{cli-bnzbn8j8.js → cli-beea4a3y.js} +73 -24
- package/dist/{cli-q21d49ac.js → cli-bw80ck94.js} +1 -1
- package/dist/{cli-pwc5e83c.js → cli-cchksf5q.js} +12 -7
- package/dist/{cli-30bbaveh.js → cli-cmw9rzy5.js} +2 -2
- package/dist/{cli-y6mrptcn.js → cli-cnwssmsr.js} +1 -1
- package/dist/{cli-r5g6ngtn.js → cli-dcca6j8m.js} +46 -5
- package/dist/{cli-tkmm5nf9.js → cli-e61fs2hp.js} +59 -2
- package/dist/{cli-6wscj9z0.js → cli-e7306swv.js} +14 -14
- package/dist/{cli-dcfyyh3t.js → cli-ftyf58xj.js} +2 -2
- package/dist/{cli-4xyj9jtq.js → cli-hrhye2rm.js} +1062 -478
- package/dist/cli-jkhwmmgg.js +136 -0
- package/dist/{cli-y4ycrnjc.js → cli-k3y4z3h5.js} +1 -1
- package/dist/{cli-6xd0bvf8.js → cli-ma04faw5.js} +1 -1
- package/dist/{cli-2fc97xm5.js → cli-mnbgv3w7.js} +4 -4
- package/dist/{cli-trmapn9k.js → cli-pet3aev6.js} +95 -10
- package/dist/{cli-z46pnksz.js → cli-q1rx7g0d.js} +175 -28
- package/dist/{cli-qwgtacd6.js → cli-r9f6ngrf.js} +1244 -336
- package/dist/{cli-gtkqc144.js → cli-shvaxw27.js} +13 -7
- package/dist/{cli-ywez6yps.js → cli-sq7f72d6.js} +1 -1
- package/dist/cli-t0vqevnm.js +393 -0
- package/dist/{cli-tbw13sx8.js → cli-tece6zgm.js} +1 -1
- package/dist/{cli-bgvskydy.js → cli-tffvrh6j.js} +1 -1
- package/dist/cli-wh91zby6.js +187222 -0
- package/dist/{cli-anrj584m.js → cli-xz9aq0rf.js} +1 -1
- package/dist/{cli-mq580e06.js → cli-zsrdty74.js} +18 -18
- package/dist/cli.js +32 -15
- package/dist/{codex-adapter-5cmy858w.js → codex-adapter-de3w0nch.js} +10 -10
- package/dist/{codex-session-runner-46q8mrba.js → codex-session-runner-bnw8cysk.js} +10 -10
- package/dist/{commands-z5dbwta3.js → commands-hch4e2k1.js} +4 -4
- package/dist/{db-bbgahh4z.js → db-p3h2j06x.js} +14 -4
- package/dist/{handlers-6b44317z.js → handlers-9er4zx9q.js} +17 -31
- package/dist/{hook-sbp5fmps.js → hook-4axs756z.js} +7 -7
- package/dist/{http-2xz43gz3.js → http-809fvtz2.js} +1934 -886
- package/dist/{index-wz1ea15p.js → index-2dgfj9av.js} +3 -3
- package/dist/{index-nhat35em.js → index-47em3d2n.js} +14 -11
- package/dist/{index-5ghxn8s6.js → index-9fqaktjr.js} +12 -9
- package/dist/{index-0gzmqj4k.js → index-9ht8gr56.js} +13 -10
- package/dist/{index-vfedgdw6.js → index-rm85cfdx.js} +368 -40
- package/dist/{index-ykvs43z4.js → index-t2snazez.js} +4 -4
- package/dist/{keepalive-62gfj91d.js → keepalive-3cps6e3e.js} +7 -6
- package/dist/{lead-ag0akn0v.js → lead-fkk95wgq.js} +36 -32
- package/dist/{maintenance-xxrrzk5x.js → maintenance-fweqh0yj.js} +6 -6
- package/dist/{mistral-conversations-26yrnbf1.js → mistral-conversations-7gqnz2wt.js} +7 -7
- package/dist/oauth-refresh-sweep-sycqtbph.js +114 -0
- package/dist/{onboard-64zyfcbs.js → onboard-cy0rkgvx.js} +2 -2
- package/dist/{opencode-adapter-mqsxr170.js → opencode-adapter-p6bxnbm4.js} +2 -2
- package/dist/{otel-zjd9zaxs.js → otel-g299k41b.js} +2 -2
- package/dist/{otel-impl-b86qseaa.js → otel-impl-wbns8hf7.js} +1 -1
- package/dist/{pi-mono-adapter-kxjxpfgy.js → pi-mono-adapter-05sck1ds.js} +17 -109
- package/dist/{pricing-refresh-cgtkffpa.js → pricing-refresh-0hvazmkf.js} +6 -6
- package/dist/rbac-roles-kfesjy9y.js +28 -0
- package/dist/rbac-roles-mwcwhbxc.js +283 -0
- package/dist/{seed-pricing-1r1y1xkq.js → seed-pricing-h2kdq4cq.js} +5 -5
- package/dist/{setup-mkcgkjf7.js → setup-8506q97w.js} +2 -2
- package/dist/{worker-351sze63.js → worker-4r5c6zyb.js} +36 -32
- package/openapi.json +1327 -20
- package/package.json +3 -3
- package/src/agentmail/handlers.ts +16 -6
- package/src/agentmail/templates.ts +25 -5
- package/src/be/audit-user.ts +28 -4
- package/src/be/db-queries/oauth.ts +42 -0
- package/src/be/db.ts +314 -28
- package/src/be/identity.ts +79 -0
- package/src/be/mcp-proxy.ts +132 -0
- package/src/be/migrations/109_rbac_roles.sql +79 -0
- package/src/be/migrations/111_oauth_credential_bindings.sql +6 -0
- package/src/be/migrations/112_script_connections_graphql.sql +96 -0
- package/src/be/migrations/113_task_routing_affinity.sql +9 -0
- package/src/be/migrations/114_backfill_gpt_5_6_pricing.sql +15 -0
- package/src/be/modelsdev-cache.json +135 -0
- package/src/be/oauth-credential-bindings.ts +35 -0
- package/src/be/oauth-refresh-sweep.ts +159 -0
- package/src/be/rbac-audit.ts +52 -13
- package/src/be/rbac-roles.ts +424 -0
- package/src/be/script-connections.ts +459 -40
- package/src/be/script-credential-broker.ts +29 -3
- package/src/be/scripts/typecheck.ts +51 -7
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +325 -24
- package/src/be/seed-scripts/catalog/catalog-report.ts +325 -24
- package/src/be/users.ts +24 -0
- package/src/cli.tsx +17 -0
- package/src/github/handlers.ts +10 -9
- package/src/github/templates.ts +45 -9
- package/src/gitlab/handlers.ts +20 -8
- package/src/gitlab/templates.ts +18 -6
- package/src/heartbeat/heartbeat.ts +143 -21
- package/src/http/all-routes.ts +3 -0
- package/src/http/approval-requests.ts +12 -0
- package/src/http/config.ts +44 -10
- package/src/http/core.ts +30 -3
- package/src/http/favorites.ts +1 -5
- package/src/http/index.ts +25 -1
- package/src/http/mcp-oauth.ts +2 -0
- package/src/http/mcp-servers.ts +44 -3
- package/src/http/oauth-generic.ts +105 -0
- package/src/http/poll.ts +20 -3
- package/src/http/script-connection-proxy.ts +116 -0
- package/src/http/script-connections.ts +1657 -0
- package/src/http/scripts.ts +27 -7
- package/src/http/skills.ts +11 -0
- package/src/http/tasks.ts +27 -13
- package/src/http/x.ts +6 -2
- package/src/integrations/kapso/inbound.ts +23 -6
- package/src/jira/sync.ts +111 -17
- package/src/jira/templates.ts +10 -2
- package/src/mcp-client/http-client.ts +194 -0
- package/src/oauth/app-validation.ts +16 -0
- package/src/oauth/ensure-token.ts +69 -14
- package/src/oauth/mcp-wrapper.ts +15 -0
- package/src/oauth/wrapper.ts +48 -20
- package/src/prompts/session-templates.ts +3 -3
- package/src/providers/codex-models.ts +100 -30
- package/src/providers/modelsdev-reasoning.json +51 -0
- package/src/providers/pi-mono-mcp-client.ts +2 -133
- package/src/providers/reasoning-effort.ts +11 -5
- package/src/rbac/admission.ts +68 -0
- package/src/rbac/index.ts +2 -0
- package/src/rbac/legacy-policy.ts +12 -0
- package/src/rbac/permissions.ts +48 -0
- package/src/scheduler/scheduler.ts +9 -1
- package/src/scripts-runtime/api-client.ts +151 -19
- package/src/scripts-runtime/api-types.ts +38 -6
- package/src/scripts-runtime/credential-broker/default-bindings.ts +1 -0
- package/src/scripts-runtime/credential-broker/types.ts +8 -0
- package/src/scripts-runtime/ctx.ts +11 -1
- package/src/scripts-runtime/eval-harness.ts +5 -1
- package/src/scripts-runtime/executors/types.ts +2 -1
- package/src/scripts-runtime/loader.ts +3 -1
- package/src/scripts-runtime/mcp-client.ts +87 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +6 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +19 -0
- package/src/server-user.ts +114 -59
- package/src/server.ts +12 -0
- package/src/slack/assistant.ts +14 -6
- package/src/slack/enrich.ts +17 -0
- package/src/slack/handlers.ts +8 -22
- package/src/slack/responses.ts +91 -1
- package/src/slack/thread-buffer.ts +6 -3
- package/src/slack/watcher.ts +9 -6
- package/src/tasks/worker-follow-up.ts +100 -3
- package/src/tests/agentmail-handlers.test.ts +32 -3
- package/src/tests/approval-requests.test.ts +61 -0
- package/src/tests/audit-user.test.ts +47 -1
- package/src/tests/codex-adapter.test.ts +46 -23
- package/src/tests/credential-broker.test.ts +5 -2
- package/src/tests/delete-page-tool.test.ts +197 -0
- package/src/tests/github-handlers.test.ts +28 -2
- package/src/tests/gitlab-handlers.test.ts +125 -0
- package/src/tests/heartbeat.test.ts +48 -3
- package/src/tests/identity.test.ts +117 -0
- package/src/tests/jira-sync.test.ts +99 -1
- package/src/tests/kapso-inbound.test.ts +7 -0
- package/src/tests/mcp-tools-user.test.ts +60 -6
- package/src/tests/model-control.test.ts +8 -2
- package/src/tests/oauth-credential-bindings.test.ts +490 -0
- package/src/tests/oauth-refresh-sweep.test.ts +160 -0
- package/src/tests/pool-affinity.test.ts +401 -0
- package/src/tests/prompt-template-remaining.test.ts +2 -2
- package/src/tests/rbac-admission-e2e.test.ts +386 -0
- package/src/tests/rbac-admission.test.ts +492 -0
- package/src/tests/rbac-e2e-helpers.ts +3 -0
- package/src/tests/rbac-engine.test.ts +50 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +362 -0
- package/src/tests/rbac-roles.test.ts +328 -0
- package/src/tests/reasoning-effort.test.ts +15 -2
- package/src/tests/schedule-target-type.test.ts +50 -0
- package/src/tests/scheduled-tasks.test.ts +40 -0
- package/src/tests/script-apis-mcp.test.ts +37 -13
- package/src/tests/script-connections-http.test.ts +1158 -0
- package/src/tests/script-connections-mcp.test.ts +894 -0
- package/src/tests/script-connections.test.ts +754 -6
- package/src/tests/scripts-external-api.test.ts +49 -1
- package/src/tests/scripts-runtime-secret-egress.test.ts +1 -0
- package/src/tests/scripts-runtime.test.ts +291 -0
- package/src/tests/seed-scripts.test.ts +36 -0
- package/src/tests/slack-identity-resolution.test.ts +37 -3
- package/src/tests/slack-inline-output.test.ts +65 -0
- package/src/tests/slack-thread-buffer.test.ts +28 -0
- package/src/tests/tool-annotations.test.ts +1 -0
- package/src/tests/user-token-rest-auth.test.ts +140 -1
- package/src/tests/workflow-swarm-script.test.ts +44 -0
- package/src/tests/workflow-triggers-v2.test.ts +300 -1
- package/src/tools/credential-bindings/tool.ts +277 -18
- package/src/tools/delete-page.ts +145 -0
- package/src/tools/mcp-servers/mcp-server-delete.ts +7 -4
- package/src/tools/request-human-input.ts +2 -0
- package/src/tools/resolve-user.ts +77 -27
- package/src/tools/script-apis.ts +62 -3
- package/src/tools/script-connections/tool.ts +275 -31
- package/src/tools/send-task.ts +15 -1
- package/src/tools/task-action.ts +34 -2
- package/src/tools/templates.ts +53 -1
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/utils.ts +2 -0
- package/src/tools/workflows/create-workflow.ts +8 -1
- package/src/tools/workflows/update-workflow.ts +11 -2
- package/src/types.ts +90 -11
- package/src/utils/scoped-resource.ts +25 -0
- package/src/workflows/executors/human-in-the-loop.ts +1 -0
- package/src/workflows/executors/swarm-script.ts +7 -1
- package/src/workflows/triggers.ts +146 -18
- package/templates/skills/swarm-scripts/content.md +33 -8
- package/dist/cli-bgef578c.js +0 -771
- package/dist/cli-c9mtm09b.js +0 -43
- package/dist/cli-ceewqajw.js +0 -1698
- package/dist/{anthropic-messages-8nc6r2dw.js → anthropic-messages-kq3pjhbp.js} +6 -6
- package/dist/{azure-openai-responses-6yfzw3qw.js → azure-openai-responses-gm1ejtmn.js} +7 -7
- package/dist/{cli-vbraamxr.js → cli-6b4wwmm8.js} +9 -9
- package/dist/{cli-2r7s8jhp.js → cli-sznberje.js} +3 -3
- package/dist/{google-generative-ai-vfq6ymz9.js → google-generative-ai-hyrra2jm.js} +3 -3
- package/dist/{google-vertex-mj5y5rfj.js → google-vertex-8bs2pzgn.js} +3 -3
- package/dist/{openai-codex-responses-8h6kvfv8.js → openai-codex-responses-3nkvje79.js} +4 -4
- package/dist/{openai-completions-x6kjnqjd.js → openai-completions-0asftkwg.js} +11 -11
- package/dist/{openai-responses-7jwyz3ds.js → openai-responses-pmatek45.js} +10 -10
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
assignUnassignedTaskPending,
|
|
5
|
+
claimTask,
|
|
6
|
+
closeDb,
|
|
7
|
+
createAgent,
|
|
8
|
+
createTaskExtended,
|
|
9
|
+
getAgentById,
|
|
10
|
+
getDb,
|
|
11
|
+
getTaskById,
|
|
12
|
+
getUnassignedTaskIdsForAgent,
|
|
13
|
+
initDb,
|
|
14
|
+
isAgentEligibleForTask,
|
|
15
|
+
updateAgentProfile,
|
|
16
|
+
} from "../be/db";
|
|
17
|
+
import { codeLevelTriage } from "../heartbeat/heartbeat";
|
|
18
|
+
import type { RoutingAffinity } from "../types";
|
|
19
|
+
|
|
20
|
+
const TEST_DB_PATH = "./test-pool-affinity.sqlite";
|
|
21
|
+
|
|
22
|
+
describe("Pool Affinity", () => {
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
try {
|
|
25
|
+
await unlink(TEST_DB_PATH);
|
|
26
|
+
} catch {
|
|
27
|
+
// File doesn't exist
|
|
28
|
+
}
|
|
29
|
+
closeDb();
|
|
30
|
+
initDb(TEST_DB_PATH);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterAll(async () => {
|
|
34
|
+
closeDb();
|
|
35
|
+
try {
|
|
36
|
+
await unlink(TEST_DB_PATH);
|
|
37
|
+
await unlink(`${TEST_DB_PATH}-wal`);
|
|
38
|
+
await unlink(`${TEST_DB_PATH}-shm`);
|
|
39
|
+
} catch {
|
|
40
|
+
// Files may not exist
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Clean up between tests to avoid interference
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
getDb().run("DELETE FROM agent_tasks");
|
|
47
|
+
getDb().run("DELETE FROM agents");
|
|
48
|
+
getDb().run("DELETE FROM agent_log");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function affinity(overrides: Partial<RoutingAffinity>): RoutingAffinity {
|
|
52
|
+
return { capabilities: [], ...overrides };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ==========================================================================
|
|
56
|
+
// isAgentEligibleForTask matrix
|
|
57
|
+
// ==========================================================================
|
|
58
|
+
|
|
59
|
+
describe("isAgentEligibleForTask", () => {
|
|
60
|
+
test("untagged task (no routingAffinity) is eligible for anyone", () => {
|
|
61
|
+
const agent = createAgent({ name: "no-role-agent", isLead: false, status: "idle" });
|
|
62
|
+
const task = createTaskExtended("Untagged task");
|
|
63
|
+
expect(isAgentEligibleForTask(agent, task)).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("sourceAgentId bypass: own work is eligible even with a mismatched role", () => {
|
|
67
|
+
const owner = createAgent({ name: "owner", isLead: false, status: "idle" });
|
|
68
|
+
updateAgentProfile(owner.id, { role: "researcher" });
|
|
69
|
+
const ownerAgent = getAgentById(owner.id)!;
|
|
70
|
+
const task = createTaskExtended("Own work", {
|
|
71
|
+
routingAffinity: affinity({ sourceAgentId: owner.id, role: "coder" }),
|
|
72
|
+
});
|
|
73
|
+
expect(isAgentEligibleForTask(ownerAgent, task)).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("exact role match is eligible", () => {
|
|
77
|
+
const agent = createAgent({ name: "coder-1", isLead: false, status: "idle" });
|
|
78
|
+
updateAgentProfile(agent.id, { role: "coder" });
|
|
79
|
+
const coderAgent = getAgentById(agent.id)!;
|
|
80
|
+
const task = createTaskExtended("Coding task", {
|
|
81
|
+
routingAffinity: affinity({ role: "coder" }),
|
|
82
|
+
});
|
|
83
|
+
expect(isAgentEligibleForTask(coderAgent, task)).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("role mismatch is ineligible", () => {
|
|
87
|
+
const agent = createAgent({ name: "researcher-1", isLead: false, status: "idle" });
|
|
88
|
+
updateAgentProfile(agent.id, { role: "researcher" });
|
|
89
|
+
const researcherAgent = getAgentById(agent.id)!;
|
|
90
|
+
const task = createTaskExtended("Coding task", {
|
|
91
|
+
routingAffinity: affinity({ role: "coder" }),
|
|
92
|
+
});
|
|
93
|
+
expect(isAgentEligibleForTask(researcherAgent, task)).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("missing role on the agent is ineligible — no fail-open", () => {
|
|
97
|
+
const agent = createAgent({ name: "no-role-agent-2", isLead: false, status: "idle" });
|
|
98
|
+
const task = createTaskExtended("Coding task", {
|
|
99
|
+
routingAffinity: affinity({ role: "coder" }),
|
|
100
|
+
});
|
|
101
|
+
expect(isAgentEligibleForTask(agent, task)).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("missing role on the task's affinity (capabilities-only) is ineligible for a non-owner — no fail-open", () => {
|
|
105
|
+
const agent = createAgent({ name: "coder-2", isLead: false, status: "idle" });
|
|
106
|
+
updateAgentProfile(agent.id, { role: "coder" });
|
|
107
|
+
const coderAgent = getAgentById(agent.id)!;
|
|
108
|
+
const task = createTaskExtended("Capability-only task", {
|
|
109
|
+
routingAffinity: affinity({ capabilities: ["datadog"] }),
|
|
110
|
+
});
|
|
111
|
+
expect(isAgentEligibleForTask(coderAgent, task)).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("capability subset: missing a required capability is ineligible", () => {
|
|
115
|
+
const agent = createAgent({ name: "coder-3", isLead: false, status: "idle" });
|
|
116
|
+
updateAgentProfile(agent.id, { role: "coder", capabilities: ["typescript"] });
|
|
117
|
+
const coderAgent = getAgentById(agent.id)!;
|
|
118
|
+
const task = createTaskExtended("Needs datadog", {
|
|
119
|
+
routingAffinity: affinity({ role: "coder", capabilities: ["datadog"] }),
|
|
120
|
+
});
|
|
121
|
+
expect(isAgentEligibleForTask(coderAgent, task)).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("capability subset: a superset of required capabilities is eligible", () => {
|
|
125
|
+
const agent = createAgent({ name: "coder-4", isLead: false, status: "idle" });
|
|
126
|
+
updateAgentProfile(agent.id, { role: "coder", capabilities: ["typescript", "datadog"] });
|
|
127
|
+
const coderAgent = getAgentById(agent.id)!;
|
|
128
|
+
const task = createTaskExtended("Needs datadog", {
|
|
129
|
+
routingAffinity: affinity({ role: "coder", capabilities: ["datadog"] }),
|
|
130
|
+
});
|
|
131
|
+
expect(isAgentEligibleForTask(coderAgent, task)).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("kill-switch: POOL_AFFINITY_ENFORCEMENT=0 makes every task eligible", () => {
|
|
135
|
+
const previous = process.env.POOL_AFFINITY_ENFORCEMENT;
|
|
136
|
+
process.env.POOL_AFFINITY_ENFORCEMENT = "0";
|
|
137
|
+
try {
|
|
138
|
+
const agent = createAgent({ name: "researcher-2", isLead: false, status: "idle" });
|
|
139
|
+
updateAgentProfile(agent.id, { role: "researcher" });
|
|
140
|
+
const researcherAgent = getAgentById(agent.id)!;
|
|
141
|
+
const task = createTaskExtended("Coding task", {
|
|
142
|
+
routingAffinity: affinity({ role: "coder" }),
|
|
143
|
+
});
|
|
144
|
+
expect(isAgentEligibleForTask(researcherAgent, task)).toBe(true);
|
|
145
|
+
} finally {
|
|
146
|
+
if (previous === undefined) {
|
|
147
|
+
delete process.env.POOL_AFFINITY_ENFORCEMENT;
|
|
148
|
+
} else {
|
|
149
|
+
process.env.POOL_AFFINITY_ENFORCEMENT = previous;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ==========================================================================
|
|
156
|
+
// claimTask eligibility gate
|
|
157
|
+
// ==========================================================================
|
|
158
|
+
|
|
159
|
+
describe("claimTask", () => {
|
|
160
|
+
test("rejects an ineligible agent and logs task_claim_rejected_affinity", () => {
|
|
161
|
+
const researcher = createAgent({ name: "researcher-3", isLead: false, status: "idle" });
|
|
162
|
+
updateAgentProfile(researcher.id, { role: "researcher" });
|
|
163
|
+
const task = createTaskExtended("Coding task", {
|
|
164
|
+
routingAffinity: affinity({ role: "coder" }),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const result = claimTask(task.id, researcher.id);
|
|
168
|
+
expect(result).toBeNull();
|
|
169
|
+
|
|
170
|
+
const stillUnassigned = getTaskById(task.id);
|
|
171
|
+
expect(stillUnassigned?.status).toBe("unassigned");
|
|
172
|
+
|
|
173
|
+
const log = getDb()
|
|
174
|
+
.query("SELECT eventType FROM agent_log WHERE taskId = ? ORDER BY createdAt DESC LIMIT 1")
|
|
175
|
+
.get(task.id) as { eventType: string } | null;
|
|
176
|
+
expect(log?.eventType).toBe("task_claim_rejected_affinity");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("an eligible agent can claim after an ineligible agent was rejected", () => {
|
|
180
|
+
const researcher = createAgent({ name: "researcher-4", isLead: false, status: "idle" });
|
|
181
|
+
updateAgentProfile(researcher.id, { role: "researcher" });
|
|
182
|
+
const coder = createAgent({ name: "coder-5", isLead: false, status: "idle" });
|
|
183
|
+
updateAgentProfile(coder.id, { role: "coder" });
|
|
184
|
+
const task = createTaskExtended("Coding task", {
|
|
185
|
+
routingAffinity: affinity({ role: "coder" }),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(claimTask(task.id, researcher.id)).toBeNull();
|
|
189
|
+
|
|
190
|
+
const claimed = claimTask(task.id, coder.id);
|
|
191
|
+
expect(claimed).not.toBeNull();
|
|
192
|
+
expect(claimed?.status).toBe("in_progress");
|
|
193
|
+
expect(claimed?.agentId).toBe(coder.id);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("only one of two eligible agents wins a race for the same task", () => {
|
|
197
|
+
const coderA = createAgent({ name: "coder-6a", isLead: false, status: "idle" });
|
|
198
|
+
updateAgentProfile(coderA.id, { role: "coder" });
|
|
199
|
+
const coderB = createAgent({ name: "coder-6b", isLead: false, status: "idle" });
|
|
200
|
+
updateAgentProfile(coderB.id, { role: "coder" });
|
|
201
|
+
const task = createTaskExtended("Coding task", {
|
|
202
|
+
routingAffinity: affinity({ role: "coder" }),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const first = claimTask(task.id, coderA.id);
|
|
206
|
+
const second = claimTask(task.id, coderB.id);
|
|
207
|
+
|
|
208
|
+
expect([first, second].filter((r) => r !== null).length).toBe(1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("own sourceAgentId can reclaim its own affinity-tagged task", () => {
|
|
212
|
+
const agent = createAgent({ name: "owner-2", isLead: false, status: "idle" });
|
|
213
|
+
updateAgentProfile(agent.id, { role: "researcher" });
|
|
214
|
+
const task = createTaskExtended("Own resumed work", {
|
|
215
|
+
routingAffinity: affinity({ sourceAgentId: agent.id, role: "coder" }),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const claimed = claimTask(task.id, agent.id);
|
|
219
|
+
expect(claimed).not.toBeNull();
|
|
220
|
+
expect(claimed?.agentId).toBe(agent.id);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ==========================================================================
|
|
225
|
+
// assignUnassignedTaskPending eligibility gate
|
|
226
|
+
// ==========================================================================
|
|
227
|
+
|
|
228
|
+
describe("assignUnassignedTaskPending", () => {
|
|
229
|
+
test("rejects an ineligible agent (defense in depth)", () => {
|
|
230
|
+
const researcher = createAgent({ name: "researcher-5", isLead: false, status: "idle" });
|
|
231
|
+
updateAgentProfile(researcher.id, { role: "researcher" });
|
|
232
|
+
const task = createTaskExtended("Coding task", {
|
|
233
|
+
routingAffinity: affinity({ role: "coder" }),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const result = assignUnassignedTaskPending(task.id, researcher.id);
|
|
237
|
+
expect(result).toBeNull();
|
|
238
|
+
expect(getTaskById(task.id)?.status).toBe("unassigned");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("assigns an eligible agent", () => {
|
|
242
|
+
const coder = createAgent({ name: "coder-7", isLead: false, status: "idle" });
|
|
243
|
+
updateAgentProfile(coder.id, { role: "coder" });
|
|
244
|
+
const task = createTaskExtended("Coding task", {
|
|
245
|
+
routingAffinity: affinity({ role: "coder" }),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const result = assignUnassignedTaskPending(task.id, coder.id);
|
|
249
|
+
expect(result?.status).toBe("pending");
|
|
250
|
+
expect(result?.agentId).toBe(coder.id);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ==========================================================================
|
|
255
|
+
// getUnassignedTaskIdsForAgent ordering + filtering
|
|
256
|
+
// ==========================================================================
|
|
257
|
+
|
|
258
|
+
describe("getUnassignedTaskIdsForAgent", () => {
|
|
259
|
+
test("filters out ineligible tasks and preserves priority/creation ordering", () => {
|
|
260
|
+
const coder = createAgent({ name: "coder-8", isLead: false, status: "idle" });
|
|
261
|
+
updateAgentProfile(coder.id, { role: "coder" });
|
|
262
|
+
|
|
263
|
+
const researchTask = createTaskExtended("Research task", {
|
|
264
|
+
routingAffinity: affinity({ role: "researcher" }),
|
|
265
|
+
priority: 90,
|
|
266
|
+
});
|
|
267
|
+
const lowPriorityCoderTask = createTaskExtended("Low priority coding task", {
|
|
268
|
+
routingAffinity: affinity({ role: "coder" }),
|
|
269
|
+
priority: 10,
|
|
270
|
+
});
|
|
271
|
+
const highPriorityCoderTask = createTaskExtended("High priority coding task", {
|
|
272
|
+
routingAffinity: affinity({ role: "coder" }),
|
|
273
|
+
priority: 80,
|
|
274
|
+
});
|
|
275
|
+
const untaggedTask = createTaskExtended("Untagged task", { priority: 50 });
|
|
276
|
+
|
|
277
|
+
const ids = getUnassignedTaskIdsForAgent(coder.id, 10);
|
|
278
|
+
|
|
279
|
+
expect(ids).not.toContain(researchTask.id);
|
|
280
|
+
expect(ids).toContain(lowPriorityCoderTask.id);
|
|
281
|
+
expect(ids).toContain(highPriorityCoderTask.id);
|
|
282
|
+
expect(ids).toContain(untaggedTask.id);
|
|
283
|
+
|
|
284
|
+
// Priority DESC ordering preserved among eligible candidates.
|
|
285
|
+
const highIdx = ids.indexOf(highPriorityCoderTask.id);
|
|
286
|
+
const untaggedIdx = ids.indexOf(untaggedTask.id);
|
|
287
|
+
const lowIdx = ids.indexOf(lowPriorityCoderTask.id);
|
|
288
|
+
expect(highIdx).toBeLessThan(untaggedIdx);
|
|
289
|
+
expect(untaggedIdx).toBeLessThan(lowIdx);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("returns an empty list for an unknown agent", () => {
|
|
293
|
+
createTaskExtended("Untagged task");
|
|
294
|
+
expect(getUnassignedTaskIdsForAgent("00000000-0000-0000-0000-000000000000", 10)).toEqual([]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("paginates past a wall of ineligible affinity tasks larger than the old fixed scan window", () => {
|
|
298
|
+
// PR #954 review: the old implementation fetched a single fixed window
|
|
299
|
+
// (max(limit * 5, 25) = 50 rows for limit=10) and filtered in JS, so an
|
|
300
|
+
// eligible task sorted past row 50 was invisible no matter how many
|
|
301
|
+
// times this was called. This seeds 55 high-priority ineligible tasks
|
|
302
|
+
// ahead of one low-priority eligible task — more than the old window —
|
|
303
|
+
// to prove the scan now pages through rather than stopping at row 50.
|
|
304
|
+
const coder = createAgent({ name: "coder-9-pagination", isLead: false, status: "idle" });
|
|
305
|
+
updateAgentProfile(coder.id, { role: "coder" });
|
|
306
|
+
|
|
307
|
+
for (let i = 0; i < 55; i++) {
|
|
308
|
+
createTaskExtended(`Ineligible research task ${i}`, {
|
|
309
|
+
routingAffinity: affinity({ role: "researcher" }),
|
|
310
|
+
priority: 100,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const eligibleTask = createTaskExtended("Eligible coder task buried behind the wall", {
|
|
314
|
+
routingAffinity: affinity({ role: "coder" }),
|
|
315
|
+
priority: 1,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const ids = getUnassignedTaskIdsForAgent(coder.id, 10);
|
|
319
|
+
|
|
320
|
+
expect(ids).toContain(eligibleTask.id);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// ==========================================================================
|
|
325
|
+
// autoAssignPoolTasks (via codeLevelTriage) — per-task eligibility filtering
|
|
326
|
+
// ==========================================================================
|
|
327
|
+
|
|
328
|
+
describe("autoAssignPoolTasks eligibility", () => {
|
|
329
|
+
test("skips an ineligible idle worker and leaves the task queued", async () => {
|
|
330
|
+
const researcher = createAgent({ name: "idle-researcher", isLead: false, status: "idle" });
|
|
331
|
+
updateAgentProfile(researcher.id, { role: "researcher" });
|
|
332
|
+
const task = createTaskExtended("Coding task", {
|
|
333
|
+
routingAffinity: affinity({ role: "coder" }),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const findings = await codeLevelTriage();
|
|
337
|
+
|
|
338
|
+
expect(findings.autoAssigned.length).toBe(0);
|
|
339
|
+
expect(getTaskById(task.id)?.status).toBe("unassigned");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("assigns to the eligible worker, skipping an ineligible one that sorts first", async () => {
|
|
343
|
+
const researcher = createAgent({ name: "idle-researcher-2", isLead: false, status: "idle" });
|
|
344
|
+
updateAgentProfile(researcher.id, { role: "researcher" });
|
|
345
|
+
const coder = createAgent({ name: "idle-coder", isLead: false, status: "idle" });
|
|
346
|
+
updateAgentProfile(coder.id, { role: "coder" });
|
|
347
|
+
const task = createTaskExtended("Coding task", {
|
|
348
|
+
routingAffinity: affinity({ role: "coder" }),
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const findings = await codeLevelTriage();
|
|
352
|
+
|
|
353
|
+
expect(findings.autoAssigned.length).toBe(1);
|
|
354
|
+
expect(findings.autoAssigned[0]!.agentId).toBe(coder.id);
|
|
355
|
+
expect(getTaskById(task.id)?.agentId).toBe(coder.id);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("untagged tasks are unaffected — assigned to any idle worker", async () => {
|
|
359
|
+
const worker = createAgent({ name: "idle-any", isLead: false, status: "idle" });
|
|
360
|
+
const task = createTaskExtended("Untagged pool task");
|
|
361
|
+
|
|
362
|
+
const findings = await codeLevelTriage();
|
|
363
|
+
|
|
364
|
+
expect(findings.autoAssigned.length).toBe(1);
|
|
365
|
+
expect(findings.autoAssigned[0]!.agentId).toBe(worker.id);
|
|
366
|
+
expect(getTaskById(task.id)?.agentId).toBe(worker.id);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("paginates past a wall of ineligible affinity tasks larger than the old fixed sweep window", async () => {
|
|
370
|
+
// PR #954 review: the old implementation fetched only
|
|
371
|
+
// getUnassignedPoolTasks(MAX_AUTO_ASSIGN_PER_SWEEP) — a single bounded
|
|
372
|
+
// window (default 5) — so a high-priority run of affinity-tagged tasks
|
|
373
|
+
// for another role could hide all eligible work behind it forever,
|
|
374
|
+
// across every sweep, since the same ineligible head-of-line rows were
|
|
375
|
+
// re-fetched every time. This seeds 55 ineligible high-priority tasks
|
|
376
|
+
// (more than the default POOL_SCAN_BATCH_SIZE of 50) ahead of one
|
|
377
|
+
// low-priority eligible task, to prove the scan now pages through the
|
|
378
|
+
// pool rather than stopping at the first window.
|
|
379
|
+
const coder = createAgent({ name: "idle-coder-pagination", isLead: false, status: "idle" });
|
|
380
|
+
updateAgentProfile(coder.id, { role: "coder" });
|
|
381
|
+
|
|
382
|
+
for (let i = 0; i < 55; i++) {
|
|
383
|
+
createTaskExtended(`Ineligible research task ${i}`, {
|
|
384
|
+
routingAffinity: affinity({ role: "researcher" }),
|
|
385
|
+
priority: 100,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
const eligibleTask = createTaskExtended("Eligible coder task buried behind the wall", {
|
|
389
|
+
routingAffinity: affinity({ role: "coder" }),
|
|
390
|
+
priority: 1,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const findings = await codeLevelTriage();
|
|
394
|
+
|
|
395
|
+
expect(findings.autoAssigned.length).toBe(1);
|
|
396
|
+
expect(findings.autoAssigned[0]!.taskId).toBe(eligibleTask.id);
|
|
397
|
+
expect(findings.autoAssigned[0]!.agentId).toBe(coder.id);
|
|
398
|
+
expect(getTaskById(eligibleTask.id)?.agentId).toBe(coder.id);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
});
|
|
@@ -155,7 +155,7 @@ describe("GitLab — backward compatibility", () => {
|
|
|
155
155
|
const expected = `[GitLab MR #5] Add feature
|
|
156
156
|
|
|
157
157
|
Repo: group/project
|
|
158
|
-
Author:
|
|
158
|
+
Author: dev1
|
|
159
159
|
Branch: feature-branch \u2192 main
|
|
160
160
|
URL: https://gitlab.com/group/project/-/merge_requests/5
|
|
161
161
|
|
|
@@ -206,7 +206,7 @@ The CI pipeline has failed. Please investigate and fix the issues.
|
|
|
206
206
|
|
|
207
207
|
expect(result.skipped).toBe(false);
|
|
208
208
|
|
|
209
|
-
const expected = `[GitLab Comment on MR #5]
|
|
209
|
+
const expected = `[GitLab Comment on MR #5] dev1 mentioned bot
|
|
210
210
|
|
|
211
211
|
Repo: group/project
|
|
212
212
|
URL: https://gitlab.com/group/project/-/merge_requests/5
|