macro-agent 0.1.0 → 0.1.1
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/.claude/settings.local.json +3 -1
- package/.sudocode/specs.jsonl +4 -0
- package/CLAUDE.md +16 -14
- package/README.md +11 -29
- package/dist/acp/macro-agent.d.ts +15 -0
- package/dist/acp/macro-agent.d.ts.map +1 -1
- package/dist/acp/macro-agent.js +131 -35
- package/dist/acp/macro-agent.js.map +1 -1
- package/dist/acp/types.d.ts +32 -1
- package/dist/acp/types.d.ts.map +1 -1
- package/dist/acp/types.js.map +1 -1
- package/dist/agent/agent-manager.d.ts +65 -1
- package/dist/agent/agent-manager.d.ts.map +1 -1
- package/dist/agent/agent-manager.js +464 -183
- package/dist/agent/agent-manager.js.map +1 -1
- package/dist/agent/types.d.ts +1 -1
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/api/server.d.ts +3 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +37 -6
- package/dist/api/server.js.map +1 -1
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/token.d.ts +41 -0
- package/dist/auth/token.d.ts.map +1 -0
- package/dist/auth/token.js +73 -0
- package/dist/auth/token.js.map +1 -0
- package/dist/cli/acp.d.ts +2 -23
- package/dist/cli/acp.d.ts.map +1 -1
- package/dist/cli/acp.js +127 -61
- package/dist/cli/acp.js.map +1 -1
- package/dist/cli/index.js +147 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp.d.ts +6 -0
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +268 -181
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/parse-args.d.ts +20 -0
- package/dist/cli/parse-args.d.ts.map +1 -0
- package/dist/cli/parse-args.js +43 -0
- package/dist/cli/parse-args.js.map +1 -0
- package/dist/cli/stable-instance-id.d.ts +8 -0
- package/dist/cli/stable-instance-id.d.ts.map +1 -0
- package/dist/cli/stable-instance-id.js +14 -0
- package/dist/cli/stable-instance-id.js.map +1 -0
- package/dist/config/project-config.d.ts +74 -7
- package/dist/config/project-config.d.ts.map +1 -1
- package/dist/config/project-config.js +123 -20
- package/dist/config/project-config.js.map +1 -1
- package/dist/map/adapter/acp-over-map.d.ts +17 -0
- package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
- package/dist/map/adapter/acp-over-map.js +384 -23
- package/dist/map/adapter/acp-over-map.js.map +1 -1
- package/dist/map/adapter/connection-manager.d.ts.map +1 -1
- package/dist/map/adapter/connection-manager.js +3 -0
- package/dist/map/adapter/connection-manager.js.map +1 -1
- package/dist/map/adapter/event-log.d.ts +87 -0
- package/dist/map/adapter/event-log.d.ts.map +1 -0
- package/dist/map/adapter/event-log.js +122 -0
- package/dist/map/adapter/event-log.js.map +1 -0
- package/dist/map/adapter/event-translator.js +6 -6
- package/dist/map/adapter/event-translator.js.map +1 -1
- package/dist/map/adapter/extensions/agent-lifecycle.d.ts +82 -0
- package/dist/map/adapter/extensions/agent-lifecycle.d.ts.map +1 -0
- package/dist/map/adapter/extensions/agent-lifecycle.js +164 -0
- package/dist/map/adapter/extensions/agent-lifecycle.js.map +1 -0
- package/dist/map/adapter/extensions/index.d.ts +10 -1
- package/dist/map/adapter/extensions/index.d.ts.map +1 -1
- package/dist/map/adapter/extensions/index.js +34 -0
- package/dist/map/adapter/extensions/index.js.map +1 -1
- package/dist/map/adapter/extensions/mcp-bridge.d.ts +57 -0
- package/dist/map/adapter/extensions/mcp-bridge.d.ts.map +1 -0
- package/dist/map/adapter/extensions/mcp-bridge.js +745 -0
- package/dist/map/adapter/extensions/mcp-bridge.js.map +1 -0
- package/dist/map/adapter/extensions/rename.d.ts +29 -0
- package/dist/map/adapter/extensions/rename.d.ts.map +1 -0
- package/dist/map/adapter/extensions/rename.js +49 -0
- package/dist/map/adapter/extensions/rename.js.map +1 -0
- package/dist/map/adapter/extensions/task.d.ts.map +1 -1
- package/dist/map/adapter/extensions/task.js +10 -0
- package/dist/map/adapter/extensions/task.js.map +1 -1
- package/dist/map/adapter/extensions/update-metadata.d.ts +29 -0
- package/dist/map/adapter/extensions/update-metadata.d.ts.map +1 -0
- package/dist/map/adapter/extensions/update-metadata.js +67 -0
- package/dist/map/adapter/extensions/update-metadata.js.map +1 -0
- package/dist/map/adapter/index.d.ts +2 -1
- package/dist/map/adapter/index.d.ts.map +1 -1
- package/dist/map/adapter/index.js +8 -2
- package/dist/map/adapter/index.js.map +1 -1
- package/dist/map/adapter/interface.d.ts +2 -0
- package/dist/map/adapter/interface.d.ts.map +1 -1
- package/dist/map/adapter/map-adapter.d.ts +3 -0
- package/dist/map/adapter/map-adapter.d.ts.map +1 -1
- package/dist/map/adapter/map-adapter.js +258 -35
- package/dist/map/adapter/map-adapter.js.map +1 -1
- package/dist/map/adapter/subscription-manager.d.ts.map +1 -1
- package/dist/map/adapter/subscription-manager.js +5 -1
- package/dist/map/adapter/subscription-manager.js.map +1 -1
- package/dist/map/adapter/types.d.ts +2 -0
- package/dist/map/adapter/types.d.ts.map +1 -1
- package/dist/mcp/map-client.d.ts +39 -0
- package/dist/mcp/map-client.d.ts.map +1 -0
- package/dist/mcp/map-client.js +129 -0
- package/dist/mcp/map-client.js.map +1 -0
- package/dist/mcp/mcp-server.d.ts +14 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -1
- package/dist/mcp/mcp-server.js +113 -85
- package/dist/mcp/mcp-server.js.map +1 -1
- package/dist/mcp/types.d.ts +9 -1
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/mcp/types.js.map +1 -1
- package/dist/metrics/metrics.js +1 -1
- package/dist/metrics/metrics.js.map +1 -1
- package/dist/roles/capabilities.d.ts +3 -1
- package/dist/roles/capabilities.d.ts.map +1 -1
- package/dist/roles/capabilities.js +17 -7
- package/dist/roles/capabilities.js.map +1 -1
- package/dist/roles/config-loader.d.ts +6 -6
- package/dist/roles/config-loader.d.ts.map +1 -1
- package/dist/roles/config-loader.js +6 -6
- package/dist/roles/config-loader.js.map +1 -1
- package/dist/roles/registry.d.ts +2 -2
- package/dist/roles/registry.js +2 -2
- package/dist/server/combined-server.d.ts +20 -0
- package/dist/server/combined-server.d.ts.map +1 -1
- package/dist/server/combined-server.js +107 -8
- package/dist/server/combined-server.js.map +1 -1
- package/dist/store/event-store.d.ts +2 -1
- package/dist/store/event-store.d.ts.map +1 -1
- package/dist/store/event-store.js +69 -20
- package/dist/store/event-store.js.map +1 -1
- package/dist/store/types/agents.d.ts +18 -0
- package/dist/store/types/agents.d.ts.map +1 -1
- package/dist/store/types/events.d.ts +1 -1
- package/dist/store/types/events.d.ts.map +1 -1
- package/dist/task/backend/index.d.ts +47 -29
- package/dist/task/backend/index.d.ts.map +1 -1
- package/dist/task/backend/index.js +109 -71
- package/dist/task/backend/index.js.map +1 -1
- package/dist/task/backend/memory.d.ts +1 -0
- package/dist/task/backend/memory.d.ts.map +1 -1
- package/dist/task/backend/memory.js +3 -0
- package/dist/task/backend/memory.js.map +1 -1
- package/dist/task/backend/opentasks/backend.d.ts +140 -0
- package/dist/task/backend/opentasks/backend.d.ts.map +1 -0
- package/dist/task/backend/opentasks/backend.js +1023 -0
- package/dist/task/backend/opentasks/backend.js.map +1 -0
- package/dist/task/backend/opentasks/client.d.ts +337 -0
- package/dist/task/backend/opentasks/client.d.ts.map +1 -0
- package/dist/task/backend/opentasks/client.js +225 -0
- package/dist/task/backend/opentasks/client.js.map +1 -0
- package/dist/task/backend/opentasks/daemon-manager.d.ts +89 -0
- package/dist/task/backend/opentasks/daemon-manager.d.ts.map +1 -0
- package/dist/task/backend/opentasks/daemon-manager.js +195 -0
- package/dist/task/backend/opentasks/daemon-manager.js.map +1 -0
- package/dist/task/backend/opentasks/index.d.ts +21 -0
- package/dist/task/backend/opentasks/index.d.ts.map +1 -0
- package/dist/task/backend/opentasks/index.js +21 -0
- package/dist/task/backend/opentasks/index.js.map +1 -0
- package/dist/task/backend/opentasks/mapping.d.ts +48 -0
- package/dist/task/backend/opentasks/mapping.d.ts.map +1 -0
- package/dist/task/backend/opentasks/mapping.js +77 -0
- package/dist/task/backend/opentasks/mapping.js.map +1 -0
- package/dist/task/backend/types.d.ts +33 -53
- package/dist/task/backend/types.d.ts.map +1 -1
- package/dist/task/backend/types.js +7 -11
- package/dist/task/backend/types.js.map +1 -1
- package/dist/task/backend/unified-tool-provider.d.ts +57 -0
- package/dist/task/backend/unified-tool-provider.d.ts.map +1 -0
- package/dist/task/backend/unified-tool-provider.js +623 -0
- package/dist/task/backend/unified-tool-provider.js.map +1 -0
- package/dist/teams/team-loader.d.ts +2 -2
- package/dist/teams/team-loader.js +3 -3
- package/dist/teams/team-loader.js.map +1 -1
- package/dist/teams/team-runtime.d.ts.map +1 -1
- package/dist/teams/team-runtime.js +2 -0
- package/dist/teams/team-runtime.js.map +1 -1
- package/docs/architecture.md +7 -6
- package/docs/configuration.md +26 -62
- package/docs/implementation-details.md +5 -5
- package/docs/implementation-summary.md +17 -17
- package/docs/plan-self-driving-support.md +4 -4
- package/docs/spec-self-driving-support.md +10 -10
- package/docs/team-templates.md +2 -2
- package/docs/teams.md +3 -3
- package/docs/troubleshooting.md +10 -11
- package/package.json +6 -4
- package/src/__tests__/e2e/agent-spawn-visibility.e2e.test.ts +761 -0
- package/src/__tests__/e2e/full-agent-conflict-resolution.e2e.test.ts +2 -2
- package/src/__tests__/e2e/mcp-thin-client-bridge.e2e.test.ts +304 -0
- package/src/__tests__/e2e/mcp-tools-available.e2e.test.ts +324 -0
- package/src/__tests__/e2e/multi-agent.e2e.test.ts +5 -5
- package/src/__tests__/e2e/spawn-session-streaming.e2e.test.ts +563 -0
- package/src/acp/__tests__/integration.test.ts +56 -31
- package/src/acp/__tests__/macro-agent.test.ts +16 -7
- package/src/acp/macro-agent.ts +170 -36
- package/src/acp/types.ts +46 -1
- package/src/agent/__tests__/agent-manager.test.ts +228 -2
- package/src/agent/agent-manager.ts +714 -261
- package/src/agent/types.ts +3 -1
- package/src/api/server.ts +41 -7
- package/src/auth/__tests__/token.test.ts +100 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/token.ts +82 -0
- package/src/cli/__tests__/acp.test.ts +1 -1
- package/src/cli/__tests__/stable-instance-id.test.ts +1 -1
- package/src/cli/acp.ts +130 -72
- package/src/cli/index.ts +120 -14
- package/src/cli/mcp.ts +311 -207
- package/src/cli/parse-args.ts +54 -0
- package/src/cli/stable-instance-id.ts +14 -0
- package/src/config/project-config.ts +190 -27
- package/src/lifecycle/__tests__/cascade-termination.test.ts +1 -1
- package/src/map/adapter/__tests__/acp-over-map-cancel.test.ts +22 -4
- package/src/map/adapter/__tests__/acp-over-map-getmodels.test.ts +355 -0
- package/src/map/adapter/__tests__/acp-over-map-history.test.ts +263 -0
- package/src/map/adapter/__tests__/acp-over-map-persistence.e2e.test.ts +1 -1
- package/src/map/adapter/__tests__/event-broadcast.test.ts +420 -0
- package/src/map/adapter/__tests__/event-log.test.ts +527 -0
- package/src/map/adapter/__tests__/event-translator.test.ts +3 -3
- package/src/map/adapter/__tests__/extensions.test.ts +408 -0
- package/src/map/adapter/__tests__/map-adapter.test.ts +99 -0
- package/src/map/adapter/__tests__/mcp-bridge.test.ts +1187 -0
- package/src/map/adapter/__tests__/multi-client-broadcast.test.ts +711 -0
- package/src/map/adapter/__tests__/websocket-integration.test.ts +218 -0
- package/src/map/adapter/acp-over-map.ts +678 -66
- package/src/map/adapter/connection-manager.ts +3 -0
- package/src/map/adapter/event-log.ts +208 -0
- package/src/map/adapter/event-translator.ts +6 -6
- package/src/map/adapter/extensions/agent-lifecycle.ts +267 -0
- package/src/map/adapter/extensions/index.ts +60 -0
- package/src/map/adapter/extensions/mcp-bridge.ts +995 -0
- package/src/map/adapter/extensions/task.ts +11 -0
- package/src/map/adapter/extensions/update-metadata.ts +126 -0
- package/src/map/adapter/index.ts +28 -0
- package/src/map/adapter/interface.ts +2 -0
- package/src/map/adapter/map-adapter.ts +312 -47
- package/src/map/adapter/subscription-manager.ts +5 -1
- package/src/map/adapter/types.ts +2 -0
- package/src/mcp/__tests__/map-client.test.ts +386 -0
- package/src/mcp/__tests__/mcp-server-thin-client.test.ts +368 -0
- package/src/mcp/__tests__/mcp-server.test.ts +100 -1
- package/src/mcp/map-client.ts +177 -0
- package/src/mcp/mcp-server.ts +191 -100
- package/src/mcp/types.ts +6 -1
- package/src/metrics/metrics.ts +1 -1
- package/src/monitor/__tests__/stale-agent-flow.integration.test.ts +1 -1
- package/src/roles/__tests__/config-loader.test.ts +7 -7
- package/src/roles/capabilities.ts +17 -7
- package/src/roles/config-loader.ts +6 -6
- package/src/roles/registry.ts +2 -2
- package/src/server/__tests__/combined-server.test.ts +94 -21
- package/src/server/combined-server.ts +189 -33
- package/src/steering/__tests__/steering-integration.test.ts +1 -1
- package/src/store/__tests__/event-store.test.ts +196 -1
- package/src/store/__tests__/instance.test.ts +3 -3
- package/src/store/event-store.ts +80 -21
- package/src/store/types/agents.ts +15 -0
- package/src/store/types/events.ts +1 -1
- package/src/task/backend/__tests__/create-task-backend.test.ts +225 -0
- package/src/task/backend/__tests__/e2e/unified-tool-provider-opentasks.e2e.test.ts +524 -0
- package/src/task/backend/__tests__/unified-tool-provider.test.ts +579 -0
- package/src/task/backend/index.ts +156 -106
- package/src/task/backend/memory.ts +4 -0
- package/src/task/backend/opentasks/__tests__/backend.test.ts +968 -0
- package/src/task/backend/opentasks/__tests__/daemon-manager.test.ts +406 -0
- package/src/task/backend/opentasks/__tests__/mapping.test.ts +84 -0
- package/src/task/backend/opentasks/__tests__/opentasks-backend.e2e.test.ts +1338 -0
- package/src/task/backend/opentasks/backend.ts +1323 -0
- package/src/task/backend/opentasks/client.ts +652 -0
- package/src/task/backend/opentasks/daemon-manager.ts +253 -0
- package/src/task/backend/opentasks/index.ts +69 -0
- package/src/task/backend/opentasks/mapping.ts +94 -0
- package/src/task/backend/types.ts +42 -66
- package/src/task/backend/unified-tool-provider.ts +779 -0
- package/src/teams/__tests__/cross-subsystem.integration.test.ts +1 -1
- package/src/teams/team-loader.ts +3 -3
- package/src/teams/team-runtime.ts +2 -0
- package/test_fixtures/README.md +2 -3
- package/test_fixtures/fixtures/index.ts +0 -3
- package/test_fixtures/fixtures/projects/project-with-specs.ts +7 -149
- package/test_fixtures/fixtures/repos/index.ts +1 -3
- package/test_fixtures/fixtures/repos/temp-repo-factory.ts +0 -116
- package/test_fixtures/fixtures/repos/types.ts +0 -11
- package/test_fixtures/harness/__tests__/fixtures.test.ts +10 -102
- package/test_fixtures/harness/__tests__/temp-repo-and-simulator.test.ts +0 -33
- package/test_fixtures/harness/simulator/agent-simulator.ts +4 -4
- package/vitest.config.ts +1 -1
- package/vitest.e2e.config.ts +1 -1
- package/vitest.setup.ts +1 -30
- package/.macro-agent/teams/self-driving/prompts/grinder.md +0 -27
- package/.macro-agent/teams/self-driving/prompts/judge.md +0 -27
- package/.macro-agent/teams/self-driving/prompts/planner.md +0 -33
- package/.macro-agent/teams/self-driving/roles/grinder.yaml +0 -17
- package/.macro-agent/teams/self-driving/roles/judge.yaml +0 -24
- package/.macro-agent/teams/self-driving/roles/planner.yaml +0 -18
- package/.macro-agent/teams/self-driving/team.yaml +0 -103
- package/.macro-agent/teams/structured/prompts/developer.md +0 -26
- package/.macro-agent/teams/structured/prompts/lead.md +0 -25
- package/.macro-agent/teams/structured/prompts/reviewer.md +0 -24
- package/.macro-agent/teams/structured/roles/developer.yaml +0 -12
- package/.macro-agent/teams/structured/roles/lead.yaml +0 -11
- package/.macro-agent/teams/structured/roles/reviewer.yaml +0 -19
- package/.macro-agent/teams/structured/team.yaml +0 -89
- package/docs/sudocode-integration.md +0 -383
- package/src/task/backend/__tests__/backend-parity.test.ts +0 -451
- package/src/task/backend/__tests__/tool-provider-edge-cases.test.ts +0 -430
- package/src/task/backend/__tests__/tool-provider.test.ts +0 -983
- package/src/task/backend/sudocode/__tests__/backend-edge-cases.test.ts +0 -575
- package/src/task/backend/sudocode/__tests__/backend.test.ts +0 -1194
- package/src/task/backend/sudocode/__tests__/client-integration.test.ts +0 -418
- package/src/task/backend/sudocode/__tests__/client.test.ts +0 -345
- package/src/task/backend/sudocode/__tests__/e2e/backend.e2e.test.ts +0 -753
- package/src/task/backend/sudocode/__tests__/e2e/server-client.e2e.test.ts +0 -680
- package/src/task/backend/sudocode/__tests__/e2e-workflow.test.ts +0 -666
- package/src/task/backend/sudocode/__tests__/integration/standalone-client.integration.test.ts +0 -396
- package/src/task/backend/sudocode/__tests__/integration/sudocode-cli.integration.test.ts +0 -328
- package/src/task/backend/sudocode/__tests__/integration/test-utils.ts +0 -175
- package/src/task/backend/sudocode/__tests__/mapping-edge-cases.test.ts +0 -265
- package/src/task/backend/sudocode/__tests__/server-client.test.ts +0 -675
- package/src/task/backend/sudocode/__tests__/sync-policy-edge-cases.test.ts +0 -521
- package/src/task/backend/sudocode/__tests__/sync-policy.test.ts +0 -519
- package/src/task/backend/sudocode/__tests__/tools.test.ts +0 -471
- package/src/task/backend/sudocode/backend.ts +0 -1237
- package/src/task/backend/sudocode/client.ts +0 -515
- package/src/task/backend/sudocode/index.ts +0 -120
- package/src/task/backend/sudocode/mapping.ts +0 -93
- package/src/task/backend/sudocode/server-client.ts +0 -522
- package/src/task/backend/sudocode/standalone-client.ts +0 -623
- package/src/task/backend/sudocode/sync-policy.ts +0 -387
- package/src/task/backend/sudocode/tools.ts +0 -896
- package/src/task/backend/tool-provider.ts +0 -506
- package/test_fixtures/fixtures/sudocode/index.ts +0 -29
- package/test_fixtures/fixtures/sudocode/issues.ts +0 -185
- package/test_fixtures/fixtures/sudocode/specs.ts +0 -159
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for OpenTasksTaskBackend
|
|
3
|
+
*
|
|
4
|
+
* Uses a mock OpenTasksClient to test the backend without requiring
|
|
5
|
+
* a running OpenTasks daemon.
|
|
6
|
+
*
|
|
7
|
+
* @module task/backend/opentasks/__tests__/backend.test
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
11
|
+
import { createEventStore, type EventStore } from "../../../../store/event-store.js";
|
|
12
|
+
import {
|
|
13
|
+
OpenTasksTaskBackend,
|
|
14
|
+
OpenTasksBackendError,
|
|
15
|
+
createOpenTasksTaskBackend,
|
|
16
|
+
} from "../backend.js";
|
|
17
|
+
import type { OpenTasksClient, OpenTasksIssue, OpenTasksNodeSummary } from "../client.js";
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Mock Client
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
function createMockClient(): OpenTasksClient {
|
|
24
|
+
const issues = new Map<string, OpenTasksIssue>();
|
|
25
|
+
let issueCounter = 0;
|
|
26
|
+
|
|
27
|
+
const edges: Array<{ fromId: string; toId: string; type: string }> = [];
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
createIssue: vi.fn(async (input) => {
|
|
31
|
+
issueCounter++;
|
|
32
|
+
const id = `i-mock${issueCounter}`;
|
|
33
|
+
const issue: OpenTasksIssue = {
|
|
34
|
+
id,
|
|
35
|
+
uuid: `uuid-${id}`,
|
|
36
|
+
type: "issue",
|
|
37
|
+
title: input.title,
|
|
38
|
+
content: input.content,
|
|
39
|
+
status: input.status ?? "open",
|
|
40
|
+
assignee: input.assignee,
|
|
41
|
+
priority: input.priority,
|
|
42
|
+
tags: input.tags,
|
|
43
|
+
parent_id: input.parent_id,
|
|
44
|
+
created_at: new Date().toISOString(),
|
|
45
|
+
updated_at: new Date().toISOString(),
|
|
46
|
+
metadata: input.metadata,
|
|
47
|
+
};
|
|
48
|
+
issues.set(id, issue);
|
|
49
|
+
return issue;
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
getIssue: vi.fn(async (id) => {
|
|
53
|
+
return issues.get(id) ?? null;
|
|
54
|
+
}),
|
|
55
|
+
|
|
56
|
+
updateIssue: vi.fn(async (id, updates) => {
|
|
57
|
+
const issue = issues.get(id);
|
|
58
|
+
if (!issue) throw new Error(`Issue not found: ${id}`);
|
|
59
|
+
const updated = { ...issue, ...updates, updated_at: new Date().toISOString() };
|
|
60
|
+
// Merge metadata instead of replacing
|
|
61
|
+
if (updates.metadata && issue.metadata) {
|
|
62
|
+
updated.metadata = { ...issue.metadata, ...updates.metadata };
|
|
63
|
+
}
|
|
64
|
+
issues.set(id, updated);
|
|
65
|
+
return updated;
|
|
66
|
+
}),
|
|
67
|
+
|
|
68
|
+
deleteIssue: vi.fn(async (id) => {
|
|
69
|
+
issues.delete(id);
|
|
70
|
+
}),
|
|
71
|
+
|
|
72
|
+
listIssues: vi.fn(async (filter) => {
|
|
73
|
+
let result = Array.from(issues.values());
|
|
74
|
+
if (filter?.status) {
|
|
75
|
+
const statuses = Array.isArray(filter.status)
|
|
76
|
+
? filter.status
|
|
77
|
+
: [filter.status];
|
|
78
|
+
result = result.filter((i) => statuses.includes(i.status));
|
|
79
|
+
}
|
|
80
|
+
if (filter?.archived === false) {
|
|
81
|
+
result = result.filter((i) => !i.archived);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}),
|
|
85
|
+
|
|
86
|
+
getReadyIssues: vi.fn(async () => {
|
|
87
|
+
return Array.from(issues.values())
|
|
88
|
+
.filter((i) => i.status === "open" && !i.assignee)
|
|
89
|
+
.map((i) => ({
|
|
90
|
+
id: i.id,
|
|
91
|
+
type: "issue",
|
|
92
|
+
title: i.title,
|
|
93
|
+
status: i.status,
|
|
94
|
+
priority: i.priority,
|
|
95
|
+
archived: false,
|
|
96
|
+
}));
|
|
97
|
+
}),
|
|
98
|
+
|
|
99
|
+
createEdge: vi.fn(async (fromId, toId, type) => {
|
|
100
|
+
edges.push({ fromId, toId, type });
|
|
101
|
+
return {
|
|
102
|
+
id: `x-edge${edges.length}`,
|
|
103
|
+
uuid: `uuid-edge${edges.length}`,
|
|
104
|
+
from_id: fromId,
|
|
105
|
+
to_id: toId,
|
|
106
|
+
type,
|
|
107
|
+
created_at: new Date().toISOString(),
|
|
108
|
+
};
|
|
109
|
+
}),
|
|
110
|
+
|
|
111
|
+
removeEdge: vi.fn(async (fromId, toId, type) => {
|
|
112
|
+
const idx = edges.findIndex(
|
|
113
|
+
(e) => e.fromId === fromId && e.toId === toId && e.type === type
|
|
114
|
+
);
|
|
115
|
+
if (idx >= 0) edges.splice(idx, 1);
|
|
116
|
+
}),
|
|
117
|
+
|
|
118
|
+
getBlockers: vi.fn(async (nodeId) => {
|
|
119
|
+
// Find edges where nodeId is the target (blocked by from)
|
|
120
|
+
const blockerIds = edges
|
|
121
|
+
.filter((e) => e.toId === nodeId && e.type === "blocks")
|
|
122
|
+
.map((e) => e.fromId);
|
|
123
|
+
|
|
124
|
+
return blockerIds
|
|
125
|
+
.map((id) => {
|
|
126
|
+
const issue = issues.get(id);
|
|
127
|
+
if (!issue) return null;
|
|
128
|
+
return {
|
|
129
|
+
id: issue.id,
|
|
130
|
+
type: "issue",
|
|
131
|
+
title: issue.title,
|
|
132
|
+
status: issue.status,
|
|
133
|
+
priority: issue.priority,
|
|
134
|
+
archived: false,
|
|
135
|
+
} as OpenTasksNodeSummary;
|
|
136
|
+
})
|
|
137
|
+
.filter((s): s is OpenTasksNodeSummary => s !== null);
|
|
138
|
+
}),
|
|
139
|
+
|
|
140
|
+
getBlocking: vi.fn(async (nodeId) => {
|
|
141
|
+
const blockedIds = edges
|
|
142
|
+
.filter((e) => e.fromId === nodeId && e.type === "blocks")
|
|
143
|
+
.map((e) => e.toId);
|
|
144
|
+
|
|
145
|
+
return blockedIds
|
|
146
|
+
.map((id) => {
|
|
147
|
+
const issue = issues.get(id);
|
|
148
|
+
if (!issue) return null;
|
|
149
|
+
return {
|
|
150
|
+
id: issue.id,
|
|
151
|
+
type: "issue",
|
|
152
|
+
title: issue.title,
|
|
153
|
+
status: issue.status,
|
|
154
|
+
priority: issue.priority,
|
|
155
|
+
archived: false,
|
|
156
|
+
} as OpenTasksNodeSummary;
|
|
157
|
+
})
|
|
158
|
+
.filter((s): s is OpenTasksNodeSummary => s !== null);
|
|
159
|
+
}),
|
|
160
|
+
|
|
161
|
+
isConnected: vi.fn(() => true),
|
|
162
|
+
connect: vi.fn(async () => {}),
|
|
163
|
+
disconnect: vi.fn(() => {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// =============================================================================
|
|
168
|
+
// Tests
|
|
169
|
+
// =============================================================================
|
|
170
|
+
|
|
171
|
+
describe("OpenTasksTaskBackend", () => {
|
|
172
|
+
let eventStore: EventStore;
|
|
173
|
+
let client: OpenTasksClient;
|
|
174
|
+
let backend: OpenTasksTaskBackend;
|
|
175
|
+
const testAgentId = "agent_test123";
|
|
176
|
+
|
|
177
|
+
beforeEach(async () => {
|
|
178
|
+
eventStore = await createEventStore({ inMemory: true });
|
|
179
|
+
client = createMockClient();
|
|
180
|
+
backend = createOpenTasksTaskBackend(eventStore, client);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
afterEach(async () => {
|
|
184
|
+
await eventStore.close();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
188
|
+
// Lifecycle
|
|
189
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
describe("create", () => {
|
|
192
|
+
it("should create a task and issue in OpenTasks", async () => {
|
|
193
|
+
const task = await backend.create({
|
|
194
|
+
description: "Test task",
|
|
195
|
+
created_by: testAgentId,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(task.id).toMatch(/^task_/);
|
|
199
|
+
expect(task.description).toBe("Test task");
|
|
200
|
+
expect(task.status).toBe("pending");
|
|
201
|
+
expect(task.created_by).toBe(testAgentId);
|
|
202
|
+
expect(task.external_id).toMatch(/^i-mock/);
|
|
203
|
+
|
|
204
|
+
// Verify client was called
|
|
205
|
+
expect(client.createIssue).toHaveBeenCalledWith(
|
|
206
|
+
expect.objectContaining({
|
|
207
|
+
title: "Test task",
|
|
208
|
+
status: "open",
|
|
209
|
+
metadata: expect.objectContaining({
|
|
210
|
+
macro_agent_task_id: task.id,
|
|
211
|
+
created_by: testAgentId,
|
|
212
|
+
}),
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should create a subtask with parent reference", async () => {
|
|
218
|
+
const parent = await backend.create({
|
|
219
|
+
description: "Parent task",
|
|
220
|
+
created_by: testAgentId,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const child = await backend.create({
|
|
224
|
+
description: "Child task",
|
|
225
|
+
created_by: testAgentId,
|
|
226
|
+
parent_task: parent.id,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(child.parent_task).toBe(parent.id);
|
|
230
|
+
|
|
231
|
+
// Verify parent's subtasks array is updated
|
|
232
|
+
const updatedParent = await backend.get(parent.id);
|
|
233
|
+
expect(updatedParent?.subtasks).toContain(child.id);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should pass tags to OpenTasks when creating", async () => {
|
|
237
|
+
await backend.create({
|
|
238
|
+
description: "Tagged task",
|
|
239
|
+
created_by: testAgentId,
|
|
240
|
+
tags: ["auth", "backend"],
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Tags are passed to OpenTasks (EventStore doesn't persist tags natively)
|
|
244
|
+
expect(client.createIssue).toHaveBeenCalledWith(
|
|
245
|
+
expect.objectContaining({
|
|
246
|
+
tags: ["auth", "backend"],
|
|
247
|
+
})
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("get", () => {
|
|
253
|
+
it("should return task by ID", async () => {
|
|
254
|
+
const task = await backend.create({
|
|
255
|
+
description: "Test task",
|
|
256
|
+
created_by: testAgentId,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const retrieved = await backend.get(task.id);
|
|
260
|
+
expect(retrieved).not.toBeNull();
|
|
261
|
+
expect(retrieved!.id).toBe(task.id);
|
|
262
|
+
expect(retrieved!.external_id).toBe(task.external_id);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("should return null for non-existent task", async () => {
|
|
266
|
+
const result = await backend.get("task_nonexistent");
|
|
267
|
+
expect(result).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe("delete", () => {
|
|
272
|
+
it("should delete the task and issue", async () => {
|
|
273
|
+
const task = await backend.create({
|
|
274
|
+
description: "To delete",
|
|
275
|
+
created_by: testAgentId,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await backend.delete(task.id);
|
|
279
|
+
expect(client.deleteIssue).toHaveBeenCalledWith(task.external_id);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
284
|
+
// Status Transitions
|
|
285
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
describe("assign", () => {
|
|
288
|
+
it("should assign task and sync to OpenTasks", async () => {
|
|
289
|
+
const task = await backend.create({
|
|
290
|
+
description: "Assign me",
|
|
291
|
+
created_by: testAgentId,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
await backend.assign(task.id, "agent_worker1");
|
|
295
|
+
|
|
296
|
+
const updated = await backend.get(task.id);
|
|
297
|
+
expect(updated!.status).toBe("assigned");
|
|
298
|
+
expect(updated!.assigned_agent).toBe("agent_worker1");
|
|
299
|
+
|
|
300
|
+
// Verify OpenTasks was updated
|
|
301
|
+
expect(client.updateIssue).toHaveBeenCalledWith(
|
|
302
|
+
task.external_id,
|
|
303
|
+
expect.objectContaining({
|
|
304
|
+
assignee: "agent_worker1",
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe("unassign", () => {
|
|
311
|
+
it("should unassign task and clear in OpenTasks", async () => {
|
|
312
|
+
const task = await backend.create({
|
|
313
|
+
description: "Unassign me",
|
|
314
|
+
created_by: testAgentId,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
await backend.assign(task.id, "agent_worker1");
|
|
318
|
+
await backend.unassign(task.id);
|
|
319
|
+
|
|
320
|
+
const updated = await backend.get(task.id);
|
|
321
|
+
expect(updated!.assigned_agent).toBeUndefined();
|
|
322
|
+
|
|
323
|
+
// Verify OpenTasks cleared
|
|
324
|
+
expect(client.updateIssue).toHaveBeenCalledWith(
|
|
325
|
+
task.external_id,
|
|
326
|
+
expect.objectContaining({
|
|
327
|
+
assignee: null,
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("should throw for non-assigned task", async () => {
|
|
333
|
+
const task = await backend.create({
|
|
334
|
+
description: "Not assigned",
|
|
335
|
+
created_by: testAgentId,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await expect(backend.unassign(task.id)).rejects.toThrow(
|
|
339
|
+
OpenTasksBackendError
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("start", () => {
|
|
345
|
+
it("should transition to in_progress and sync", async () => {
|
|
346
|
+
const task = await backend.create({
|
|
347
|
+
description: "Start me",
|
|
348
|
+
created_by: testAgentId,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
await backend.start(task.id);
|
|
352
|
+
|
|
353
|
+
const updated = await backend.get(task.id);
|
|
354
|
+
expect(updated!.status).toBe("in_progress");
|
|
355
|
+
|
|
356
|
+
expect(client.updateIssue).toHaveBeenCalledWith(
|
|
357
|
+
task.external_id,
|
|
358
|
+
expect.objectContaining({ status: "in_progress" })
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("should reject invalid transitions", async () => {
|
|
363
|
+
const task = await backend.create({
|
|
364
|
+
description: "Complete me first",
|
|
365
|
+
created_by: testAgentId,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Move to completed
|
|
369
|
+
await backend.start(task.id);
|
|
370
|
+
await backend.complete(task.id);
|
|
371
|
+
|
|
372
|
+
// Can't start a completed task
|
|
373
|
+
await expect(backend.start(task.id)).rejects.toThrow(
|
|
374
|
+
OpenTasksBackendError
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe("complete", () => {
|
|
380
|
+
it("should complete task with outputs and close issue", async () => {
|
|
381
|
+
const task = await backend.create({
|
|
382
|
+
description: "Complete me",
|
|
383
|
+
created_by: testAgentId,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
await backend.start(task.id);
|
|
387
|
+
await backend.complete(task.id, {
|
|
388
|
+
summary: "Done!",
|
|
389
|
+
data: { files_changed: 3 },
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const updated = await backend.get(task.id);
|
|
393
|
+
expect(updated!.status).toBe("completed");
|
|
394
|
+
expect(updated!.outputs).toEqual(
|
|
395
|
+
expect.objectContaining({ summary: "Done!" })
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
// Verify issue was closed
|
|
399
|
+
expect(client.updateIssue).toHaveBeenCalledWith(
|
|
400
|
+
task.external_id,
|
|
401
|
+
expect.objectContaining({ status: "closed" })
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe("fail", () => {
|
|
407
|
+
it("should fail task and close issue with error metadata", async () => {
|
|
408
|
+
const task = await backend.create({
|
|
409
|
+
description: "Fail me",
|
|
410
|
+
created_by: testAgentId,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
await backend.start(task.id);
|
|
414
|
+
await backend.fail(task.id, {
|
|
415
|
+
message: "Something went wrong",
|
|
416
|
+
code: "COMPILE_ERROR",
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const updated = await backend.get(task.id);
|
|
420
|
+
expect(updated!.status).toBe("failed");
|
|
421
|
+
|
|
422
|
+
// Verify issue was closed with error metadata
|
|
423
|
+
expect(client.updateIssue).toHaveBeenCalledWith(
|
|
424
|
+
task.external_id,
|
|
425
|
+
expect.objectContaining({
|
|
426
|
+
status: "closed",
|
|
427
|
+
metadata: expect.objectContaining({
|
|
428
|
+
macro_agent_failed: true,
|
|
429
|
+
macro_agent_error: "Something went wrong",
|
|
430
|
+
}),
|
|
431
|
+
})
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
437
|
+
// Dependencies
|
|
438
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
describe("addBlocker / removeBlocker", () => {
|
|
441
|
+
it("should create a blocks edge in OpenTasks", async () => {
|
|
442
|
+
const blocker = await backend.create({
|
|
443
|
+
description: "Blocker task",
|
|
444
|
+
created_by: testAgentId,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const blocked = await backend.create({
|
|
448
|
+
description: "Blocked task",
|
|
449
|
+
created_by: testAgentId,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await backend.addBlocker(blocked.id, blocker.id);
|
|
453
|
+
|
|
454
|
+
// Verify edge was created
|
|
455
|
+
expect(client.createEdge).toHaveBeenCalledWith(
|
|
456
|
+
blocker.external_id,
|
|
457
|
+
blocked.external_id,
|
|
458
|
+
"blocks"
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
// Verify task is now blocked
|
|
462
|
+
const updated = await backend.get(blocked.id);
|
|
463
|
+
expect(updated!.isBlocked).toBe(true);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("should remove a blocks edge in OpenTasks", async () => {
|
|
467
|
+
const blocker = await backend.create({
|
|
468
|
+
description: "Blocker task",
|
|
469
|
+
created_by: testAgentId,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const blocked = await backend.create({
|
|
473
|
+
description: "Blocked task",
|
|
474
|
+
created_by: testAgentId,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
await backend.addBlocker(blocked.id, blocker.id);
|
|
478
|
+
await backend.removeBlocker(blocked.id, blocker.id);
|
|
479
|
+
|
|
480
|
+
expect(client.removeEdge).toHaveBeenCalledWith(
|
|
481
|
+
blocker.external_id,
|
|
482
|
+
blocked.external_id,
|
|
483
|
+
"blocks"
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
describe("getBlockers / getBlocking", () => {
|
|
489
|
+
it("should return blockers from OpenTasks graph", async () => {
|
|
490
|
+
const blocker = await backend.create({
|
|
491
|
+
description: "Blocker",
|
|
492
|
+
created_by: testAgentId,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const blocked = await backend.create({
|
|
496
|
+
description: "Blocked",
|
|
497
|
+
created_by: testAgentId,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
await backend.addBlocker(blocked.id, blocker.id);
|
|
501
|
+
|
|
502
|
+
const blockers = await backend.getBlockers(blocked.id);
|
|
503
|
+
expect(blockers).toHaveLength(1);
|
|
504
|
+
expect(blockers[0].id).toBe(blocker.id);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("should return tasks blocked by a given task", async () => {
|
|
508
|
+
const blocker = await backend.create({
|
|
509
|
+
description: "Blocker",
|
|
510
|
+
created_by: testAgentId,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const blocked = await backend.create({
|
|
514
|
+
description: "Blocked",
|
|
515
|
+
created_by: testAgentId,
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
await backend.addBlocker(blocked.id, blocker.id);
|
|
519
|
+
|
|
520
|
+
const blocking = await backend.getBlocking(blocker.id);
|
|
521
|
+
expect(blocking).toHaveLength(1);
|
|
522
|
+
expect(blocking[0].id).toBe(blocked.id);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
527
|
+
// Queries
|
|
528
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
describe("list", () => {
|
|
531
|
+
it("should list all non-blocked tasks", async () => {
|
|
532
|
+
await backend.create({ description: "Task 1", created_by: testAgentId });
|
|
533
|
+
await backend.create({ description: "Task 2", created_by: testAgentId });
|
|
534
|
+
|
|
535
|
+
const tasks = await backend.list();
|
|
536
|
+
expect(tasks).toHaveLength(2);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it("should filter by status", async () => {
|
|
540
|
+
const task = await backend.create({
|
|
541
|
+
description: "Task 1",
|
|
542
|
+
created_by: testAgentId,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
await backend.start(task.id);
|
|
546
|
+
|
|
547
|
+
await backend.create({
|
|
548
|
+
description: "Task 2",
|
|
549
|
+
created_by: testAgentId,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
const inProgress = await backend.list({ status: "in_progress" });
|
|
553
|
+
expect(inProgress).toHaveLength(1);
|
|
554
|
+
expect(inProgress[0].id).toBe(task.id);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("should filter by assigned agent", async () => {
|
|
558
|
+
const t1 = await backend.create({
|
|
559
|
+
description: "Agent 1 task",
|
|
560
|
+
created_by: testAgentId,
|
|
561
|
+
});
|
|
562
|
+
await backend.create({
|
|
563
|
+
description: "Unassigned task",
|
|
564
|
+
created_by: testAgentId,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
await backend.assign(t1.id, "agent_worker1");
|
|
568
|
+
|
|
569
|
+
const agentTasks = await backend.list({ assigned_agent: "agent_worker1" });
|
|
570
|
+
expect(agentTasks).toHaveLength(1);
|
|
571
|
+
expect(agentTasks[0].id).toBe(t1.id);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("should exclude blocked tasks by default", async () => {
|
|
575
|
+
const blocker = await backend.create({
|
|
576
|
+
description: "Blocker",
|
|
577
|
+
created_by: testAgentId,
|
|
578
|
+
});
|
|
579
|
+
const blocked = await backend.create({
|
|
580
|
+
description: "Blocked",
|
|
581
|
+
created_by: testAgentId,
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
await backend.addBlocker(blocked.id, blocker.id);
|
|
585
|
+
|
|
586
|
+
const tasks = await backend.list();
|
|
587
|
+
expect(tasks).toHaveLength(1);
|
|
588
|
+
expect(tasks[0].id).toBe(blocker.id);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("should include blocked tasks when requested", async () => {
|
|
592
|
+
const blocker = await backend.create({
|
|
593
|
+
description: "Blocker",
|
|
594
|
+
created_by: testAgentId,
|
|
595
|
+
});
|
|
596
|
+
const blocked = await backend.create({
|
|
597
|
+
description: "Blocked",
|
|
598
|
+
created_by: testAgentId,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
await backend.addBlocker(blocked.id, blocker.id);
|
|
602
|
+
|
|
603
|
+
const tasks = await backend.list({ includeBlocked: true });
|
|
604
|
+
expect(tasks).toHaveLength(2);
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
describe("listReady", () => {
|
|
609
|
+
it("should return only pending/assigned unblocked tasks", async () => {
|
|
610
|
+
const t1 = await backend.create({
|
|
611
|
+
description: "Ready task",
|
|
612
|
+
created_by: testAgentId,
|
|
613
|
+
});
|
|
614
|
+
const t2 = await backend.create({
|
|
615
|
+
description: "Blocked task",
|
|
616
|
+
created_by: testAgentId,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
await backend.addBlocker(t2.id, t1.id);
|
|
620
|
+
|
|
621
|
+
const ready = await backend.listReady();
|
|
622
|
+
expect(ready).toHaveLength(1);
|
|
623
|
+
expect(ready[0].id).toBe(t1.id);
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
describe("getChildren / getSubtaskStatus", () => {
|
|
628
|
+
it("should return children of a parent task", async () => {
|
|
629
|
+
const parent = await backend.create({
|
|
630
|
+
description: "Parent",
|
|
631
|
+
created_by: testAgentId,
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
await backend.create({
|
|
635
|
+
description: "Child 1",
|
|
636
|
+
created_by: testAgentId,
|
|
637
|
+
parent_task: parent.id,
|
|
638
|
+
});
|
|
639
|
+
await backend.create({
|
|
640
|
+
description: "Child 2",
|
|
641
|
+
created_by: testAgentId,
|
|
642
|
+
parent_task: parent.id,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const children = await backend.getChildren(parent.id);
|
|
646
|
+
expect(children).toHaveLength(2);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it("should compute subtask status aggregates", async () => {
|
|
650
|
+
const parent = await backend.create({
|
|
651
|
+
description: "Parent",
|
|
652
|
+
created_by: testAgentId,
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
const c1 = await backend.create({
|
|
656
|
+
description: "Child 1",
|
|
657
|
+
created_by: testAgentId,
|
|
658
|
+
parent_task: parent.id,
|
|
659
|
+
});
|
|
660
|
+
await backend.create({
|
|
661
|
+
description: "Child 2",
|
|
662
|
+
created_by: testAgentId,
|
|
663
|
+
parent_task: parent.id,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
await backend.start(c1.id);
|
|
667
|
+
await backend.complete(c1.id);
|
|
668
|
+
|
|
669
|
+
const status = await backend.getSubtaskStatus(parent.id);
|
|
670
|
+
expect(status.total).toBe(2);
|
|
671
|
+
expect(status.completed).toBe(1);
|
|
672
|
+
expect(status.pending).toBe(1);
|
|
673
|
+
expect(status.allCompleted).toBe(false);
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
678
|
+
// Pull Model
|
|
679
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
680
|
+
|
|
681
|
+
describe("claim / unclaim / listClaimable", () => {
|
|
682
|
+
it("should claim a pending task", async () => {
|
|
683
|
+
await backend.create({
|
|
684
|
+
description: "Claimable task",
|
|
685
|
+
created_by: testAgentId,
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const claimed = await backend.claim("agent_worker1");
|
|
689
|
+
expect(claimed).not.toBeNull();
|
|
690
|
+
expect(claimed!.assigned_agent).toBe("agent_worker1");
|
|
691
|
+
expect(claimed!.status).toBe("assigned");
|
|
692
|
+
|
|
693
|
+
// Verify OpenTasks was updated
|
|
694
|
+
expect(client.updateIssue).toHaveBeenCalledWith(
|
|
695
|
+
claimed!.external_id,
|
|
696
|
+
expect.objectContaining({
|
|
697
|
+
assignee: "agent_worker1",
|
|
698
|
+
})
|
|
699
|
+
);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("should return null when no tasks available", async () => {
|
|
703
|
+
const claimed = await backend.claim("agent_worker1");
|
|
704
|
+
expect(claimed).toBeNull();
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("should not claim blocked tasks", async () => {
|
|
708
|
+
const blocker = await backend.create({
|
|
709
|
+
description: "Blocker",
|
|
710
|
+
created_by: testAgentId,
|
|
711
|
+
});
|
|
712
|
+
const blocked = await backend.create({
|
|
713
|
+
description: "Blocked",
|
|
714
|
+
created_by: testAgentId,
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
await backend.addBlocker(blocked.id, blocker.id);
|
|
718
|
+
|
|
719
|
+
// Claim should pick the blocker, not the blocked task
|
|
720
|
+
const claimed = await backend.claim("agent_worker1");
|
|
721
|
+
expect(claimed).not.toBeNull();
|
|
722
|
+
expect(claimed!.id).toBe(blocker.id);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it("should unclaim a task", async () => {
|
|
726
|
+
await backend.create({
|
|
727
|
+
description: "Unclaim me",
|
|
728
|
+
created_by: testAgentId,
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
const claimed = await backend.claim("agent_worker1");
|
|
732
|
+
await backend.unclaim(claimed!.id);
|
|
733
|
+
|
|
734
|
+
const updated = await backend.get(claimed!.id);
|
|
735
|
+
expect(updated!.assigned_agent).toBeUndefined();
|
|
736
|
+
|
|
737
|
+
// Verify OpenTasks cleared
|
|
738
|
+
expect(client.updateIssue).toHaveBeenCalledWith(
|
|
739
|
+
claimed!.external_id,
|
|
740
|
+
expect.objectContaining({
|
|
741
|
+
assignee: null,
|
|
742
|
+
})
|
|
743
|
+
);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it("should list claimable tasks", async () => {
|
|
747
|
+
await backend.create({
|
|
748
|
+
description: "Available 1",
|
|
749
|
+
created_by: testAgentId,
|
|
750
|
+
tags: ["auth"],
|
|
751
|
+
});
|
|
752
|
+
await backend.create({
|
|
753
|
+
description: "Available 2",
|
|
754
|
+
created_by: testAgentId,
|
|
755
|
+
tags: ["ui"],
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// Claim one
|
|
759
|
+
await backend.claim("agent_worker1");
|
|
760
|
+
|
|
761
|
+
const claimable = await backend.listClaimable();
|
|
762
|
+
expect(claimable).toHaveLength(1);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it("should filter claimable to root tasks only", async () => {
|
|
766
|
+
const parent = await backend.create({
|
|
767
|
+
description: "Parent task",
|
|
768
|
+
created_by: testAgentId,
|
|
769
|
+
});
|
|
770
|
+
await backend.create({
|
|
771
|
+
description: "Child task",
|
|
772
|
+
created_by: testAgentId,
|
|
773
|
+
parent_task: parent.id,
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
const claimable = await backend.listClaimable({ rootTasksOnly: true });
|
|
777
|
+
expect(claimable).toHaveLength(1);
|
|
778
|
+
expect(claimable[0].description).toBe("Parent task");
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
783
|
+
// Import
|
|
784
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
785
|
+
|
|
786
|
+
describe("importIssue", () => {
|
|
787
|
+
it("should import an existing OpenTasks issue as a task", async () => {
|
|
788
|
+
// Create an issue directly in the mock client
|
|
789
|
+
const issue = await client.createIssue({
|
|
790
|
+
title: "External issue",
|
|
791
|
+
status: "open",
|
|
792
|
+
tags: ["imported"],
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
const task = await backend.importIssue(issue.id, testAgentId);
|
|
796
|
+
|
|
797
|
+
expect(task.id).toMatch(/^task_/);
|
|
798
|
+
expect(task.description).toBe("External issue");
|
|
799
|
+
expect(task.status).toBe("pending");
|
|
800
|
+
expect(task.external_id).toBe(issue.id);
|
|
801
|
+
|
|
802
|
+
// Verify ID mapping
|
|
803
|
+
expect(backend.getIssueForTask(task.id)).toBe(issue.id);
|
|
804
|
+
expect(backend.getTaskForIssue(issue.id)).toBe(task.id);
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
it("should import an in_progress issue with correct status", async () => {
|
|
808
|
+
const issue = await client.createIssue({
|
|
809
|
+
title: "Active issue",
|
|
810
|
+
status: "in_progress",
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
// Update the mock to return in_progress
|
|
814
|
+
(client.getIssue as any).mockResolvedValueOnce({
|
|
815
|
+
...issue,
|
|
816
|
+
status: "in_progress",
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
const task = await backend.importIssue(issue.id, testAgentId);
|
|
820
|
+
expect(task.status).toBe("in_progress");
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it("should not re-import an already imported issue", async () => {
|
|
824
|
+
const issue = await client.createIssue({
|
|
825
|
+
title: "Already imported",
|
|
826
|
+
status: "open",
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
const task1 = await backend.importIssue(issue.id, testAgentId);
|
|
830
|
+
const task2 = await backend.importIssue(issue.id, testAgentId);
|
|
831
|
+
|
|
832
|
+
expect(task1.id).toBe(task2.id);
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
837
|
+
// ID Mapping
|
|
838
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
839
|
+
|
|
840
|
+
describe("ID mapping", () => {
|
|
841
|
+
it("should maintain bidirectional task <-> issue mapping", async () => {
|
|
842
|
+
const task = await backend.create({
|
|
843
|
+
description: "Mapped task",
|
|
844
|
+
created_by: testAgentId,
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
const issueId = backend.getIssueForTask(task.id);
|
|
848
|
+
expect(issueId).toBeDefined();
|
|
849
|
+
expect(issueId).toMatch(/^i-mock/);
|
|
850
|
+
|
|
851
|
+
const taskId = backend.getTaskForIssue(issueId!);
|
|
852
|
+
expect(taskId).toBe(task.id);
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
857
|
+
// Event Subscriptions
|
|
858
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
859
|
+
|
|
860
|
+
describe("onTaskChange", () => {
|
|
861
|
+
it("should fire callback on task creation", async () => {
|
|
862
|
+
const events: any[] = [];
|
|
863
|
+
backend.onTaskChange((event) => events.push(event));
|
|
864
|
+
|
|
865
|
+
await backend.create({
|
|
866
|
+
description: "New task",
|
|
867
|
+
created_by: testAgentId,
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
expect(events.length).toBeGreaterThan(0);
|
|
871
|
+
expect(events[0].type).toBe("created");
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it("should filter by taskId", async () => {
|
|
875
|
+
const task1 = await backend.create({
|
|
876
|
+
description: "Task 1",
|
|
877
|
+
created_by: testAgentId,
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
const events: any[] = [];
|
|
881
|
+
backend.onTaskChange(task1.id, (event) => events.push(event));
|
|
882
|
+
|
|
883
|
+
await backend.create({
|
|
884
|
+
description: "Task 2",
|
|
885
|
+
created_by: testAgentId,
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
await backend.start(task1.id);
|
|
889
|
+
|
|
890
|
+
// Should only have events for task1
|
|
891
|
+
expect(events.every((e) => e.taskId === task1.id)).toBe(true);
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
896
|
+
// close()
|
|
897
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
898
|
+
|
|
899
|
+
describe("close()", () => {
|
|
900
|
+
it("should mark backend as closed", async () => {
|
|
901
|
+
await backend.close();
|
|
902
|
+
|
|
903
|
+
// Write operations should throw BACKEND_CLOSED
|
|
904
|
+
await expect(
|
|
905
|
+
backend.create({ description: "after close", created_by: testAgentId })
|
|
906
|
+
).rejects.toThrow("Backend is closed");
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
it("should throw BACKEND_CLOSED on write operations after close", async () => {
|
|
910
|
+
// Create a task before closing
|
|
911
|
+
const task = await backend.create({
|
|
912
|
+
description: "Test task",
|
|
913
|
+
created_by: testAgentId,
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
await backend.close();
|
|
917
|
+
|
|
918
|
+
// All write methods should throw with BACKEND_CLOSED code
|
|
919
|
+
const expectClosed = async (fn: () => Promise<unknown>) => {
|
|
920
|
+
try {
|
|
921
|
+
await fn();
|
|
922
|
+
throw new Error("Expected to throw");
|
|
923
|
+
} catch (err: any) {
|
|
924
|
+
expect(err.code).toBe("BACKEND_CLOSED");
|
|
925
|
+
expect(err.message).toBe("Backend is closed");
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
await expectClosed(() => backend.create({ description: "x", created_by: testAgentId }));
|
|
930
|
+
await expectClosed(() => backend.update(task.id, { description: "x" }));
|
|
931
|
+
await expectClosed(() => backend.delete(task.id));
|
|
932
|
+
await expectClosed(() => backend.assign(task.id, testAgentId));
|
|
933
|
+
await expectClosed(() => backend.start(task.id));
|
|
934
|
+
await expectClosed(() => backend.complete(task.id));
|
|
935
|
+
await expectClosed(() => backend.fail(task.id, { message: "err" }));
|
|
936
|
+
await expectClosed(() => backend.addBlocker(task.id, task.id));
|
|
937
|
+
await expectClosed(() => backend.removeBlocker(task.id, task.id));
|
|
938
|
+
await expectClosed(() => backend.claim!(testAgentId));
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
it("should still allow read operations after close", async () => {
|
|
942
|
+
// Create a task before closing
|
|
943
|
+
const task = await backend.create({
|
|
944
|
+
description: "readable after close",
|
|
945
|
+
created_by: testAgentId,
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
await backend.close();
|
|
949
|
+
|
|
950
|
+
// Read-only operations should still work
|
|
951
|
+
const fetched = await backend.get(task.id);
|
|
952
|
+
expect(fetched).not.toBeNull();
|
|
953
|
+
expect(fetched!.description).toBe("readable after close");
|
|
954
|
+
|
|
955
|
+
const listed = await backend.list();
|
|
956
|
+
expect(listed.length).toBe(1);
|
|
957
|
+
|
|
958
|
+
const children = await backend.getChildren(task.id);
|
|
959
|
+
expect(children).toEqual([]);
|
|
960
|
+
|
|
961
|
+
const status = await backend.getSubtaskStatus(task.id);
|
|
962
|
+
expect(status.total).toBe(0);
|
|
963
|
+
|
|
964
|
+
const history = await backend.getAgentHistory(task.id);
|
|
965
|
+
expect(history).toEqual([]);
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
});
|