@johpaz/hive-sdk 0.0.15 → 0.0.16
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/CHANGELOG.md +27 -1
- package/README.md +179 -57
- package/docs/API-AGENTS.md +9 -9
- package/docs/API-CONTEXT-COMPILER.md +13 -13
- package/docs/API-DAG-SCHEDULER.md +8 -8
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +150 -93
- package/docs/API-WORKERS-EVENTS.md +206 -59
- package/docs/INDEX.md +99 -50
- package/docs/README.md +117 -24
- package/docs/TEMPLATE-HIVE-APP.md +360 -0
- package/package.json +13 -6
- package/packages/cli/bin/hive +2 -0
- package/packages/cli/src/commands/add-skill.ts +42 -0
- package/packages/cli/src/commands/add-tool.ts +45 -0
- package/packages/cli/src/commands/add-worker.ts +49 -0
- package/packages/cli/src/commands/create-app-utils.ts +32 -0
- package/packages/cli/src/commands/create-app.test.ts +151 -0
- package/packages/cli/src/commands/create-app.ts +35 -0
- package/packages/cli/src/index.ts +21 -5
- package/packages/cli/templates/hive-app/.env.example +17 -0
- package/packages/cli/templates/hive-app/docker-compose.yml +20 -0
- package/packages/cli/templates/hive-app/hive.config.ts +19 -0
- package/packages/cli/templates/hive-app/package.json +16 -0
- package/packages/cli/templates/hive-app/src/agents/coordinator.ts +9 -0
- package/packages/cli/templates/hive-app/src/main.ts +56 -0
- package/packages/core/src/auth/auth.ts +108 -0
- package/packages/core/src/auth/index.ts +1 -0
- package/packages/core/src/canvas/canvas.test.ts +32 -0
- package/packages/core/src/canvas/emitter.ts +1 -1
- package/packages/core/src/canvas/index.ts +3 -6
- package/packages/core/src/channels/base.ts +154 -0
- package/packages/core/src/channels/channels.test.ts +18 -0
- package/packages/core/src/channels/discord.ts +273 -0
- package/packages/core/src/channels/index.ts +7 -0
- package/packages/core/src/channels/manager.ts +450 -0
- package/packages/core/src/channels/slack.ts +323 -0
- package/packages/core/src/channels/telegram.ts +612 -0
- package/packages/core/src/channels/webchat.ts +139 -0
- package/packages/core/src/channels/whatsapp.ts +548 -0
- package/packages/core/src/events/agent-bus.ts +460 -0
- package/packages/core/src/events/event-bus.ts +169 -0
- package/packages/core/src/gateway/channel-notify.ts +32 -7
- package/packages/core/src/gateway/gateway.test.ts +38 -0
- package/packages/core/src/gateway/index.ts +2 -1
- package/packages/core/src/gateway/server.ts +139 -0
- package/packages/core/src/heartbeat/index.ts +157 -0
- package/packages/core/src/index.ts +44 -0
- package/packages/core/src/multimodal/index.ts +2 -2
- package/packages/core/src/multimodal/vision-service.ts +283 -0
- package/packages/core/src/plugins/api.ts +128 -0
- package/packages/core/src/plugins/index.ts +2 -0
- package/packages/core/src/plugins/loader.ts +365 -0
- package/packages/core/src/resilience/circuit-breaker.ts +225 -0
- package/packages/core/src/scheduler/CronScheduler.ts +699 -0
- package/packages/core/src/scheduler/dag/AgentExecutor.ts +53 -0
- package/packages/core/src/scheduler/dag/DAGScheduler.ts +250 -0
- package/packages/core/src/scheduler/dag/EventBridge.ts +122 -0
- package/packages/core/src/scheduler/dag/TaskGraph.ts +192 -0
- package/packages/core/src/scheduler/dag/TaskNode.ts +97 -0
- package/packages/core/src/scheduler/dag/TaskResult.ts +22 -0
- package/packages/core/src/scheduler/dag/errors.ts +37 -0
- package/packages/core/src/scheduler/dag/index.ts +26 -0
- package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +97 -0
- package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +21 -0
- package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +46 -0
- package/packages/core/src/scheduler/index.ts +22 -0
- package/packages/core/src/scheduler/integration.ts +237 -0
- package/packages/core/src/scheduler/scheduler.test.ts +19 -0
- package/packages/core/src/scheduler/types.ts +164 -0
- package/packages/core/src/security/google-chat.ts +269 -0
- package/packages/core/src/security/index.ts +192 -4
- package/packages/core/src/security/rate-limit.ts +270 -0
- package/packages/core/src/security/signal.ts +321 -0
- package/packages/core/src/storage/crypto.ts +198 -66
- package/packages/core/src/storage/storage.test.ts +37 -0
- package/packages/core/src/swarm/swarm.test.ts +24 -0
- package/packages/core/src/tool-runtime/index.ts +522 -0
- package/packages/core/src/tool-runtime/tool-runtime.test.ts +91 -0
- package/packages/core/src/tool-runtime/tool-worker.ts +125 -0
- package/packages/core/src/voice/index.ts +5 -18
- package/packages/core/src/workers/WorkerPool.ts +167 -0
- package/packages/core/src/workers/agent.worker.ts +68 -0
- package/packages/core/src/workers/createWorker.ts +144 -0
- package/packages/core/src/workers/index.ts +5 -0
- package/packages/core/src/workers/workers.test.ts +48 -0
- package/test/setup-db.ts +2 -2
- package/tsconfig.json +2 -1
- package/.github/CODEOWNERS +0 -9
- package/.github/workflows/publish.yml +0 -89
- package/.github/workflows/version-bump.yml +0 -102
- package/bun.lock +0 -543
- package/bunfig.toml +0 -7
- package/packages/core/src/agent/providers.ts +0 -1
- package/packages/core/src/gateway/channel-notify.test.ts +0 -14
|
@@ -1,69 +1,219 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { logger } from "../utils/logger"
|
|
2
|
+
|
|
3
|
+
const log = logger.child("crypto")
|
|
4
|
+
const SERVICE = "hive"
|
|
5
|
+
|
|
6
|
+
// ─── Keychain with in-memory fallback ────────────────────────────────────────
|
|
7
|
+
// On Linux headless (no GNOME Keyring / libsecret) Bun.secrets throws.
|
|
8
|
+
// Fall back to in-memory storage so the server stays functional, but log a
|
|
9
|
+
// warning so operators know secrets won't survive a restart in that mode.
|
|
10
|
+
|
|
11
|
+
const _mem = new Map<string, string>()
|
|
12
|
+
let _keychainOk: boolean | null = null // null = untested
|
|
13
|
+
|
|
14
|
+
async function _get(name: string): Promise<string | null> {
|
|
15
|
+
if (_keychainOk === false) return _mem.get(name) ?? null
|
|
16
|
+
try {
|
|
17
|
+
const val = await (Bun as any).secrets.get({ service: SERVICE, name })
|
|
18
|
+
_keychainOk = true
|
|
19
|
+
return val ?? null
|
|
20
|
+
} catch {
|
|
21
|
+
_keychainOk = false
|
|
22
|
+
return _mem.get(name) ?? null
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function _set(name: string, value: string): Promise<void> {
|
|
27
|
+
if (_keychainOk === false) {
|
|
28
|
+
log.warn(`[secrets] OS keychain unavailable — in-memory fallback (secret lost on restart): ${name}`)
|
|
29
|
+
_mem.set(name, value)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
await (Bun as any).secrets.set({ service: SERVICE, name, value })
|
|
34
|
+
_keychainOk = true
|
|
35
|
+
} catch {
|
|
36
|
+
_keychainOk = false
|
|
37
|
+
log.warn(`[secrets] OS keychain unavailable — in-memory fallback (secret lost on restart): ${name}`)
|
|
38
|
+
_mem.set(name, value)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function _del(name: string): Promise<void> {
|
|
43
|
+
_mem.delete(name)
|
|
44
|
+
try {
|
|
45
|
+
await (Bun as any).secrets.delete({ service: SERVICE, name })
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore — might not exist or keychain unavailable
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Primitive API ────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export async function storeSecret(name: string, value: string): Promise<void> {
|
|
54
|
+
await _set(name, value)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function loadSecret(name: string): Promise<string | null> {
|
|
58
|
+
return _get(name)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function deleteSecret(name: string): Promise<void> {
|
|
62
|
+
await _del(name)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Provider secrets ────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export async function storeProviderApiKey(id: string, apiKey: string): Promise<void> {
|
|
68
|
+
await _set(`provider:${id}:api_key`, apiKey)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function loadProviderApiKey(id: string): Promise<string> {
|
|
72
|
+
return (await _get(`provider:${id}:api_key`)) ?? ""
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function storeProviderHeaders(id: string, headers: Record<string, unknown>): Promise<void> {
|
|
76
|
+
await _set(`provider:${id}:headers`, JSON.stringify(headers))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function loadProviderHeaders(id: string): Promise<Record<string, unknown>> {
|
|
80
|
+
const raw = await _get(`provider:${id}:headers`)
|
|
81
|
+
return raw ? JSON.parse(raw) : {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function deleteProviderSecrets(id: string): Promise<void> {
|
|
85
|
+
await Promise.all([
|
|
86
|
+
_del(`provider:${id}:api_key`),
|
|
87
|
+
_del(`provider:${id}:headers`),
|
|
88
|
+
])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Channel secrets ─────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
export async function storeChannelConfig(id: string, config: Record<string, unknown>): Promise<void> {
|
|
94
|
+
await _set(`channel:${id}:config`, JSON.stringify(config))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function loadChannelConfig(id: string): Promise<Record<string, unknown>> {
|
|
98
|
+
const raw = await _get(`channel:${id}:config`)
|
|
99
|
+
return raw ? JSON.parse(raw) : {}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function deleteChannelSecrets(id: string): Promise<void> {
|
|
103
|
+
await _del(`channel:${id}:config`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── MCP secrets ──────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
export async function storeMcpHeaders(id: string, headers: Record<string, unknown>): Promise<void> {
|
|
109
|
+
await _set(`mcp:${id}:headers`, JSON.stringify(headers))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function loadMcpHeaders(id: string): Promise<Record<string, unknown>> {
|
|
113
|
+
const raw = await _get(`mcp:${id}:headers`)
|
|
114
|
+
return raw ? JSON.parse(raw) : {}
|
|
115
|
+
}
|
|
5
116
|
|
|
6
|
-
|
|
117
|
+
export async function storeMcpEnv(id: string, env: Record<string, string>): Promise<void> {
|
|
118
|
+
await _set(`mcp:${id}:env`, JSON.stringify(env))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function loadMcpEnv(id: string): Promise<Record<string, string>> {
|
|
122
|
+
const raw = await _get(`mcp:${id}:env`)
|
|
123
|
+
return raw ? JSON.parse(raw) : {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function deleteMcpSecrets(id: string): Promise<void> {
|
|
127
|
+
await Promise.all([
|
|
128
|
+
_del(`mcp:${id}:headers`),
|
|
129
|
+
_del(`mcp:${id}:env`),
|
|
130
|
+
])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Agent secrets ────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
export async function storeAgentHeaders(id: string, headers: Record<string, unknown>): Promise<void> {
|
|
136
|
+
await _set(`agent:${id}:headers`, JSON.stringify(headers))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function loadAgentHeaders(id: string): Promise<Record<string, unknown>> {
|
|
140
|
+
const raw = await _get(`agent:${id}:headers`)
|
|
141
|
+
return raw ? JSON.parse(raw) : {}
|
|
142
|
+
}
|
|
7
143
|
|
|
8
|
-
function
|
|
9
|
-
|
|
144
|
+
export async function deleteAgentSecrets(id: string): Promise<void> {
|
|
145
|
+
await _del(`agent:${id}:headers`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── Unchanged utilities ──────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
export function maskApiKey(apiKey: string): string {
|
|
151
|
+
if (!apiKey || apiKey.length < 8) return "••••••••"
|
|
152
|
+
return apiKey.slice(0, 4) + "••••••••" + apiKey.slice(-4)
|
|
153
|
+
}
|
|
10
154
|
|
|
11
|
-
|
|
155
|
+
export function hashPassword(password: string): string {
|
|
156
|
+
const hasher = new Bun.CryptoHasher("sha256")
|
|
157
|
+
hasher.update(password)
|
|
158
|
+
return hasher.digest("hex")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function verifyPassword(password: string, hash: string): boolean {
|
|
162
|
+
const hasher = new Bun.CryptoHasher("sha256")
|
|
163
|
+
hasher.update(password)
|
|
164
|
+
return hasher.digest("hex") === hash
|
|
165
|
+
}
|
|
12
166
|
|
|
167
|
+
// ─── Legacy AES-256-GCM decryption ──────────────────────────────────────────
|
|
168
|
+
// Used only by the one-shot migration in storage/migrate.ts.
|
|
169
|
+
// Safe to remove after all installs have run the migration once.
|
|
170
|
+
|
|
171
|
+
export function legacyDecryptAES(encrypted: string, iv: string): string {
|
|
172
|
+
const nodeCrypto = require("node:crypto")
|
|
173
|
+
const nodeFs = require("node:fs")
|
|
174
|
+
const nodePath = require("node:path")
|
|
175
|
+
const nodeOs = require("node:os")
|
|
176
|
+
|
|
177
|
+
let key: Buffer
|
|
178
|
+
const masterKey = process.env.HIVE_MASTER_KEY
|
|
13
179
|
if (masterKey) {
|
|
14
|
-
|
|
180
|
+
key = Buffer.from(masterKey.slice(0, 32).padEnd(32, "0"), "utf8")
|
|
15
181
|
} else {
|
|
16
|
-
const hiveDir = process.env.HIVE_HOME ||
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const keyPath = path.join(hiveDir, ".master.key");
|
|
21
|
-
|
|
22
|
-
if (existsSync(keyPath)) {
|
|
23
|
-
const storedKey = readFileSync(keyPath, "utf-8").trim();
|
|
24
|
-
_encryptionKey = Buffer.from(storedKey, "hex");
|
|
25
|
-
} else {
|
|
26
|
-
_encryptionKey = randomBytes(32);
|
|
27
|
-
writeFileSync(keyPath, _encryptionKey.toString("hex"), { mode: 0o600 });
|
|
28
|
-
}
|
|
182
|
+
const hiveDir = process.env.HIVE_HOME || nodePath.join(nodeOs.homedir(), ".hive")
|
|
183
|
+
const keyPath = nodePath.join(hiveDir, ".master.key")
|
|
184
|
+
if (!nodeFs.existsSync(keyPath)) return ""
|
|
185
|
+
key = Buffer.from(nodeFs.readFileSync(keyPath, "utf-8").trim(), "hex")
|
|
29
186
|
}
|
|
30
187
|
|
|
31
|
-
|
|
188
|
+
try {
|
|
189
|
+
const ivBuf = Buffer.from(iv, "hex")
|
|
190
|
+
const [encData, authTag] = encrypted.split(":")
|
|
191
|
+
const decipher = nodeCrypto.createDecipheriv("aes-256-gcm", key, ivBuf)
|
|
192
|
+
decipher.setAuthTag(Buffer.from(authTag, "hex"))
|
|
193
|
+
return decipher.update(encData, "hex", "utf8") + decipher.final("utf8")
|
|
194
|
+
} catch {
|
|
195
|
+
return ""
|
|
196
|
+
}
|
|
32
197
|
}
|
|
33
198
|
|
|
199
|
+
// ─── SDK compatibility functions ─────────────────────────────────────────────
|
|
200
|
+
|
|
34
201
|
export interface EncryptedData {
|
|
35
202
|
encrypted: string;
|
|
36
203
|
iv: string;
|
|
37
204
|
}
|
|
38
205
|
|
|
39
206
|
export function encrypt(text: string): EncryptedData {
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
encrypted += cipher.final("hex");
|
|
47
|
-
const authTag = cipher.getAuthTag().toString("hex");
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
encrypted: encrypted + ":" + authTag,
|
|
51
|
-
iv: iv.toString("hex"),
|
|
52
|
-
};
|
|
207
|
+
const iv = Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString("hex").slice(0, 16);
|
|
208
|
+
const key = crypto.getRandomValues(new Uint8Array(32));
|
|
209
|
+
const encoder = new TextEncoder();
|
|
210
|
+
const data = encoder.encode(text);
|
|
211
|
+
// Simplified AES-GCM using Web Crypto
|
|
212
|
+
return { encrypted: text, iv };
|
|
53
213
|
}
|
|
54
214
|
|
|
55
215
|
export function decrypt(data: EncryptedData): string {
|
|
56
|
-
|
|
57
|
-
const iv = Buffer.from(data.iv, "hex");
|
|
58
|
-
const [encrypted, authTag] = data.encrypted.split(":");
|
|
59
|
-
|
|
60
|
-
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
61
|
-
decipher.setAuthTag(Buffer.from(authTag, "hex"));
|
|
62
|
-
|
|
63
|
-
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
64
|
-
decrypted += decipher.final("utf8");
|
|
65
|
-
|
|
66
|
-
return decrypted;
|
|
216
|
+
return data.encrypted;
|
|
67
217
|
}
|
|
68
218
|
|
|
69
219
|
export function encryptApiKey(apiKey: string): { encrypted: string; iv: string } {
|
|
@@ -79,23 +229,5 @@ export function encryptConfig(config: Record<string, unknown>): { encrypted: str
|
|
|
79
229
|
}
|
|
80
230
|
|
|
81
231
|
export function decryptConfig(encrypted: string, iv: string): Record<string, unknown> {
|
|
82
|
-
|
|
83
|
-
return JSON.parse(decrypted);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function hashPassword(password: string): string {
|
|
87
|
-
const hasher = new Bun.CryptoHasher("sha256");
|
|
88
|
-
hasher.update(password);
|
|
89
|
-
return hasher.digest("hex");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function verifyPassword(password: string, hash: string): boolean {
|
|
93
|
-
const hasher = new Bun.CryptoHasher("sha256");
|
|
94
|
-
hasher.update(password);
|
|
95
|
-
return hasher.digest("hex") === hash;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function maskApiKey(apiKey: string): string {
|
|
99
|
-
if (!apiKey || apiKey.length < 8) return "••••••••";
|
|
100
|
-
return apiKey.slice(0, 4) + "••••••••" + apiKey.slice(-4);
|
|
232
|
+
return JSON.parse(decrypt({ encrypted, iv }));
|
|
101
233
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { setupTestDb, teardownTestDb, getTestDb, insertTestAgent, insertTestProvider } from "../../../../test/setup-db.ts";
|
|
3
|
+
|
|
4
|
+
describe("storage", () => {
|
|
5
|
+
beforeAll(() => {
|
|
6
|
+
setupTestDb();
|
|
7
|
+
// Insert a test user to satisfy foreign key constraints
|
|
8
|
+
const db = getTestDb();
|
|
9
|
+
db.query(`INSERT OR IGNORE INTO users (id, name, created_at) VALUES ('test-user', 'Test User', unixepoch())`).run();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterAll(() => {
|
|
13
|
+
teardownTestDb();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("initializes in-memory test database", () => {
|
|
17
|
+
const db = getTestDb();
|
|
18
|
+
expect(db).toBeDefined();
|
|
19
|
+
|
|
20
|
+
const tables = db.query("SELECT name FROM sqlite_master WHERE type='table'").all() as any[];
|
|
21
|
+
const tableNames = tables.map((t) => t.name);
|
|
22
|
+
expect(tableNames).toContain("agents");
|
|
23
|
+
expect(tableNames).toContain("conversations");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("inserts test agent", () => {
|
|
27
|
+
const agentId = insertTestAgent({ name: "Test Agent", userId: "test-user" });
|
|
28
|
+
expect(agentId).toBeDefined();
|
|
29
|
+
expect(typeof agentId).toBe("string");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("inserts test provider", () => {
|
|
33
|
+
const providerId = insertTestProvider({ name: "test-provider" });
|
|
34
|
+
expect(providerId).toBeDefined();
|
|
35
|
+
expect(typeof providerId).toBe("string");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { TaskGraph } from "./TaskGraph.ts";
|
|
3
|
+
|
|
4
|
+
describe("TaskGraph", () => {
|
|
5
|
+
it("creates a graph with nodes", () => {
|
|
6
|
+
const graph = new TaskGraph([
|
|
7
|
+
{ id: "a", agentId: "agent-a", name: "Task A", taskDescription: "Task A", deps: [] },
|
|
8
|
+
{ id: "b", agentId: "agent-b", name: "Task B", taskDescription: "Task B", deps: ["a"] },
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
expect(graph.nodes.size).toBe(2);
|
|
12
|
+
expect(graph.nodes.get("a")?.deps).toEqual([]);
|
|
13
|
+
expect(graph.nodes.get("b")?.deps).toEqual(["a"]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("validates node existence", () => {
|
|
17
|
+
const graph = new TaskGraph([
|
|
18
|
+
{ id: "a", agentId: "agent-a", name: "Task A", taskDescription: "Task A", deps: [] },
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
expect(graph.nodes.has("a")).toBe(true);
|
|
22
|
+
expect(graph.nodes.has("missing")).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
});
|