@agent-workspace/mcp-server 0.4.0 → 0.5.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.
Files changed (53) hide show
  1. package/dist/index.d.ts +5 -15
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +30 -1836
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.test.d.ts +2 -0
  6. package/dist/index.test.d.ts.map +1 -0
  7. package/dist/index.test.js +726 -0
  8. package/dist/index.test.js.map +1 -0
  9. package/dist/tools/artifact.d.ts +6 -0
  10. package/dist/tools/artifact.d.ts.map +1 -0
  11. package/dist/tools/artifact.js +428 -0
  12. package/dist/tools/artifact.js.map +1 -0
  13. package/dist/tools/config.d.ts +6 -0
  14. package/dist/tools/config.d.ts.map +1 -0
  15. package/dist/tools/config.js +91 -0
  16. package/dist/tools/config.js.map +1 -0
  17. package/dist/tools/contract.d.ts +6 -0
  18. package/dist/tools/contract.d.ts.map +1 -0
  19. package/dist/tools/contract.js +233 -0
  20. package/dist/tools/contract.js.map +1 -0
  21. package/dist/tools/identity.d.ts +6 -0
  22. package/dist/tools/identity.d.ts.map +1 -0
  23. package/dist/tools/identity.js +91 -0
  24. package/dist/tools/identity.js.map +1 -0
  25. package/dist/tools/memory.d.ts +6 -0
  26. package/dist/tools/memory.d.ts.map +1 -0
  27. package/dist/tools/memory.js +145 -0
  28. package/dist/tools/memory.js.map +1 -0
  29. package/dist/tools/project.d.ts +6 -0
  30. package/dist/tools/project.d.ts.map +1 -0
  31. package/dist/tools/project.js +184 -0
  32. package/dist/tools/project.js.map +1 -0
  33. package/dist/tools/reputation.d.ts +6 -0
  34. package/dist/tools/reputation.d.ts.map +1 -0
  35. package/dist/tools/reputation.js +206 -0
  36. package/dist/tools/reputation.js.map +1 -0
  37. package/dist/tools/status.d.ts +6 -0
  38. package/dist/tools/status.d.ts.map +1 -0
  39. package/dist/tools/status.js +223 -0
  40. package/dist/tools/status.js.map +1 -0
  41. package/dist/tools/swarm.d.ts +6 -0
  42. package/dist/tools/swarm.d.ts.map +1 -0
  43. package/dist/tools/swarm.js +483 -0
  44. package/dist/tools/swarm.js.map +1 -0
  45. package/dist/tools/task.d.ts +6 -0
  46. package/dist/tools/task.d.ts.map +1 -0
  47. package/dist/tools/task.js +347 -0
  48. package/dist/tools/task.js.map +1 -0
  49. package/dist/utils.d.ts +23 -0
  50. package/dist/utils.d.ts.map +1 -0
  51. package/dist/utils.js +56 -0
  52. package/dist/utils.js.map +1 -0
  53. package/package.json +3 -2
