@paperclipai/plugin-sdk 2026.3.17-canary.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/LICENSE +21 -0
- package/README.md +888 -0
- package/dist/bundlers.d.ts +57 -0
- package/dist/bundlers.d.ts.map +1 -0
- package/dist/bundlers.js +105 -0
- package/dist/bundlers.js.map +1 -0
- package/dist/define-plugin.d.ts +218 -0
- package/dist/define-plugin.d.ts.map +1 -0
- package/dist/define-plugin.js +85 -0
- package/dist/define-plugin.js.map +1 -0
- package/dist/dev-cli.d.ts +3 -0
- package/dist/dev-cli.d.ts.map +1 -0
- package/dist/dev-cli.js +49 -0
- package/dist/dev-cli.js.map +1 -0
- package/dist/dev-server.d.ts +34 -0
- package/dist/dev-server.d.ts.map +1 -0
- package/dist/dev-server.js +194 -0
- package/dist/dev-server.js.map +1 -0
- package/dist/host-client-factory.d.ts +229 -0
- package/dist/host-client-factory.d.ts.map +1 -0
- package/dist/host-client-factory.js +353 -0
- package/dist/host-client-factory.js.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +881 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +297 -0
- package/dist/protocol.js.map +1 -0
- package/dist/testing.d.ts +63 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +700 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +982 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/components.d.ts +257 -0
- package/dist/ui/components.d.ts.map +1 -0
- package/dist/ui/components.js +97 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/hooks.d.ts +120 -0
- package/dist/ui/hooks.d.ts.map +1 -0
- package/dist/ui/hooks.js +148 -0
- package/dist/ui/hooks.js.map +1 -0
- package/dist/ui/index.d.ts +50 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +48 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/runtime.d.ts +3 -0
- package/dist/ui/runtime.d.ts.map +1 -0
- package/dist/ui/runtime.js +30 -0
- package/dist/ui/runtime.js.map +1 -0
- package/dist/ui/types.d.ts +308 -0
- package/dist/ui/types.d.ts.map +1 -0
- package/dist/ui/types.js +17 -0
- package/dist/ui/types.js.map +1 -0
- package/dist/worker-rpc-host.d.ts +127 -0
- package/dist/worker-rpc-host.d.ts.map +1 -0
- package/dist/worker-rpc-host.js +941 -0
- package/dist/worker-rpc-host.js.map +1 -0
- package/package.json +88 -0
package/dist/testing.js
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
function normalizeScope(input) {
|
|
3
|
+
return {
|
|
4
|
+
scopeKind: input.scopeKind,
|
|
5
|
+
scopeId: input.scopeId,
|
|
6
|
+
namespace: input.namespace ?? "default",
|
|
7
|
+
stateKey: input.stateKey,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function stateMapKey(input) {
|
|
11
|
+
const normalized = normalizeScope(input);
|
|
12
|
+
return `${normalized.scopeKind}|${normalized.scopeId ?? ""}|${normalized.namespace}|${normalized.stateKey}`;
|
|
13
|
+
}
|
|
14
|
+
function allowsEvent(filter, event) {
|
|
15
|
+
if (!filter)
|
|
16
|
+
return true;
|
|
17
|
+
if (filter.companyId && filter.companyId !== String(event.payload?.companyId ?? ""))
|
|
18
|
+
return false;
|
|
19
|
+
if (filter.projectId && filter.projectId !== String(event.payload?.projectId ?? ""))
|
|
20
|
+
return false;
|
|
21
|
+
if (filter.agentId && filter.agentId !== String(event.payload?.agentId ?? ""))
|
|
22
|
+
return false;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
function requireCapability(manifest, allowed, capability) {
|
|
26
|
+
if (allowed.has(capability))
|
|
27
|
+
return;
|
|
28
|
+
throw new Error(`Plugin '${manifest.id}' is missing required capability '${capability}' in test harness`);
|
|
29
|
+
}
|
|
30
|
+
function requireCompanyId(companyId) {
|
|
31
|
+
if (!companyId)
|
|
32
|
+
throw new Error("companyId is required for this operation");
|
|
33
|
+
return companyId;
|
|
34
|
+
}
|
|
35
|
+
function isInCompany(record, companyId) {
|
|
36
|
+
return Boolean(record && record.companyId === companyId);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create an in-memory host harness for plugin worker tests.
|
|
40
|
+
*
|
|
41
|
+
* The harness enforces declared capabilities and simulates host APIs, so tests
|
|
42
|
+
* can validate plugin behavior without spinning up the Paperclip server runtime.
|
|
43
|
+
*/
|
|
44
|
+
export function createTestHarness(options) {
|
|
45
|
+
const manifest = options.manifest;
|
|
46
|
+
const capabilitySet = new Set(options.capabilities ?? manifest.capabilities);
|
|
47
|
+
let currentConfig = { ...(options.config ?? {}) };
|
|
48
|
+
const logs = [];
|
|
49
|
+
const activity = [];
|
|
50
|
+
const metrics = [];
|
|
51
|
+
const state = new Map();
|
|
52
|
+
const entities = new Map();
|
|
53
|
+
const entityExternalIndex = new Map();
|
|
54
|
+
const companies = new Map();
|
|
55
|
+
const projects = new Map();
|
|
56
|
+
const issues = new Map();
|
|
57
|
+
const issueComments = new Map();
|
|
58
|
+
const agents = new Map();
|
|
59
|
+
const goals = new Map();
|
|
60
|
+
const projectWorkspaces = new Map();
|
|
61
|
+
const sessions = new Map();
|
|
62
|
+
const sessionEventCallbacks = new Map();
|
|
63
|
+
const events = [];
|
|
64
|
+
const jobs = new Map();
|
|
65
|
+
const launchers = new Map();
|
|
66
|
+
const dataHandlers = new Map();
|
|
67
|
+
const actionHandlers = new Map();
|
|
68
|
+
const toolHandlers = new Map();
|
|
69
|
+
const ctx = {
|
|
70
|
+
manifest,
|
|
71
|
+
config: {
|
|
72
|
+
async get() {
|
|
73
|
+
return { ...currentConfig };
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
events: {
|
|
77
|
+
on(name, filterOrFn, maybeFn) {
|
|
78
|
+
requireCapability(manifest, capabilitySet, "events.subscribe");
|
|
79
|
+
let registration;
|
|
80
|
+
if (typeof filterOrFn === "function") {
|
|
81
|
+
registration = { name, fn: filterOrFn };
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
if (!maybeFn)
|
|
85
|
+
throw new Error("event handler is required");
|
|
86
|
+
registration = { name, filter: filterOrFn, fn: maybeFn };
|
|
87
|
+
}
|
|
88
|
+
events.push(registration);
|
|
89
|
+
return () => {
|
|
90
|
+
const idx = events.indexOf(registration);
|
|
91
|
+
if (idx !== -1)
|
|
92
|
+
events.splice(idx, 1);
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
async emit(name, companyId, payload) {
|
|
96
|
+
requireCapability(manifest, capabilitySet, "events.emit");
|
|
97
|
+
await harness.emit(`plugin.${manifest.id}.${name}`, payload, { companyId });
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
jobs: {
|
|
101
|
+
register(key, fn) {
|
|
102
|
+
requireCapability(manifest, capabilitySet, "jobs.schedule");
|
|
103
|
+
jobs.set(key, fn);
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
launchers: {
|
|
107
|
+
register(launcher) {
|
|
108
|
+
launchers.set(launcher.id, launcher);
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
http: {
|
|
112
|
+
async fetch(url, init) {
|
|
113
|
+
requireCapability(manifest, capabilitySet, "http.outbound");
|
|
114
|
+
return fetch(url, init);
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
secrets: {
|
|
118
|
+
async resolve(secretRef) {
|
|
119
|
+
requireCapability(manifest, capabilitySet, "secrets.read-ref");
|
|
120
|
+
return `resolved:${secretRef}`;
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
activity: {
|
|
124
|
+
async log(entry) {
|
|
125
|
+
requireCapability(manifest, capabilitySet, "activity.log.write");
|
|
126
|
+
activity.push(entry);
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
state: {
|
|
130
|
+
async get(input) {
|
|
131
|
+
requireCapability(manifest, capabilitySet, "plugin.state.read");
|
|
132
|
+
return state.has(stateMapKey(input)) ? state.get(stateMapKey(input)) : null;
|
|
133
|
+
},
|
|
134
|
+
async set(input, value) {
|
|
135
|
+
requireCapability(manifest, capabilitySet, "plugin.state.write");
|
|
136
|
+
state.set(stateMapKey(input), value);
|
|
137
|
+
},
|
|
138
|
+
async delete(input) {
|
|
139
|
+
requireCapability(manifest, capabilitySet, "plugin.state.write");
|
|
140
|
+
state.delete(stateMapKey(input));
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
entities: {
|
|
144
|
+
async upsert(input) {
|
|
145
|
+
const externalKey = input.externalId
|
|
146
|
+
? `${input.entityType}|${input.scopeKind}|${input.scopeId ?? ""}|${input.externalId}`
|
|
147
|
+
: null;
|
|
148
|
+
const existingId = externalKey ? entityExternalIndex.get(externalKey) : undefined;
|
|
149
|
+
const existing = existingId ? entities.get(existingId) : undefined;
|
|
150
|
+
const now = new Date().toISOString();
|
|
151
|
+
const previousExternalKey = existing?.externalId
|
|
152
|
+
? `${existing.entityType}|${existing.scopeKind}|${existing.scopeId ?? ""}|${existing.externalId}`
|
|
153
|
+
: null;
|
|
154
|
+
const record = existing
|
|
155
|
+
? {
|
|
156
|
+
...existing,
|
|
157
|
+
entityType: input.entityType,
|
|
158
|
+
scopeKind: input.scopeKind,
|
|
159
|
+
scopeId: input.scopeId ?? null,
|
|
160
|
+
externalId: input.externalId ?? null,
|
|
161
|
+
title: input.title ?? null,
|
|
162
|
+
status: input.status ?? null,
|
|
163
|
+
data: input.data,
|
|
164
|
+
updatedAt: now,
|
|
165
|
+
}
|
|
166
|
+
: {
|
|
167
|
+
id: randomUUID(),
|
|
168
|
+
entityType: input.entityType,
|
|
169
|
+
scopeKind: input.scopeKind,
|
|
170
|
+
scopeId: input.scopeId ?? null,
|
|
171
|
+
externalId: input.externalId ?? null,
|
|
172
|
+
title: input.title ?? null,
|
|
173
|
+
status: input.status ?? null,
|
|
174
|
+
data: input.data,
|
|
175
|
+
createdAt: now,
|
|
176
|
+
updatedAt: now,
|
|
177
|
+
};
|
|
178
|
+
entities.set(record.id, record);
|
|
179
|
+
if (previousExternalKey && previousExternalKey !== externalKey) {
|
|
180
|
+
entityExternalIndex.delete(previousExternalKey);
|
|
181
|
+
}
|
|
182
|
+
if (externalKey)
|
|
183
|
+
entityExternalIndex.set(externalKey, record.id);
|
|
184
|
+
return record;
|
|
185
|
+
},
|
|
186
|
+
async list(query) {
|
|
187
|
+
let out = [...entities.values()];
|
|
188
|
+
if (query.entityType)
|
|
189
|
+
out = out.filter((r) => r.entityType === query.entityType);
|
|
190
|
+
if (query.scopeKind)
|
|
191
|
+
out = out.filter((r) => r.scopeKind === query.scopeKind);
|
|
192
|
+
if (query.scopeId)
|
|
193
|
+
out = out.filter((r) => r.scopeId === query.scopeId);
|
|
194
|
+
if (query.externalId)
|
|
195
|
+
out = out.filter((r) => r.externalId === query.externalId);
|
|
196
|
+
if (query.offset)
|
|
197
|
+
out = out.slice(query.offset);
|
|
198
|
+
if (query.limit)
|
|
199
|
+
out = out.slice(0, query.limit);
|
|
200
|
+
return out;
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
projects: {
|
|
204
|
+
async list(input) {
|
|
205
|
+
requireCapability(manifest, capabilitySet, "projects.read");
|
|
206
|
+
const companyId = requireCompanyId(input?.companyId);
|
|
207
|
+
let out = [...projects.values()];
|
|
208
|
+
out = out.filter((project) => project.companyId === companyId);
|
|
209
|
+
if (input?.offset)
|
|
210
|
+
out = out.slice(input.offset);
|
|
211
|
+
if (input?.limit)
|
|
212
|
+
out = out.slice(0, input.limit);
|
|
213
|
+
return out;
|
|
214
|
+
},
|
|
215
|
+
async get(projectId, companyId) {
|
|
216
|
+
requireCapability(manifest, capabilitySet, "projects.read");
|
|
217
|
+
const project = projects.get(projectId);
|
|
218
|
+
return isInCompany(project, companyId) ? project : null;
|
|
219
|
+
},
|
|
220
|
+
async listWorkspaces(projectId, companyId) {
|
|
221
|
+
requireCapability(manifest, capabilitySet, "project.workspaces.read");
|
|
222
|
+
if (!isInCompany(projects.get(projectId), companyId))
|
|
223
|
+
return [];
|
|
224
|
+
return projectWorkspaces.get(projectId) ?? [];
|
|
225
|
+
},
|
|
226
|
+
async getPrimaryWorkspace(projectId, companyId) {
|
|
227
|
+
requireCapability(manifest, capabilitySet, "project.workspaces.read");
|
|
228
|
+
if (!isInCompany(projects.get(projectId), companyId))
|
|
229
|
+
return null;
|
|
230
|
+
const workspaces = projectWorkspaces.get(projectId) ?? [];
|
|
231
|
+
return workspaces.find((workspace) => workspace.isPrimary) ?? null;
|
|
232
|
+
},
|
|
233
|
+
async getWorkspaceForIssue(issueId, companyId) {
|
|
234
|
+
requireCapability(manifest, capabilitySet, "project.workspaces.read");
|
|
235
|
+
const issue = issues.get(issueId);
|
|
236
|
+
if (!isInCompany(issue, companyId))
|
|
237
|
+
return null;
|
|
238
|
+
const projectId = issue?.projectId;
|
|
239
|
+
if (!projectId)
|
|
240
|
+
return null;
|
|
241
|
+
if (!isInCompany(projects.get(projectId), companyId))
|
|
242
|
+
return null;
|
|
243
|
+
const workspaces = projectWorkspaces.get(projectId) ?? [];
|
|
244
|
+
return workspaces.find((workspace) => workspace.isPrimary) ?? null;
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
companies: {
|
|
248
|
+
async list(input) {
|
|
249
|
+
requireCapability(manifest, capabilitySet, "companies.read");
|
|
250
|
+
let out = [...companies.values()];
|
|
251
|
+
if (input?.offset)
|
|
252
|
+
out = out.slice(input.offset);
|
|
253
|
+
if (input?.limit)
|
|
254
|
+
out = out.slice(0, input.limit);
|
|
255
|
+
return out;
|
|
256
|
+
},
|
|
257
|
+
async get(companyId) {
|
|
258
|
+
requireCapability(manifest, capabilitySet, "companies.read");
|
|
259
|
+
return companies.get(companyId) ?? null;
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
issues: {
|
|
263
|
+
async list(input) {
|
|
264
|
+
requireCapability(manifest, capabilitySet, "issues.read");
|
|
265
|
+
const companyId = requireCompanyId(input?.companyId);
|
|
266
|
+
let out = [...issues.values()];
|
|
267
|
+
out = out.filter((issue) => issue.companyId === companyId);
|
|
268
|
+
if (input?.projectId)
|
|
269
|
+
out = out.filter((issue) => issue.projectId === input.projectId);
|
|
270
|
+
if (input?.assigneeAgentId)
|
|
271
|
+
out = out.filter((issue) => issue.assigneeAgentId === input.assigneeAgentId);
|
|
272
|
+
if (input?.status)
|
|
273
|
+
out = out.filter((issue) => issue.status === input.status);
|
|
274
|
+
if (input?.offset)
|
|
275
|
+
out = out.slice(input.offset);
|
|
276
|
+
if (input?.limit)
|
|
277
|
+
out = out.slice(0, input.limit);
|
|
278
|
+
return out;
|
|
279
|
+
},
|
|
280
|
+
async get(issueId, companyId) {
|
|
281
|
+
requireCapability(manifest, capabilitySet, "issues.read");
|
|
282
|
+
const issue = issues.get(issueId);
|
|
283
|
+
return isInCompany(issue, companyId) ? issue : null;
|
|
284
|
+
},
|
|
285
|
+
async create(input) {
|
|
286
|
+
requireCapability(manifest, capabilitySet, "issues.create");
|
|
287
|
+
const now = new Date();
|
|
288
|
+
const record = {
|
|
289
|
+
id: randomUUID(),
|
|
290
|
+
companyId: input.companyId,
|
|
291
|
+
projectId: input.projectId ?? null,
|
|
292
|
+
projectWorkspaceId: null,
|
|
293
|
+
goalId: input.goalId ?? null,
|
|
294
|
+
parentId: input.parentId ?? null,
|
|
295
|
+
title: input.title,
|
|
296
|
+
description: input.description ?? null,
|
|
297
|
+
status: "todo",
|
|
298
|
+
priority: input.priority ?? "medium",
|
|
299
|
+
assigneeAgentId: input.assigneeAgentId ?? null,
|
|
300
|
+
assigneeUserId: null,
|
|
301
|
+
checkoutRunId: null,
|
|
302
|
+
executionRunId: null,
|
|
303
|
+
executionAgentNameKey: null,
|
|
304
|
+
executionLockedAt: null,
|
|
305
|
+
createdByAgentId: null,
|
|
306
|
+
createdByUserId: null,
|
|
307
|
+
issueNumber: null,
|
|
308
|
+
identifier: null,
|
|
309
|
+
requestDepth: 0,
|
|
310
|
+
billingCode: null,
|
|
311
|
+
assigneeAdapterOverrides: null,
|
|
312
|
+
executionWorkspaceId: null,
|
|
313
|
+
executionWorkspacePreference: null,
|
|
314
|
+
executionWorkspaceSettings: null,
|
|
315
|
+
startedAt: null,
|
|
316
|
+
completedAt: null,
|
|
317
|
+
cancelledAt: null,
|
|
318
|
+
hiddenAt: null,
|
|
319
|
+
createdAt: now,
|
|
320
|
+
updatedAt: now,
|
|
321
|
+
};
|
|
322
|
+
issues.set(record.id, record);
|
|
323
|
+
return record;
|
|
324
|
+
},
|
|
325
|
+
async update(issueId, patch, companyId) {
|
|
326
|
+
requireCapability(manifest, capabilitySet, "issues.update");
|
|
327
|
+
const record = issues.get(issueId);
|
|
328
|
+
if (!isInCompany(record, companyId))
|
|
329
|
+
throw new Error(`Issue not found: ${issueId}`);
|
|
330
|
+
const updated = {
|
|
331
|
+
...record,
|
|
332
|
+
...patch,
|
|
333
|
+
updatedAt: new Date(),
|
|
334
|
+
};
|
|
335
|
+
issues.set(issueId, updated);
|
|
336
|
+
return updated;
|
|
337
|
+
},
|
|
338
|
+
async listComments(issueId, companyId) {
|
|
339
|
+
requireCapability(manifest, capabilitySet, "issue.comments.read");
|
|
340
|
+
if (!isInCompany(issues.get(issueId), companyId))
|
|
341
|
+
return [];
|
|
342
|
+
return issueComments.get(issueId) ?? [];
|
|
343
|
+
},
|
|
344
|
+
async createComment(issueId, body, companyId) {
|
|
345
|
+
requireCapability(manifest, capabilitySet, "issue.comments.create");
|
|
346
|
+
const parentIssue = issues.get(issueId);
|
|
347
|
+
if (!isInCompany(parentIssue, companyId)) {
|
|
348
|
+
throw new Error(`Issue not found: ${issueId}`);
|
|
349
|
+
}
|
|
350
|
+
const now = new Date();
|
|
351
|
+
const comment = {
|
|
352
|
+
id: randomUUID(),
|
|
353
|
+
companyId: parentIssue.companyId,
|
|
354
|
+
issueId,
|
|
355
|
+
authorAgentId: null,
|
|
356
|
+
authorUserId: null,
|
|
357
|
+
body,
|
|
358
|
+
createdAt: now,
|
|
359
|
+
updatedAt: now,
|
|
360
|
+
};
|
|
361
|
+
const current = issueComments.get(issueId) ?? [];
|
|
362
|
+
current.push(comment);
|
|
363
|
+
issueComments.set(issueId, current);
|
|
364
|
+
return comment;
|
|
365
|
+
},
|
|
366
|
+
documents: {
|
|
367
|
+
async list(issueId, companyId) {
|
|
368
|
+
requireCapability(manifest, capabilitySet, "issue.documents.read");
|
|
369
|
+
if (!isInCompany(issues.get(issueId), companyId))
|
|
370
|
+
return [];
|
|
371
|
+
return [];
|
|
372
|
+
},
|
|
373
|
+
async get(issueId, _key, companyId) {
|
|
374
|
+
requireCapability(manifest, capabilitySet, "issue.documents.read");
|
|
375
|
+
if (!isInCompany(issues.get(issueId), companyId))
|
|
376
|
+
return null;
|
|
377
|
+
return null;
|
|
378
|
+
},
|
|
379
|
+
async upsert(input) {
|
|
380
|
+
requireCapability(manifest, capabilitySet, "issue.documents.write");
|
|
381
|
+
const parentIssue = issues.get(input.issueId);
|
|
382
|
+
if (!isInCompany(parentIssue, input.companyId)) {
|
|
383
|
+
throw new Error(`Issue not found: ${input.issueId}`);
|
|
384
|
+
}
|
|
385
|
+
throw new Error("documents.upsert is not implemented in test context");
|
|
386
|
+
},
|
|
387
|
+
async delete(issueId, _key, companyId) {
|
|
388
|
+
requireCapability(manifest, capabilitySet, "issue.documents.write");
|
|
389
|
+
const parentIssue = issues.get(issueId);
|
|
390
|
+
if (!isInCompany(parentIssue, companyId)) {
|
|
391
|
+
throw new Error(`Issue not found: ${issueId}`);
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
agents: {
|
|
397
|
+
async list(input) {
|
|
398
|
+
requireCapability(manifest, capabilitySet, "agents.read");
|
|
399
|
+
const companyId = requireCompanyId(input?.companyId);
|
|
400
|
+
let out = [...agents.values()];
|
|
401
|
+
out = out.filter((agent) => agent.companyId === companyId);
|
|
402
|
+
if (input?.status)
|
|
403
|
+
out = out.filter((agent) => agent.status === input.status);
|
|
404
|
+
if (input?.offset)
|
|
405
|
+
out = out.slice(input.offset);
|
|
406
|
+
if (input?.limit)
|
|
407
|
+
out = out.slice(0, input.limit);
|
|
408
|
+
return out;
|
|
409
|
+
},
|
|
410
|
+
async get(agentId, companyId) {
|
|
411
|
+
requireCapability(manifest, capabilitySet, "agents.read");
|
|
412
|
+
const agent = agents.get(agentId);
|
|
413
|
+
return isInCompany(agent, companyId) ? agent : null;
|
|
414
|
+
},
|
|
415
|
+
async pause(agentId, companyId) {
|
|
416
|
+
requireCapability(manifest, capabilitySet, "agents.pause");
|
|
417
|
+
const cid = requireCompanyId(companyId);
|
|
418
|
+
const agent = agents.get(agentId);
|
|
419
|
+
if (!isInCompany(agent, cid))
|
|
420
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
421
|
+
if (agent.status === "terminated")
|
|
422
|
+
throw new Error("Cannot pause terminated agent");
|
|
423
|
+
const updated = { ...agent, status: "paused", updatedAt: new Date() };
|
|
424
|
+
agents.set(agentId, updated);
|
|
425
|
+
return updated;
|
|
426
|
+
},
|
|
427
|
+
async resume(agentId, companyId) {
|
|
428
|
+
requireCapability(manifest, capabilitySet, "agents.resume");
|
|
429
|
+
const cid = requireCompanyId(companyId);
|
|
430
|
+
const agent = agents.get(agentId);
|
|
431
|
+
if (!isInCompany(agent, cid))
|
|
432
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
433
|
+
if (agent.status === "terminated")
|
|
434
|
+
throw new Error("Cannot resume terminated agent");
|
|
435
|
+
if (agent.status === "pending_approval")
|
|
436
|
+
throw new Error("Pending approval agents cannot be resumed");
|
|
437
|
+
const updated = { ...agent, status: "idle", updatedAt: new Date() };
|
|
438
|
+
agents.set(agentId, updated);
|
|
439
|
+
return updated;
|
|
440
|
+
},
|
|
441
|
+
async invoke(agentId, companyId, opts) {
|
|
442
|
+
requireCapability(manifest, capabilitySet, "agents.invoke");
|
|
443
|
+
const cid = requireCompanyId(companyId);
|
|
444
|
+
const agent = agents.get(agentId);
|
|
445
|
+
if (!isInCompany(agent, cid))
|
|
446
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
447
|
+
if (agent.status === "paused" ||
|
|
448
|
+
agent.status === "terminated" ||
|
|
449
|
+
agent.status === "pending_approval") {
|
|
450
|
+
throw new Error(`Agent is not invokable in its current state: ${agent.status}`);
|
|
451
|
+
}
|
|
452
|
+
return { runId: randomUUID() };
|
|
453
|
+
},
|
|
454
|
+
sessions: {
|
|
455
|
+
async create(agentId, companyId, opts) {
|
|
456
|
+
requireCapability(manifest, capabilitySet, "agent.sessions.create");
|
|
457
|
+
const cid = requireCompanyId(companyId);
|
|
458
|
+
const agent = agents.get(agentId);
|
|
459
|
+
if (!isInCompany(agent, cid))
|
|
460
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
461
|
+
const session = {
|
|
462
|
+
sessionId: randomUUID(),
|
|
463
|
+
agentId,
|
|
464
|
+
companyId: cid,
|
|
465
|
+
status: "active",
|
|
466
|
+
createdAt: new Date().toISOString(),
|
|
467
|
+
};
|
|
468
|
+
sessions.set(session.sessionId, session);
|
|
469
|
+
return session;
|
|
470
|
+
},
|
|
471
|
+
async list(agentId, companyId) {
|
|
472
|
+
requireCapability(manifest, capabilitySet, "agent.sessions.list");
|
|
473
|
+
const cid = requireCompanyId(companyId);
|
|
474
|
+
return [...sessions.values()].filter((s) => s.agentId === agentId && s.companyId === cid && s.status === "active");
|
|
475
|
+
},
|
|
476
|
+
async sendMessage(sessionId, companyId, opts) {
|
|
477
|
+
requireCapability(manifest, capabilitySet, "agent.sessions.send");
|
|
478
|
+
const session = sessions.get(sessionId);
|
|
479
|
+
if (!session || session.status !== "active")
|
|
480
|
+
throw new Error(`Session not found or closed: ${sessionId}`);
|
|
481
|
+
if (session.companyId !== companyId)
|
|
482
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
483
|
+
if (opts.onEvent) {
|
|
484
|
+
sessionEventCallbacks.set(sessionId, opts.onEvent);
|
|
485
|
+
}
|
|
486
|
+
return { runId: randomUUID() };
|
|
487
|
+
},
|
|
488
|
+
async close(sessionId, companyId) {
|
|
489
|
+
requireCapability(manifest, capabilitySet, "agent.sessions.close");
|
|
490
|
+
const session = sessions.get(sessionId);
|
|
491
|
+
if (!session)
|
|
492
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
493
|
+
if (session.companyId !== companyId)
|
|
494
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
495
|
+
session.status = "closed";
|
|
496
|
+
sessionEventCallbacks.delete(sessionId);
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
goals: {
|
|
501
|
+
async list(input) {
|
|
502
|
+
requireCapability(manifest, capabilitySet, "goals.read");
|
|
503
|
+
const companyId = requireCompanyId(input?.companyId);
|
|
504
|
+
let out = [...goals.values()];
|
|
505
|
+
out = out.filter((goal) => goal.companyId === companyId);
|
|
506
|
+
if (input?.level)
|
|
507
|
+
out = out.filter((goal) => goal.level === input.level);
|
|
508
|
+
if (input?.status)
|
|
509
|
+
out = out.filter((goal) => goal.status === input.status);
|
|
510
|
+
if (input?.offset)
|
|
511
|
+
out = out.slice(input.offset);
|
|
512
|
+
if (input?.limit)
|
|
513
|
+
out = out.slice(0, input.limit);
|
|
514
|
+
return out;
|
|
515
|
+
},
|
|
516
|
+
async get(goalId, companyId) {
|
|
517
|
+
requireCapability(manifest, capabilitySet, "goals.read");
|
|
518
|
+
const goal = goals.get(goalId);
|
|
519
|
+
return isInCompany(goal, companyId) ? goal : null;
|
|
520
|
+
},
|
|
521
|
+
async create(input) {
|
|
522
|
+
requireCapability(manifest, capabilitySet, "goals.create");
|
|
523
|
+
const now = new Date();
|
|
524
|
+
const record = {
|
|
525
|
+
id: randomUUID(),
|
|
526
|
+
companyId: input.companyId,
|
|
527
|
+
title: input.title,
|
|
528
|
+
description: input.description ?? null,
|
|
529
|
+
level: input.level ?? "task",
|
|
530
|
+
status: input.status ?? "planned",
|
|
531
|
+
parentId: input.parentId ?? null,
|
|
532
|
+
ownerAgentId: input.ownerAgentId ?? null,
|
|
533
|
+
createdAt: now,
|
|
534
|
+
updatedAt: now,
|
|
535
|
+
};
|
|
536
|
+
goals.set(record.id, record);
|
|
537
|
+
return record;
|
|
538
|
+
},
|
|
539
|
+
async update(goalId, patch, companyId) {
|
|
540
|
+
requireCapability(manifest, capabilitySet, "goals.update");
|
|
541
|
+
const record = goals.get(goalId);
|
|
542
|
+
if (!isInCompany(record, companyId))
|
|
543
|
+
throw new Error(`Goal not found: ${goalId}`);
|
|
544
|
+
const updated = {
|
|
545
|
+
...record,
|
|
546
|
+
...patch,
|
|
547
|
+
updatedAt: new Date(),
|
|
548
|
+
};
|
|
549
|
+
goals.set(goalId, updated);
|
|
550
|
+
return updated;
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
data: {
|
|
554
|
+
register(key, handler) {
|
|
555
|
+
dataHandlers.set(key, handler);
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
actions: {
|
|
559
|
+
register(key, handler) {
|
|
560
|
+
actionHandlers.set(key, handler);
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
streams: (() => {
|
|
564
|
+
const channelCompanyMap = new Map();
|
|
565
|
+
return {
|
|
566
|
+
open(channel, companyId) {
|
|
567
|
+
channelCompanyMap.set(channel, companyId);
|
|
568
|
+
},
|
|
569
|
+
emit(_channel, _event) {
|
|
570
|
+
// No-op in test harness — events are not forwarded
|
|
571
|
+
},
|
|
572
|
+
close(channel) {
|
|
573
|
+
channelCompanyMap.delete(channel);
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
})(),
|
|
577
|
+
tools: {
|
|
578
|
+
register(name, _decl, fn) {
|
|
579
|
+
requireCapability(manifest, capabilitySet, "agent.tools.register");
|
|
580
|
+
toolHandlers.set(name, fn);
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
metrics: {
|
|
584
|
+
async write(name, value, tags) {
|
|
585
|
+
requireCapability(manifest, capabilitySet, "metrics.write");
|
|
586
|
+
metrics.push({ name, value, tags });
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
logger: {
|
|
590
|
+
info(message, meta) {
|
|
591
|
+
logs.push({ level: "info", message, meta });
|
|
592
|
+
},
|
|
593
|
+
warn(message, meta) {
|
|
594
|
+
logs.push({ level: "warn", message, meta });
|
|
595
|
+
},
|
|
596
|
+
error(message, meta) {
|
|
597
|
+
logs.push({ level: "error", message, meta });
|
|
598
|
+
},
|
|
599
|
+
debug(message, meta) {
|
|
600
|
+
logs.push({ level: "debug", message, meta });
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
const harness = {
|
|
605
|
+
ctx,
|
|
606
|
+
seed(input) {
|
|
607
|
+
for (const row of input.companies ?? [])
|
|
608
|
+
companies.set(row.id, row);
|
|
609
|
+
for (const row of input.projects ?? [])
|
|
610
|
+
projects.set(row.id, row);
|
|
611
|
+
for (const row of input.issues ?? [])
|
|
612
|
+
issues.set(row.id, row);
|
|
613
|
+
for (const row of input.issueComments ?? []) {
|
|
614
|
+
const list = issueComments.get(row.issueId) ?? [];
|
|
615
|
+
list.push(row);
|
|
616
|
+
issueComments.set(row.issueId, list);
|
|
617
|
+
}
|
|
618
|
+
for (const row of input.agents ?? [])
|
|
619
|
+
agents.set(row.id, row);
|
|
620
|
+
for (const row of input.goals ?? [])
|
|
621
|
+
goals.set(row.id, row);
|
|
622
|
+
},
|
|
623
|
+
setConfig(config) {
|
|
624
|
+
currentConfig = { ...config };
|
|
625
|
+
},
|
|
626
|
+
async emit(eventType, payload, base) {
|
|
627
|
+
const event = {
|
|
628
|
+
eventId: base?.eventId ?? randomUUID(),
|
|
629
|
+
eventType,
|
|
630
|
+
companyId: base?.companyId ?? "test-company",
|
|
631
|
+
occurredAt: base?.occurredAt ?? new Date().toISOString(),
|
|
632
|
+
actorId: base?.actorId,
|
|
633
|
+
actorType: base?.actorType,
|
|
634
|
+
entityId: base?.entityId,
|
|
635
|
+
entityType: base?.entityType,
|
|
636
|
+
payload,
|
|
637
|
+
};
|
|
638
|
+
for (const handler of events) {
|
|
639
|
+
const exactMatch = handler.name === event.eventType;
|
|
640
|
+
const wildcardPluginAll = handler.name === "plugin.*" && String(event.eventType).startsWith("plugin.");
|
|
641
|
+
const wildcardPluginOne = String(handler.name).endsWith(".*")
|
|
642
|
+
&& String(event.eventType).startsWith(String(handler.name).slice(0, -1));
|
|
643
|
+
if (!exactMatch && !wildcardPluginAll && !wildcardPluginOne)
|
|
644
|
+
continue;
|
|
645
|
+
if (!allowsEvent(handler.filter, event))
|
|
646
|
+
continue;
|
|
647
|
+
await handler.fn(event);
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
async runJob(jobKey, partial = {}) {
|
|
651
|
+
const handler = jobs.get(jobKey);
|
|
652
|
+
if (!handler)
|
|
653
|
+
throw new Error(`No job handler registered for '${jobKey}'`);
|
|
654
|
+
await handler({
|
|
655
|
+
jobKey,
|
|
656
|
+
runId: partial.runId ?? randomUUID(),
|
|
657
|
+
trigger: partial.trigger ?? "manual",
|
|
658
|
+
scheduledAt: partial.scheduledAt ?? new Date().toISOString(),
|
|
659
|
+
});
|
|
660
|
+
},
|
|
661
|
+
async getData(key, params = {}) {
|
|
662
|
+
const handler = dataHandlers.get(key);
|
|
663
|
+
if (!handler)
|
|
664
|
+
throw new Error(`No data handler registered for '${key}'`);
|
|
665
|
+
return await handler(params);
|
|
666
|
+
},
|
|
667
|
+
async performAction(key, params = {}) {
|
|
668
|
+
const handler = actionHandlers.get(key);
|
|
669
|
+
if (!handler)
|
|
670
|
+
throw new Error(`No action handler registered for '${key}'`);
|
|
671
|
+
return await handler(params);
|
|
672
|
+
},
|
|
673
|
+
async executeTool(name, params, runCtx = {}) {
|
|
674
|
+
const handler = toolHandlers.get(name);
|
|
675
|
+
if (!handler)
|
|
676
|
+
throw new Error(`No tool handler registered for '${name}'`);
|
|
677
|
+
const ctxToPass = {
|
|
678
|
+
agentId: runCtx.agentId ?? "agent-test",
|
|
679
|
+
runId: runCtx.runId ?? randomUUID(),
|
|
680
|
+
companyId: runCtx.companyId ?? "company-test",
|
|
681
|
+
projectId: runCtx.projectId ?? "project-test",
|
|
682
|
+
};
|
|
683
|
+
return await handler(params, ctxToPass);
|
|
684
|
+
},
|
|
685
|
+
getState(input) {
|
|
686
|
+
return state.get(stateMapKey(input));
|
|
687
|
+
},
|
|
688
|
+
simulateSessionEvent(sessionId, event) {
|
|
689
|
+
const cb = sessionEventCallbacks.get(sessionId);
|
|
690
|
+
if (!cb)
|
|
691
|
+
throw new Error(`No active session event callback for session: ${sessionId}`);
|
|
692
|
+
cb({ ...event, sessionId });
|
|
693
|
+
},
|
|
694
|
+
logs,
|
|
695
|
+
activity,
|
|
696
|
+
metrics,
|
|
697
|
+
};
|
|
698
|
+
return harness;
|
|
699
|
+
}
|
|
700
|
+
//# sourceMappingURL=testing.js.map
|