@@ -0,0 +1,726 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdir, writeFile, rm, readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import matter from "gray-matter";
6
+ import { validatePath, sanitizeSlug, safeReadFile } from "@agent-workspace/utils";
7
+ // =============================================================================
8
+ // Test Utilities
9
+ // =============================================================================
10
+ /**
11
+ * Create a temporary workspace directory for testing.
12
+ */
13
+ async function createTestWorkspace() {
14
+ const testDir = join(tmpdir(), `awp-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
15
+ await mkdir(testDir, { recursive: true });
16
+ return testDir;
17
+ }
18
+ /**
19
+ * Create a minimal AWP workspace structure.
20
+ */
21
+ async function setupMinimalWorkspace(root) {
22
+ // Create .awp directory and manifest
23
+ await mkdir(join(root, ".awp"), { recursive: true });
24
+ await writeFile(join(root, ".awp", "workspace.json"), JSON.stringify({
25
+ awp: "1.0.0",
26
+ name: "test-workspace",
27
+ id: "urn:awp:workspace:test123",
28
+ agent: {
29
+ did: "did:awp:test-agent",
30
+ identityFile: "IDENTITY.md",
31
+ },
32
+ }));
33
+ // Create identity file
34
+ const identityContent = matter.stringify("\n# Test Agent\n\nA test agent for unit tests.\n", {
35
+ awp: "1.0.0",
36
+ type: "identity",
37
+ name: "Test Agent",
38
+ creature: "test-bot",
39
+ did: "did:awp:test-agent",
40
+ });
41
+ await writeFile(join(root, "IDENTITY.md"), identityContent);
42
+ // Create soul file
43
+ const soulContent = matter.stringify("\n# Soul\n\nCore values and boundaries.\n", {
44
+ awp: "1.0.0",
45
+ type: "soul",
46
+ values: ["accuracy", "helpfulness"],
47
+ });
48
+ await writeFile(join(root, "SOUL.md"), soulContent);
49
+ // Create user file
50
+ const userContent = matter.stringify("\n# User Profile\n\nThe human user.\n", {
51
+ awp: "1.0.0",
52
+ type: "user",
53
+ name: "Test User",
54
+ });
55
+ await writeFile(join(root, "USER.md"), userContent);
56
+ }
57
+ /**
58
+ * Clean up test workspace.
59
+ */
60
+ async function cleanupTestWorkspace(root) {
61
+ await rm(root, { recursive: true, force: true });
62
+ }
63
+ // =============================================================================
64
+ // Unit Tests: Security Utilities
65
+ // =============================================================================
66
+ describe("security utilities", () => {
67
+ let testRoot;
68
+ beforeEach(async () => {
69
+ testRoot = await createTestWorkspace();
70
+ });
71
+ afterEach(async () => {
72
+ await cleanupTestWorkspace(testRoot);
73
+ });
74
+ describe("validatePath", () => {
75
+ it("allows paths within workspace root", () => {
76
+ const result = validatePath(testRoot, "subdir/file.txt");
77
+ expect(result).toBe(join(testRoot, "subdir/file.txt"));
78
+ });
79
+ it("allows absolute paths within workspace root", () => {
80
+ const result = validatePath(testRoot, join(testRoot, "subdir/file.txt"));
81
+ expect(result).toBe(join(testRoot, "subdir/file.txt"));
82
+ });
83
+ it("throws on directory traversal with ..", () => {
84
+ expect(() => validatePath(testRoot, "../outside.txt")).toThrow("Path traversal detected");
85
+ });
86
+ it("throws on nested directory traversal", () => {
87
+ expect(() => validatePath(testRoot, "subdir/../../outside.txt")).toThrow("Path traversal detected");
88
+ });
89
+ it("throws on absolute paths outside workspace", () => {
90
+ expect(() => validatePath(testRoot, "/etc/passwd")).toThrow("Path traversal detected");
91
+ });
92
+ it("normalizes paths with redundant segments", () => {
93
+ const result = validatePath(testRoot, "./subdir/../subdir/file.txt");
94
+ expect(result).toBe(join(testRoot, "subdir/file.txt"));
95
+ });
96
+ });
97
+ describe("sanitizeSlug", () => {
98
+ it("accepts valid lowercase alphanumeric slugs", () => {
99
+ expect(sanitizeSlug("my-artifact")).toBe("my-artifact");
100
+ expect(sanitizeSlug("test123")).toBe("test123");
101
+ expect(sanitizeSlug("a")).toBe("a");
102
+ });
103
+ it("converts uppercase to lowercase", () => {
104
+ expect(sanitizeSlug("My-Artifact")).toBe("my-artifact");
105
+ expect(sanitizeSlug("TEST")).toBe("test");
106
+ });
107
+ it("trims whitespace", () => {
108
+ expect(sanitizeSlug(" my-slug ")).toBe("my-slug");
109
+ });
110
+ it("rejects slugs starting with hyphen", () => {
111
+ expect(() => sanitizeSlug("-invalid")).toThrow("Invalid slug");
112
+ });
113
+ it("rejects slugs with invalid characters", () => {
114
+ expect(() => sanitizeSlug("my_slug")).toThrow("Invalid slug");
115
+ expect(() => sanitizeSlug("my.slug")).toThrow("Invalid slug");
116
+ expect(() => sanitizeSlug("my slug")).toThrow("Invalid slug");
117
+ expect(() => sanitizeSlug("my/slug")).toThrow("Invalid slug");
118
+ });
119
+ it("rejects empty slugs", () => {
120
+ expect(() => sanitizeSlug("")).toThrow("Invalid slug");
121
+ expect(() => sanitizeSlug(" ")).toThrow("Invalid slug");
122
+ });
123
+ it("rejects slugs longer than 100 characters", () => {
124
+ const longSlug = "a".repeat(101);
125
+ expect(() => sanitizeSlug(longSlug)).toThrow("Slug too long");
126
+ });
127
+ it("accepts slugs exactly 100 characters", () => {
128
+ const maxSlug = "a".repeat(100);
129
+ expect(sanitizeSlug(maxSlug)).toBe(maxSlug);
130
+ });
131
+ });
132
+ describe("safeReadFile", () => {
133
+ it("reads files within size limit", async () => {
134
+ const filePath = join(testRoot, "test.txt");
135
+ const content = "Hello, World!";
136
+ await writeFile(filePath, content);
137
+ const result = await safeReadFile(filePath);
138
+ expect(result).toBe(content);
139
+ });
140
+ it("throws for non-existent files", async () => {
141
+ const filePath = join(testRoot, "nonexistent.txt");
142
+ await expect(safeReadFile(filePath)).rejects.toThrow();
143
+ });
144
+ it("throws for files exceeding 1MB", async () => {
145
+ const filePath = join(testRoot, "large.txt");
146
+ // Create a file just over 1MB
147
+ const largeContent = "x".repeat(1024 * 1024 + 1);
148
+ await writeFile(filePath, largeContent);
149
+ await expect(safeReadFile(filePath)).rejects.toThrow("File too large");
150
+ });
151
+ it("allows files exactly at 1MB limit", async () => {
152
+ const filePath = join(testRoot, "exact.txt");
153
+ const exactContent = "x".repeat(1024 * 1024);
154
+ await writeFile(filePath, exactContent);
155
+ const result = await safeReadFile(filePath);
156
+ expect(result.length).toBe(1024 * 1024);
157
+ });
158
+ });
159
+ });
160
+ // =============================================================================
161
+ // Integration Tests: Tool Handlers
162
+ // =============================================================================
163
+ /**
164
+ * Note: The MCP server uses process.env.AWP_WORKSPACE to determine the workspace root.
165
+ * We use vi.stubEnv to set this for testing, then dynamically import the module
166
+ * to pick up the new env value. However, since the server auto-connects on import,
167
+ * we test the exported utilities and manually construct tool handler tests.
168
+ *
169
+ * For full integration tests, consider refactoring the server to export tool handlers
170
+ * separately from the server initialization.
171
+ */
172
+ describe("workspace file operations", () => {
173
+ let testRoot;
174
+ beforeEach(async () => {
175
+ testRoot = await createTestWorkspace();
176
+ await setupMinimalWorkspace(testRoot);
177
+ });
178
+ afterEach(async () => {
179
+ await cleanupTestWorkspace(testRoot);
180
+ });
181
+ describe("identity file", () => {
182
+ it("exists with valid frontmatter", async () => {
183
+ const content = await readFile(join(testRoot, "IDENTITY.md"), "utf-8");
184
+ const { data } = matter(content);
185
+ expect(data.type).toBe("identity");
186
+ expect(data.name).toBe("Test Agent");
187
+ expect(data.did).toBe("did:awp:test-agent");
188
+ });
189
+ });
190
+ describe("soul file", () => {
191
+ it("exists with valid frontmatter", async () => {
192
+ const content = await readFile(join(testRoot, "SOUL.md"), "utf-8");
193
+ const { data } = matter(content);
194
+ expect(data.type).toBe("soul");
195
+ expect(data.values).toContain("accuracy");
196
+ });
197
+ });
198
+ describe("user file", () => {
199
+ it("exists with valid frontmatter", async () => {
200
+ const content = await readFile(join(testRoot, "USER.md"), "utf-8");
201
+ const { data } = matter(content);
202
+ expect(data.type).toBe("user");
203
+ expect(data.name).toBe("Test User");
204
+ });
205
+ });
206
+ describe("workspace manifest", () => {
207
+ it("contains valid agent DID", async () => {
208
+ const content = await readFile(join(testRoot, ".awp", "workspace.json"), "utf-8");
209
+ const manifest = JSON.parse(content);
210
+ expect(manifest.agent.did).toBe("did:awp:test-agent");
211
+ expect(manifest.id).toMatch(/^urn:awp:workspace:/);
212
+ });
213
+ });
214
+ });
215
+ describe("memory operations", () => {
216
+ let testRoot;
217
+ beforeEach(async () => {
218
+ testRoot = await createTestWorkspace();
219
+ await setupMinimalWorkspace(testRoot);
220
+ });
221
+ afterEach(async () => {
222
+ await cleanupTestWorkspace(testRoot);
223
+ });
224
+ it("creates memory directory and daily log", async () => {
225
+ const memoryDir = join(testRoot, "memory");
226
+ await mkdir(memoryDir, { recursive: true });
227
+ const date = new Date().toISOString().split("T")[0];
228
+ const memoryContent = matter.stringify(`\n# ${date}\n\n- **10:00** — Test entry\n`, {
229
+ awp: "1.0.0",
230
+ type: "memory-daily",
231
+ date,
232
+ entries: [{ time: "10:00", content: "Test entry" }],
233
+ });
234
+ await writeFile(join(memoryDir, `${date}.md`), memoryContent);
235
+ const content = await readFile(join(memoryDir, `${date}.md`), "utf-8");
236
+ const { data } = matter(content);
237
+ expect(data.type).toBe("memory-daily");
238
+ expect(data.date).toBe(date);
239
+ expect(data.entries).toHaveLength(1);
240
+ expect(data.entries[0].content).toBe("Test entry");
241
+ });
242
+ it("creates long-term memory file", async () => {
243
+ const memoryContent = matter.stringify("\n# Long-term Memory\n\nPersistent knowledge.\n", {
244
+ awp: "1.0.0",
245
+ type: "memory-longterm",
246
+ lastUpdated: new Date().toISOString(),
247
+ });
248
+ await writeFile(join(testRoot, "MEMORY.md"), memoryContent);
249
+ const content = await readFile(join(testRoot, "MEMORY.md"), "utf-8");
250
+ const { data } = matter(content);
251
+ expect(data.type).toBe("memory-longterm");
252
+ });
253
+ });
254
+ describe("artifact operations", () => {
255
+ let testRoot;
256
+ beforeEach(async () => {
257
+ testRoot = await createTestWorkspace();
258
+ await setupMinimalWorkspace(testRoot);
259
+ await mkdir(join(testRoot, "artifacts"), { recursive: true });
260
+ });
261
+ afterEach(async () => {
262
+ await cleanupTestWorkspace(testRoot);
263
+ });
264
+ it("creates artifact with valid frontmatter", async () => {
265
+ const now = new Date().toISOString();
266
+ const artifactContent = matter.stringify("\n# Test Artifact\n\nSome research content.\n", {
267
+ awp: "1.0.0",
268
+ smp: "1.0.0",
269
+ type: "knowledge-artifact",
270
+ id: "artifact:test-research",
271
+ title: "Test Research",
272
+ authors: ["did:awp:test-agent"],
273
+ version: 1,
274
+ confidence: 0.8,
275
+ tags: ["research", "testing"],
276
+ created: now,
277
+ lastModified: now,
278
+ modifiedBy: "did:awp:test-agent",
279
+ provenance: [
280
+ {
281
+ agent: "did:awp:test-agent",
282
+ action: "created",
283
+ timestamp: now,
284
+ },
285
+ ],
286
+ });
287
+ await writeFile(join(testRoot, "artifacts", "test-research.md"), artifactContent);
288
+ const content = await readFile(join(testRoot, "artifacts", "test-research.md"), "utf-8");
289
+ const { data, content: body } = matter(content);
290
+ expect(data.type).toBe("knowledge-artifact");
291
+ expect(data.title).toBe("Test Research");
292
+ expect(data.version).toBe(1);
293
+ expect(data.confidence).toBe(0.8);
294
+ expect(data.tags).toContain("research");
295
+ expect(data.provenance).toHaveLength(1);
296
+ expect(body).toContain("Some research content");
297
+ });
298
+ it("increments version on update", async () => {
299
+ const now = new Date().toISOString();
300
+ const v1 = matter.stringify("\nV1 content\n", {
301
+ awp: "1.0.0",
302
+ smp: "1.0.0",
303
+ type: "knowledge-artifact",
304
+ id: "artifact:versioned",
305
+ title: "Versioned Artifact",
306
+ authors: ["did:awp:test-agent"],
307
+ version: 1,
308
+ created: now,
309
+ lastModified: now,
310
+ modifiedBy: "did:awp:test-agent",
311
+ provenance: [],
312
+ });
313
+ await writeFile(join(testRoot, "artifacts", "versioned.md"), v1);
314
+ // Read and update
315
+ const raw = await readFile(join(testRoot, "artifacts", "versioned.md"), "utf-8");
316
+ const parsed = matter(raw);
317
+ parsed.data.version = 2;
318
+ parsed.data.lastModified = new Date().toISOString();
319
+ parsed.content = "\nV2 content\n";
320
+ parsed.data.provenance.push({
321
+ agent: "did:awp:test-agent",
322
+ action: "updated",
323
+ timestamp: new Date().toISOString(),
324
+ });
325
+ await writeFile(join(testRoot, "artifacts", "versioned.md"), matter.stringify(parsed.content, parsed.data));
326
+ const updated = await readFile(join(testRoot, "artifacts", "versioned.md"), "utf-8");
327
+ const { data } = matter(updated);
328
+ expect(data.version).toBe(2);
329
+ expect(data.provenance).toHaveLength(1);
330
+ });
331
+ });
332
+ describe("reputation operations", () => {
333
+ let testRoot;
334
+ beforeEach(async () => {
335
+ testRoot = await createTestWorkspace();
336
+ await setupMinimalWorkspace(testRoot);
337
+ await mkdir(join(testRoot, "reputation"), { recursive: true });
338
+ });
339
+ afterEach(async () => {
340
+ await cleanupTestWorkspace(testRoot);
341
+ });
342
+ it("creates reputation profile with dimensions", async () => {
343
+ const now = new Date().toISOString();
344
+ const profile = matter.stringify("\n# External Agent — Reputation Profile\n\nTracked since today.\n", {
345
+ awp: "1.0.0",
346
+ rdp: "1.0.0",
347
+ type: "reputation-profile",
348
+ id: "reputation:external-agent",
349
+ agentDid: "did:awp:external-agent",
350
+ agentName: "External Agent",
351
+ lastUpdated: now,
352
+ dimensions: {
353
+ reliability: {
354
+ score: 0.85,
355
+ confidence: 0.5,
356
+ sampleSize: 5,
357
+ lastSignal: now,
358
+ },
359
+ },
360
+ domainCompetence: {
361
+ typescript: {
362
+ score: 0.9,
363
+ confidence: 0.6,
364
+ sampleSize: 8,
365
+ lastSignal: now,
366
+ },
367
+ },
368
+ signals: [],
369
+ });
370
+ await writeFile(join(testRoot, "reputation", "external-agent.md"), profile);
371
+ const content = await readFile(join(testRoot, "reputation", "external-agent.md"), "utf-8");
372
+ const { data } = matter(content);
373
+ expect(data.type).toBe("reputation-profile");
374
+ expect(data.dimensions.reliability.score).toBe(0.85);
375
+ expect(data.domainCompetence.typescript.score).toBe(0.9);
376
+ });
377
+ it("applies EWMA to reputation signals", () => {
378
+ const ALPHA = 0.15;
379
+ const existingScore = 0.7;
380
+ const newSignalScore = 0.9;
381
+ // EWMA formula: new = α * signal + (1 - α) * existing
382
+ const expected = ALPHA * newSignalScore + (1 - ALPHA) * existingScore;
383
+ const result = Math.round(expected * 1000) / 1000;
384
+ expect(result).toBe(0.73); // 0.15 * 0.9 + 0.85 * 0.7 = 0.135 + 0.595 = 0.73
385
+ });
386
+ it("calculates confidence from sample size", () => {
387
+ // Confidence formula: 1 - 1 / (1 + sampleSize * 0.1)
388
+ const calculateConfidence = (sampleSize) => Math.round((1 - 1 / (1 + sampleSize * 0.1)) * 100) / 100;
389
+ expect(calculateConfidence(1)).toBe(0.09); // 1 - 1/1.1 ≈ 0.09
390
+ expect(calculateConfidence(5)).toBe(0.33); // 1 - 1/1.5 ≈ 0.33
391
+ expect(calculateConfidence(10)).toBe(0.5); // 1 - 1/2 = 0.5
392
+ expect(calculateConfidence(50)).toBe(0.83); // 1 - 1/6 ≈ 0.83
393
+ });
394
+ });
395
+ describe("contract operations", () => {
396
+ let testRoot;
397
+ beforeEach(async () => {
398
+ testRoot = await createTestWorkspace();
399
+ await setupMinimalWorkspace(testRoot);
400
+ await mkdir(join(testRoot, "contracts"), { recursive: true });
401
+ });
402
+ afterEach(async () => {
403
+ await cleanupTestWorkspace(testRoot);
404
+ });
405
+ it("creates delegation contract with evaluation criteria", async () => {
406
+ const now = new Date().toISOString();
407
+ const contract = matter.stringify("\n# research-task — Delegation Contract\n\nDelegated to external-agent: Research TypeScript patterns.\n", {
408
+ awp: "1.0.0",
409
+ rdp: "1.0.0",
410
+ type: "delegation-contract",
411
+ id: "contract:research-task",
412
+ status: "active",
413
+ delegator: "did:awp:test-agent",
414
+ delegate: "did:awp:external-agent",
415
+ delegateSlug: "external-agent",
416
+ created: now,
417
+ deadline: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
418
+ task: {
419
+ description: "Research TypeScript patterns",
420
+ outputFormat: "markdown",
421
+ outputSlug: "typescript-patterns",
422
+ },
423
+ evaluation: {
424
+ criteria: {
425
+ completeness: 0.3,
426
+ accuracy: 0.4,
427
+ clarity: 0.2,
428
+ timeliness: 0.1,
429
+ },
430
+ result: null,
431
+ },
432
+ });
433
+ await writeFile(join(testRoot, "contracts", "research-task.md"), contract);
434
+ const content = await readFile(join(testRoot, "contracts", "research-task.md"), "utf-8");
435
+ const { data } = matter(content);
436
+ expect(data.type).toBe("delegation-contract");
437
+ expect(data.status).toBe("active");
438
+ expect(data.evaluation.criteria.accuracy).toBe(0.4);
439
+ expect(data.task.description).toBe("Research TypeScript patterns");
440
+ });
441
+ it("calculates weighted evaluation score", () => {
442
+ const criteria = {
443
+ completeness: 0.3,
444
+ accuracy: 0.4,
445
+ clarity: 0.2,
446
+ timeliness: 0.1,
447
+ };
448
+ const scores = {
449
+ completeness: 0.9,
450
+ accuracy: 0.85,
451
+ clarity: 0.8,
452
+ timeliness: 1.0,
453
+ };
454
+ let weightedScore = 0;
455
+ for (const [name, weight] of Object.entries(criteria)) {
456
+ weightedScore += weight * scores[name];
457
+ }
458
+ weightedScore = Math.round(weightedScore * 1000) / 1000;
459
+ // 0.3*0.9 + 0.4*0.85 + 0.2*0.8 + 0.1*1.0 = 0.27 + 0.34 + 0.16 + 0.1 = 0.87
460
+ expect(weightedScore).toBe(0.87);
461
+ });
462
+ });
463
+ describe("project operations", () => {
464
+ let testRoot;
465
+ beforeEach(async () => {
466
+ testRoot = await createTestWorkspace();
467
+ await setupMinimalWorkspace(testRoot);
468
+ await mkdir(join(testRoot, "projects"), { recursive: true });
469
+ });
470
+ afterEach(async () => {
471
+ await cleanupTestWorkspace(testRoot);
472
+ });
473
+ it("creates project with members", async () => {
474
+ const now = new Date().toISOString();
475
+ const project = matter.stringify("\n# Test Project\n\nA test coordination project.\n", {
476
+ awp: "1.0.0",
477
+ cdp: "1.0.0",
478
+ type: "project",
479
+ id: "project:test-project",
480
+ title: "Test Project",
481
+ status: "active",
482
+ owner: "did:awp:test-agent",
483
+ created: now,
484
+ members: [
485
+ { did: "did:awp:test-agent", role: "lead", slug: "self" },
486
+ { did: "did:awp:external-agent", role: "contributor", slug: "external-agent" },
487
+ ],
488
+ taskCount: 0,
489
+ completedCount: 0,
490
+ });
491
+ await writeFile(join(testRoot, "projects", "test-project.md"), project);
492
+ const content = await readFile(join(testRoot, "projects", "test-project.md"), "utf-8");
493
+ const { data } = matter(content);
494
+ expect(data.type).toBe("project");
495
+ expect(data.status).toBe("active");
496
+ expect(data.members).toHaveLength(2);
497
+ expect(data.members[0].role).toBe("lead");
498
+ });
499
+ it("creates task within project", async () => {
500
+ // Create project directory and tasks subdirectory
501
+ await mkdir(join(testRoot, "projects", "test-project", "tasks"), { recursive: true });
502
+ const now = new Date().toISOString();
503
+ const task = matter.stringify("\n# Implement Feature\n\nImplement the new feature.\n", {
504
+ awp: "1.0.0",
505
+ cdp: "1.0.0",
506
+ type: "task",
507
+ id: "task:test-project/implement-feature",
508
+ projectId: "project:test-project",
509
+ title: "Implement Feature",
510
+ status: "pending",
511
+ priority: "high",
512
+ created: now,
513
+ assignee: "did:awp:external-agent",
514
+ assigneeSlug: "external-agent",
515
+ blockedBy: [],
516
+ blocks: [],
517
+ lastModified: now,
518
+ modifiedBy: "did:awp:test-agent",
519
+ });
520
+ await writeFile(join(testRoot, "projects", "test-project", "tasks", "implement-feature.md"), task);
521
+ const content = await readFile(join(testRoot, "projects", "test-project", "tasks", "implement-feature.md"), "utf-8");
522
+ const { data } = matter(content);
523
+ expect(data.type).toBe("task");
524
+ expect(data.status).toBe("pending");
525
+ expect(data.priority).toBe("high");
526
+ expect(data.assigneeSlug).toBe("external-agent");
527
+ });
528
+ it("tracks task dependencies", async () => {
529
+ await mkdir(join(testRoot, "projects", "test-project", "tasks"), { recursive: true });
530
+ const now = new Date().toISOString();
531
+ // Task A (no dependencies)
532
+ const taskA = matter.stringify("\n# Task A\n", {
533
+ awp: "1.0.0",
534
+ cdp: "1.0.0",
535
+ type: "task",
536
+ id: "task:test-project/task-a",
537
+ projectId: "project:test-project",
538
+ title: "Task A",
539
+ status: "completed",
540
+ priority: "medium",
541
+ created: now,
542
+ blockedBy: [],
543
+ blocks: ["task:test-project/task-b"],
544
+ lastModified: now,
545
+ modifiedBy: "did:awp:test-agent",
546
+ });
547
+ await writeFile(join(testRoot, "projects", "test-project", "tasks", "task-a.md"), taskA);
548
+ // Task B (blocked by A)
549
+ const taskB = matter.stringify("\n# Task B\n", {
550
+ awp: "1.0.0",
551
+ cdp: "1.0.0",
552
+ type: "task",
553
+ id: "task:test-project/task-b",
554
+ projectId: "project:test-project",
555
+ title: "Task B",
556
+ status: "pending",
557
+ priority: "medium",
558
+ created: now,
559
+ blockedBy: ["task:test-project/task-a"],
560
+ blocks: [],
561
+ lastModified: now,
562
+ modifiedBy: "did:awp:test-agent",
563
+ });
564
+ await writeFile(join(testRoot, "projects", "test-project", "tasks", "task-b.md"), taskB);
565
+ const contentB = await readFile(join(testRoot, "projects", "test-project", "tasks", "task-b.md"), "utf-8");
566
+ const { data: dataB } = matter(contentB);
567
+ expect(dataB.blockedBy).toContain("task:test-project/task-a");
568
+ });
569
+ });
570
+ describe("workspace status", () => {
571
+ let testRoot;
572
+ beforeEach(async () => {
573
+ testRoot = await createTestWorkspace();
574
+ await setupMinimalWorkspace(testRoot);
575
+ });
576
+ afterEach(async () => {
577
+ await cleanupTestWorkspace(testRoot);
578
+ });
579
+ it("detects missing required files", async () => {
580
+ // Remove SOUL.md
581
+ await rm(join(testRoot, "SOUL.md"));
582
+ const warnings = [];
583
+ const fileChecks = ["IDENTITY.md", "SOUL.md"];
584
+ for (const f of fileChecks) {
585
+ try {
586
+ await readFile(join(testRoot, f), "utf-8");
587
+ }
588
+ catch {
589
+ warnings.push(`${f} missing`);
590
+ }
591
+ }
592
+ expect(warnings).toContain("SOUL.md missing");
593
+ expect(warnings).not.toContain("IDENTITY.md missing");
594
+ });
595
+ it("detects past-deadline contracts", async () => {
596
+ await mkdir(join(testRoot, "contracts"), { recursive: true });
597
+ const pastDeadline = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
598
+ const contract = matter.stringify("\n# Expired Contract\n", {
599
+ awp: "1.0.0",
600
+ rdp: "1.0.0",
601
+ type: "delegation-contract",
602
+ id: "contract:expired",
603
+ status: "active",
604
+ deadline: pastDeadline,
605
+ delegator: "did:awp:test-agent",
606
+ delegate: "did:awp:external-agent",
607
+ delegateSlug: "external-agent",
608
+ created: pastDeadline,
609
+ task: { description: "Expired task" },
610
+ evaluation: { criteria: {}, result: null },
611
+ });
612
+ await writeFile(join(testRoot, "contracts", "expired.md"), contract);
613
+ // Check for past deadline
614
+ const content = await readFile(join(testRoot, "contracts", "expired.md"), "utf-8");
615
+ const { data } = matter(content);
616
+ const now = new Date();
617
+ const isPastDeadline = data.deadline &&
618
+ (data.status === "active" || data.status === "draft") &&
619
+ new Date(data.deadline) < now;
620
+ expect(isPastDeadline).toBe(true);
621
+ });
622
+ it("detects reputation decay warning", async () => {
623
+ await mkdir(join(testRoot, "reputation"), { recursive: true });
624
+ // Create a profile with old lastUpdated
625
+ const oldDate = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString(); // 60 days ago
626
+ const profile = matter.stringify("\n# Stale Agent\n", {
627
+ awp: "1.0.0",
628
+ rdp: "1.0.0",
629
+ type: "reputation-profile",
630
+ id: "reputation:stale-agent",
631
+ agentDid: "did:awp:stale-agent",
632
+ agentName: "Stale Agent",
633
+ lastUpdated: oldDate,
634
+ dimensions: {},
635
+ domainCompetence: {},
636
+ signals: [],
637
+ });
638
+ await writeFile(join(testRoot, "reputation", "stale-agent.md"), profile);
639
+ const content = await readFile(join(testRoot, "reputation", "stale-agent.md"), "utf-8");
640
+ const { data } = matter(content);
641
+ const now = new Date();
642
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
643
+ const daysSince = Math.floor((now.getTime() - new Date(data.lastUpdated).getTime()) / MS_PER_DAY);
644
+ expect(daysSince).toBeGreaterThan(30);
645
+ });
646
+ });
647
+ describe("artifact merge operations", () => {
648
+ let testRoot;
649
+ beforeEach(async () => {
650
+ testRoot = await createTestWorkspace();
651
+ await setupMinimalWorkspace(testRoot);
652
+ await mkdir(join(testRoot, "artifacts"), { recursive: true });
653
+ });
654
+ afterEach(async () => {
655
+ await cleanupTestWorkspace(testRoot);
656
+ });
657
+ it("performs additive merge", async () => {
658
+ const now = new Date().toISOString();
659
+ // Target artifact
660
+ const target = matter.stringify("\n# Target Content\n\nOriginal content.\n", {
661
+ awp: "1.0.0",
662
+ smp: "1.0.0",
663
+ type: "knowledge-artifact",
664
+ id: "artifact:target",
665
+ title: "Target",
666
+ authors: ["did:awp:agent-a"],
667
+ version: 1,
668
+ tags: ["shared"],
669
+ created: now,
670
+ lastModified: now,
671
+ modifiedBy: "did:awp:agent-a",
672
+ provenance: [],
673
+ });
674
+ await writeFile(join(testRoot, "artifacts", "target.md"), target);
675
+ // Source artifact
676
+ const source = matter.stringify("\n# Source Content\n\nAdditional content.\n", {
677
+ awp: "1.0.0",
678
+ smp: "1.0.0",
679
+ type: "knowledge-artifact",
680
+ id: "artifact:source",
681
+ title: "Source",
682
+ authors: ["did:awp:agent-b"],
683
+ version: 1,
684
+ tags: ["unique-tag"],
685
+ confidence: 0.7,
686
+ created: now,
687
+ lastModified: now,
688
+ modifiedBy: "did:awp:agent-b",
689
+ provenance: [],
690
+ });
691
+ await writeFile(join(testRoot, "artifacts", "source.md"), source);
692
+ // Simulate additive merge
693
+ const targetRaw = await readFile(join(testRoot, "artifacts", "target.md"), "utf-8");
694
+ const sourceRaw = await readFile(join(testRoot, "artifacts", "source.md"), "utf-8");
695
+ const targetParsed = matter(targetRaw);
696
+ const sourceParsed = matter(sourceRaw);
697
+ // Add source content
698
+ const separator = `\n---\n*Merged from ${sourceParsed.data.id}*\n\n`;
699
+ targetParsed.content += separator + sourceParsed.content.trim() + "\n";
700
+ // Union authors
701
+ for (const author of sourceParsed.data.authors || []) {
702
+ if (!targetParsed.data.authors.includes(author)) {
703
+ targetParsed.data.authors.push(author);
704
+ }
705
+ }
706
+ // Union tags
707
+ for (const tag of sourceParsed.data.tags || []) {
708
+ if (!targetParsed.data.tags.includes(tag)) {
709
+ targetParsed.data.tags.push(tag);
710
+ }
711
+ }
712
+ // Bump version
713
+ targetParsed.data.version = 2;
714
+ await writeFile(join(testRoot, "artifacts", "target.md"), matter.stringify(targetParsed.content, targetParsed.data));
715
+ const merged = await readFile(join(testRoot, "artifacts", "target.md"), "utf-8");
716
+ const { data, content } = matter(merged);
717
+ expect(data.version).toBe(2);
718
+ expect(data.authors).toContain("did:awp:agent-a");
719
+ expect(data.authors).toContain("did:awp:agent-b");
720
+ expect(data.tags).toContain("shared");
721
+ expect(data.tags).toContain("unique-tag");
722
+ expect(content).toContain("Original content");
723
+ expect(content).toContain("Additional content");
724
+ });
725
+ });
726
+ //# sourceMappingURL=index.test.js.map