pi-harness-runtime 1.1.410 → 1.1.412

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 (171) hide show
  1. package/harness/notification-settings.ts +130 -0
  2. package/harness/skill-remote-source.ts +157 -0
  3. package/harness/skill-sync.ts +74 -22
  4. package/harness/skills-startup.ts +202 -0
  5. package/harness/startup-report.ts +98 -56
  6. package/harness-startup-status.ts +31 -2
  7. package/index.ts +125 -31
  8. package/package.json +1 -1
  9. package/packages/a2a-adapter/package.json +1 -1
  10. package/packages/agent-policy/package.json +1 -1
  11. package/packages/architecture-generator/package.json +1 -1
  12. package/packages/auth/package.json +1 -1
  13. package/packages/autonomous-refactor/package.json +1 -1
  14. package/packages/autonomous-runtime/package.json +1 -1
  15. package/packages/autonomous-runtime/test/types.test.ts +59 -32
  16. package/packages/cache-strategy/package.json +1 -1
  17. package/packages/capability-registry/package.json +1 -1
  18. package/packages/checkpoint/package.json +1 -1
  19. package/packages/cli-plugin-sdk/package.json +1 -1
  20. package/packages/clipboard/package.json +1 -1
  21. package/packages/clipboard-plugin/package.json +1 -1
  22. package/packages/code-generation/package.json +1 -1
  23. package/packages/code-review/package.json +1 -1
  24. package/packages/codex-adapter/package.json +1 -1
  25. package/packages/codex-watcher/package.json +1 -1
  26. package/packages/config-capture/package.json +1 -1
  27. package/packages/connection-runtime/package.json +1 -1
  28. package/packages/context-compiler/package.json +1 -1
  29. package/packages/context-discovery/package.json +1 -1
  30. package/packages/context-manager/package.json +1 -1
  31. package/packages/contracts/package.json +1 -1
  32. package/packages/cookie-sanitizer/package.json +1 -1
  33. package/packages/cookie-sanitizer/test/sync.test.ts +7 -1
  34. package/packages/cost-optimizer/package.json +1 -1
  35. package/packages/dependency-analyzer/dist/src/imports.js +3 -3
  36. package/packages/dependency-analyzer/dist/src/imports.js.map +1 -1
  37. package/packages/dependency-analyzer/package.json +1 -1
  38. package/packages/dependency-analyzer/src/imports.ts +3 -3
  39. package/packages/dependency-analyzer/test/analyzer.test.ts +1 -0
  40. package/packages/django-plugin/package.json +1 -1
  41. package/packages/doc-generator/package.json +1 -1
  42. package/packages/durable-review/package.json +1 -1
  43. package/packages/durable-review/test/acceptance.test.ts +24 -2
  44. package/packages/durable-review/test/smoke.test.ts +85 -47
  45. package/packages/e2e-tester/package.json +1 -1
  46. package/packages/evaluation-engine/package.json +1 -1
  47. package/packages/evaluation-runner/package.json +1 -1
  48. package/packages/event-bus/package.json +1 -1
  49. package/packages/event-bus/test/bus.test.ts +16 -4
  50. package/packages/event-sink/package.json +1 -1
  51. package/packages/event-store/package.json +1 -1
  52. package/packages/experience-replay/package.json +1 -1
  53. package/packages/feedback-collector/package.json +1 -1
  54. package/packages/file-copy-helper/package.json +1 -1
  55. package/packages/framework-detector/package.json +1 -1
  56. package/packages/framework-plugin-sdk/package.json +1 -1
  57. package/packages/frappe-plugin/package.json +1 -1
  58. package/packages/generic-web-plugin/package.json +1 -1
  59. package/packages/health-monitor/package.json +1 -1
  60. package/packages/intent-analyzer/package.json +1 -1
  61. package/packages/jev-judge/package.json +1 -1
  62. package/packages/knowledge-graph/package.json +1 -1
  63. package/packages/knowledge-retrieval/package.json +1 -1
  64. package/packages/laravel-plugin/package.json +1 -1
  65. package/packages/learning-engine/package.json +1 -1
  66. package/packages/logging/package.json +1 -1
  67. package/packages/mcp-adapter/package.json +1 -1
  68. package/packages/memory-engine/package.json +1 -1
  69. package/packages/milestone-manager/package.json +1 -1
  70. package/packages/model-registry/package.json +1 -1
  71. package/packages/nextjs-plugin/package.json +1 -1
  72. package/packages/notification/package.json +1 -1
  73. package/packages/observability/package.json +1 -1
  74. package/packages/okf-indexer/package.json +1 -1
  75. package/packages/paperclip-integration/package.json +1 -1
  76. package/packages/performance-optimizer/package.json +1 -1
  77. package/packages/permission-mode/package.json +1 -1
  78. package/packages/privilege-broker/package.json +1 -1
  79. package/packages/project-analyzer/package.json +1 -1
  80. package/packages/project-bootstrap/package.json +1 -1
  81. package/packages/projection-engine/package.json +1 -1
  82. package/packages/prompt-compiler/package.json +1 -1
  83. package/packages/prompt-versioning/package.json +1 -1
  84. package/packages/provider-adapter-sdk/package.json +1 -1
  85. package/packages/provider-router/package.json +1 -1
  86. package/packages/provider-selector/package.json +1 -1
  87. package/packages/providers/package.json +1 -1
  88. package/packages/qdrant-skills/dist/client.d.ts.map +1 -1
  89. package/packages/qdrant-skills/dist/client.js +21 -8
  90. package/packages/qdrant-skills/dist/client.js.map +1 -1
  91. package/packages/qdrant-skills/dist/embedder.d.ts +37 -0
  92. package/packages/qdrant-skills/dist/embedder.d.ts.map +1 -1
  93. package/packages/qdrant-skills/dist/embedder.js +65 -1
  94. package/packages/qdrant-skills/dist/embedder.js.map +1 -1
  95. package/packages/qdrant-skills/dist/index.d.ts +1 -1
  96. package/packages/qdrant-skills/dist/index.d.ts.map +1 -1
  97. package/packages/qdrant-skills/dist/index.js +1 -1
  98. package/packages/qdrant-skills/dist/index.js.map +1 -1
  99. package/packages/qdrant-skills/package.json +1 -1
  100. package/packages/qdrant-skills/src/client.ts +19 -8
  101. package/packages/qdrant-skills/src/embedder.ts +103 -1
  102. package/packages/qdrant-skills/src/index.ts +5 -0
  103. package/packages/qdrant-skills/test/collection-info.test.ts +129 -0
  104. package/packages/qdrant-skills/test/embedder-batch.test.ts +184 -0
  105. package/packages/quota-manager/package.json +1 -1
  106. package/packages/rate-limiter/package.json +1 -1
  107. package/packages/react-vite-plugin/package.json +1 -1
  108. package/packages/relay/package.json +1 -1
  109. package/packages/release-manager/package.json +1 -1
  110. package/packages/requirement-compiler/package.json +1 -1
  111. package/packages/runtime/package.json +1 -1
  112. package/packages/scheduler/package.json +1 -1
  113. package/packages/scheduler-adapter/package.json +1 -1
  114. package/packages/session/package.json +1 -1
  115. package/packages/session-api/package.json +1 -1
  116. package/packages/session-export/package.json +1 -1
  117. package/packages/shared-context/package.json +1 -1
  118. package/packages/skill-mcp-client/package.json +1 -1
  119. package/packages/skill-registry/package.json +1 -1
  120. package/packages/skills/dist/index.d.ts +19 -1
  121. package/packages/skills/dist/index.d.ts.map +1 -1
  122. package/packages/skills/dist/index.js +7 -2
  123. package/packages/skills/dist/index.js.map +1 -1
  124. package/packages/skills/dist/loader.d.ts +273 -3
  125. package/packages/skills/dist/loader.d.ts.map +1 -1
  126. package/packages/skills/dist/loader.js +525 -36
  127. package/packages/skills/dist/loader.js.map +1 -1
  128. package/packages/skills/dist/remote-source.d.ts +107 -0
  129. package/packages/skills/dist/remote-source.d.ts.map +1 -0
  130. package/packages/skills/dist/remote-source.js +79 -0
  131. package/packages/skills/dist/remote-source.js.map +1 -0
  132. package/packages/skills/dist/vector-schema.d.ts +58 -0
  133. package/packages/skills/dist/vector-schema.d.ts.map +1 -0
  134. package/packages/skills/dist/vector-schema.js +55 -0
  135. package/packages/skills/dist/vector-schema.js.map +1 -0
  136. package/packages/skills/package.json +1 -1
  137. package/packages/skills/src/index.ts +28 -5
  138. package/packages/skills/src/loader.ts +688 -53
  139. package/packages/skills/src/remote-source.ts +148 -0
  140. package/packages/skills/src/vector-schema.ts +72 -0
  141. package/packages/skills/test/init-skills.test.ts +107 -111
  142. package/packages/skills/test/remote-skills.test.ts +242 -0
  143. package/packages/skills/test/remote-source.test.ts +248 -0
  144. package/packages/skills/test/vector-indexing.test.ts +205 -0
  145. package/packages/sprint-planner/package.json +1 -1
  146. package/packages/subscription-engine/package.json +1 -1
  147. package/packages/task-compiler/package.json +1 -1
  148. package/packages/telegram/package.json +1 -1
  149. package/packages/tencentdb-memory/package.json +1 -1
  150. package/packages/tencentdb-sync/package.json +1 -1
  151. package/packages/test-data-generator/package.json +1 -1
  152. package/packages/test-generator/package.json +1 -1
  153. package/packages/todo-bd-sync/package.json +1 -1
  154. package/packages/token-estimation/package.json +1 -1
  155. package/packages/token-optimizer/package.json +1 -1
  156. package/packages/tooling-preference/package.json +1 -1
  157. package/packages/trajectory/package.json +1 -1
  158. package/packages/tui/package.json +1 -1
  159. package/packages/two-phase-review/package.json +1 -1
  160. package/packages/types/package.json +1 -1
  161. package/packages/voice-api/package.json +1 -1
  162. package/packages/webmcp-tester/package.json +1 -1
  163. package/packages/wiki/package.json +1 -1
  164. package/packages/workflow-events/package.json +1 -1
  165. package/packages/workspace-scanner/package.json +1 -1
  166. package/packages/workspace-scanner/test/scanner.test.ts +4 -3
  167. package/packages/worktree/package.json +1 -1
  168. package/packages/write-review/package.json +1 -1
  169. package/scripts/sync-to-qdrant.ts +79 -14
  170. package/packages/autonomous-runtime/test/autonomous-runtime.test.ts +0 -194
  171. package/packages/autonomous-runtime/test/inbox.test.ts +0 -80
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moocoding/write-review",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "Two-agent write-review loop with blackboard coordination",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -14,6 +14,21 @@
14
14
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
15
15
  import { join, basename } from "node:path";
16
16
  import { QdrantClient } from "@qdrant/js-client-rest";
17
+ // The runtime and this script must write into the SAME collection with the
18
+ // SAME vectors: a collection holds vectors of exactly one size, and a vector
19
+ // of a different size cannot be searched with the vectors of the other one.
20
+ // The embedder and the collection name are therefore imported from the
21
+ // packages that own them - they are not repeated here.
22
+ import {
23
+ EMBEDDING_DIMENSIONS,
24
+ EMBEDDING_MODEL,
25
+ createEmbedding,
26
+ createEmbeddingsBatch,
27
+ } from "../packages/qdrant-skills/dist/index.js";
28
+ import {
29
+ LEGACY_SKILL_COLLECTION,
30
+ SKILL_COLLECTION,
31
+ } from "../packages/skills/dist/vector-schema.js";
17
32
 
18
33
  // =============================================================================
19
34
  // SANITIZATION: Replace client-specific names with placeholders
@@ -81,17 +96,17 @@ interface QdrantConfig {
81
96
  function getQdrantConfig(): QdrantConfig | null {
82
97
  let apiKey = process.env.QDRANT_API_KEY;
83
98
  const url = process.env.QDRANT_CLUSTER_ENDPOINT || "https://api.qdrant.tech";
84
- const collection = process.env.QDRANT_COLLECTION || "pi-harness-skills";
85
-
99
+ const collection = process.env.QDRANT_COLLECTION || SKILL_COLLECTION;
100
+
86
101
  if (!apiKey) {
87
102
  const keyPath = `${process.env.HOME}/.pi-harness-runtime/keys/qdrant-api-key.txt`;
88
103
  if (existsSync(keyPath)) {
89
104
  apiKey = readFileSync(keyPath, "utf-8").trim();
90
105
  }
91
106
  }
92
-
107
+
93
108
  if (!apiKey) return null;
94
-
109
+
95
110
  return { url, apiKey, collection };
96
111
  }
97
112
 
@@ -180,15 +195,58 @@ function findSkillDirs(dir: string): string[] {
180
195
  // =============================================================================
181
196
 
182
197
  async function ensureCollection(client: QdrantClient, name: string): Promise<void> {
198
+ const existing = await collectionSchema(client, name);
199
+ if (existing?.dimensions !== undefined && existing.dimensions !== EMBEDDING_DIMENSIONS) {
200
+ // The collection holds vectors of another size. Writing into it would
201
+ // fail, and recreating it would destroy the skills of the other model.
202
+ // The name says so the user can act.
203
+ console.error(
204
+ `[Sync] The collection ${name} holds ${existing.dimensions}-dimensional vectors,` +
205
+ ` but the runtime embeds with ${EMBEDDING_DIMENSIONS} (${EMBEDDING_MODEL}).`,
206
+ );
207
+ if (name === LEGACY_SKILL_COLLECTION) {
208
+ console.error(
209
+ `[Sync] ${LEGACY_SKILL_COLLECTION} is the legacy collection of the MiniLM model.`,
210
+ );
211
+ }
212
+ console.error(
213
+ `[Sync] Use a different collection (QDRANT_COLLECTION) or re-create this one.`,
214
+ );
215
+ process.exit(1);
216
+ }
183
217
  try {
184
218
  await client.createCollection(name, {
185
- vectors: { size: 384, distance: "Cosine" }
219
+ vectors: { size: EMBEDDING_DIMENSIONS, distance: "Cosine" }
186
220
  });
187
221
  } catch {
188
222
  // Already exists - OK
189
223
  }
190
224
  }
191
225
 
226
+ /**
227
+ * The vector size and the distance of a collection.
228
+ *
229
+ * `null` means the collection does not exist (or cannot be read).
230
+ */
231
+ async function collectionSchema(
232
+ client: QdrantClient,
233
+ name: string,
234
+ ): Promise<{ dimensions?: number; distance?: string } | null> {
235
+ try {
236
+ // SAFETY: the Qdrant client answers with the collection description;
237
+ // the shape below is the one field this function reads. The cast is
238
+ // needed because the generated client types the response as `unknown`.
239
+ const info = (await client.getCollection(name)) as unknown as {
240
+ config?: { params?: { vectors?: { size?: number; distance?: string } } };
241
+ };
242
+ const vectors = info?.config?.params?.vectors;
243
+ if (!vectors) return { };
244
+ return { dimensions: vectors.size, distance: vectors.distance };
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+
192
250
  async function clearCollection(client: QdrantClient, name: string): Promise<void> {
193
251
  try {
194
252
  await client.deleteCollection(name);
@@ -203,6 +261,17 @@ async function upsertSkills(
203
261
  collectionName: string,
204
262
  skills: SkillDoc[]
205
263
  ): Promise<void> {
264
+ // The vectors are produced by the same embedder the runtime uses, so the
265
+ // two can search each other's points. The embedding happens in batches:
266
+ // one request per 64 skills instead of one per skill.
267
+ const { embeddings } = await createEmbeddingsBatch(skills.map((skill) => skill.text));
268
+ if (embeddings.length !== skills.length) {
269
+ console.error(
270
+ `[Sync] The embedder returned ${embeddings.length} vectors for ${skills.length} skills.`,
271
+ );
272
+ process.exit(1);
273
+ }
274
+
206
275
  const points = skills.map((skill, idx) => ({
207
276
  id: idx + 1,
208
277
  payload: {
@@ -210,12 +279,9 @@ async function upsertSkills(
210
279
  description: skill.description,
211
280
  body: skill.body,
212
281
  },
213
- vector: {
214
- text: skill.text,
215
- model: "sentence-transformers/all-MiniLM-L6-v2"
216
- }
282
+ vector: embeddings[idx],
217
283
  }));
218
-
284
+
219
285
  await client.upsert(collectionName, { wait: true, points });
220
286
  }
221
287
 
@@ -224,11 +290,10 @@ async function searchSkills(
224
290
  collectionName: string,
225
291
  query: string
226
292
  ): Promise<void> {
293
+ // The query is embedded with the same model as the stored vectors.
294
+ const { embedding } = await createEmbedding(query);
227
295
  const results = await client.query(collectionName, {
228
- query: {
229
- text: query,
230
- model: "sentence-transformers/all-MiniLM-L6-v2"
231
- },
296
+ query: embedding,
232
297
  limit: 10,
233
298
  with_payload: true,
234
299
  } as Parameters<typeof client.query>[1]);
@@ -1,194 +0,0 @@
1
- /**
2
- * Tests for autonomous-runtime (RFC-0101 Phase 1)
3
- *
4
- * Covers: TaskRecord, TaskStatus, TaskLease, TaskInbox, LeaseManager
5
- */
6
-
7
- import { describe, it, expect, beforeEach, afterEach } from "bun:test";
8
- import { mkdtempSync, rmSync, existsSync } from "node:fs";
9
- import { join } from "node:path";
10
- import { tmpdir } from "node:os";
11
-
12
- import type { TaskRecord, TaskLease } from "../src/types.js";
13
- import { TaskInbox, LeaseManager } from "../src/index.js";
14
-
15
- const TEST_DIR = mkdtempSync(join(tmpdir(), "pi-autonomous-"));
16
- const INBOX_DIR = join(TEST_DIR, "inbox");
17
- const LEASE_DIR = join(TEST_DIR, "leases");
18
-
19
- afterEach(() => {
20
- try {
21
- rmSync(TEST_DIR, { recursive: true, force: true });
22
- } catch {
23
- // ignore
24
- }
25
- });
26
-
27
- describe("TaskRecord types", () => {
28
- it("accepts valid task shape", () => {
29
- const task: TaskRecord = {
30
- id: "task-1",
31
- status: "pending",
32
- title: "Test task",
33
- created_at: new Date().toISOString(),
34
- updated_at: new Date().toISOString(),
35
- attempts: 0,
36
- };
37
- expect(task.id).toBe("task-1");
38
- expect(task.status).toBe("pending");
39
- });
40
- });
41
-
42
- describe("LeaseManager", () => {
43
- it("grants exclusive lease to first worker", async () => {
44
- const mgr = new LeaseManager({ leases_dir: LEASE_DIR });
45
- const lease = await mgr.acquire("task-1", "worker-1");
46
- expect(lease).not.toBeNull();
47
- expect(lease!.worker_id).toBe("worker-1");
48
- expect(lease!.task_id).toBe("task-1");
49
- expect(lease!.acquired_at).toBeTruthy();
50
- });
51
-
52
- it("denies lease to second worker", async () => {
53
- const mgr = new LeaseManager({ leases_dir: LEASE_DIR });
54
- await mgr.acquire("task-1", "worker-1");
55
- const second = await mgr.acquire("task-1", "worker-2");
56
- expect(second).toBeNull();
57
- });
58
-
59
- it("releases and allows re-acquisition", async () => {
60
- const mgr = new LeaseManager({ leases_dir: LEASE_DIR });
61
- await mgr.acquire("task-1", "worker-1");
62
- await mgr.release("task-1", "worker-1");
63
- const next = await mgr.acquire("task-1", "worker-2");
64
- expect(next).not.toBeNull();
65
- expect(next!.worker_id).toBe("worker-2");
66
- });
67
-
68
- it("only owner can release", async () => {
69
- const mgr = new LeaseManager({ leases_dir: LEASE_DIR });
70
- await mgr.acquire("task-1", "worker-1");
71
- const released = await mgr.release("task-1", "worker-2");
72
- expect(released).toBe(false);
73
- });
74
-
75
- it("renewLease extends expiry if owner", async () => {
76
- const mgr = new LeaseManager({ leases_dir: LEASE_DIR });
77
- await mgr.acquire("task-1", "worker-1");
78
- const renewed = await mgr.renewLease("task-1", "worker-1");
79
- expect(renewed).toBe(true);
80
- });
81
-
82
- it("renewLease fails for non-owner", async () => {
83
- const mgr = new LeaseManager({ leases_dir: LEASE_DIR });
84
- await mgr.acquire("task-1", "worker-1");
85
- const renewed = await mgr.renewLease("task-1", "worker-2");
86
- expect(renewed).toBe(false);
87
- });
88
-
89
- it("getLease returns current lease", async () => {
90
- const mgr = new LeaseManager({ leases_dir: LEASE_DIR });
91
- const lease = await mgr.acquire("task-1", "worker-1");
92
- const found = await mgr.getLease("task-1");
93
- expect(found).toEqual(lease);
94
- });
95
-
96
- it("expired lease allows re-acquisition", async () => {
97
- const mgr = new LeaseManager({ leases_dir: LEASE_DIR, default_ttl_ms: 1 });
98
- await mgr.acquire("task-1", "worker-1");
99
- // Wait for expiry
100
- await new Promise((r) => setTimeout(r, 10));
101
- const next = await mgr.acquire("task-1", "worker-2");
102
- expect(next).not.toBeNull();
103
- expect(next!.worker_id).toBe("worker-2");
104
- });
105
- });
106
-
107
- describe("TaskInbox", () => {
108
- it("accepts and returns pending tasks", async () => {
109
- const inbox = new TaskInbox({ inbox_dir: INBOX_DIR });
110
- await inbox.submit({
111
- title: "Do the thing",
112
- description: "A test task",
113
- capability: "read",
114
- });
115
- const tasks = await inbox.getPending();
116
- expect(tasks.length).toBe(1);
117
- expect(tasks[0].status).toBe("pending");
118
- });
119
-
120
- it("assigns sequential integer IDs", async () => {
121
- const inbox = new TaskInbox({ inbox_dir: INBOX_DIR });
122
- await inbox.submit({ title: "Task 1", capability: "read" });
123
- await inbox.submit({ title: "Task 2", capability: "read" });
124
- const tasks = await inbox.getPending();
125
- expect(tasks.length).toBe(2);
126
- expect(tasks[0].id).toMatch(/^\d+$/);
127
- expect(tasks[1].id).toMatch(/^\d+$/);
128
- expect(Number(tasks[0].id)).toBeLessThan(Number(tasks[1].id));
129
- });
130
-
131
- it("transitions pending → running → completed", async () => {
132
- const inbox = new TaskInbox({ inbox_dir: INBOX_DIR });
133
- await inbox.submit({ title: "Test", capability: "read" });
134
-
135
- const task = (await inbox.getPending())[0];
136
- const started = await inbox.start(task.id);
137
- expect(started.status).toBe("running");
138
-
139
- const completed = await inbox.complete(task.id, { success: true });
140
- expect(completed.status).toBe("completed");
141
- });
142
-
143
- it("fails a running task", async () => {
144
- const inbox = new TaskInbox({ inbox_dir: INBOX_DIR });
145
- await inbox.submit({ title: "Fail task", capability: "read" });
146
- const task = (await inbox.getPending())[0];
147
- await inbox.start(task.id);
148
- const failed = await inbox.fail(task.id, "Intentional test failure");
149
- expect(failed.status).toBe("failed");
150
- expect(failed.error).toBe("Intentional test failure");
151
- });
152
-
153
- it("retries a failed task if max_attempts not exceeded", async () => {
154
- const inbox = new TaskInbox({
155
- inbox_dir: INBOX_DIR,
156
- options: { max_attempts: 2 },
157
- });
158
- await inbox.submit({ title: "Retry task", capability: "read" });
159
- const task = (await inbox.getPending())[0];
160
- await inbox.start(task.id);
161
- await inbox.fail(task.id, "First attempt failed");
162
- const retried = (await inbox.getPending()).find((t) => t.id === task.id);
163
- expect(retried).not.toBeUndefined();
164
- expect(retried!.attempts).toBe(1);
165
- });
166
-
167
- it("does not retry if max_attempts exceeded", async () => {
168
- const inbox = new TaskInbox({
169
- inbox_dir: INBOX_DIR,
170
- options: { max_attempts: 1 },
171
- });
172
- await inbox.submit({ title: "No retry task", capability: "read" });
173
- const task = (await inbox.getPending())[0];
174
- await inbox.start(task.id);
175
- await inbox.fail(task.id, "Final failure");
176
- const retried = await inbox.getTask(task.id);
177
- expect(retried?.status).toBe("failed");
178
- expect(retried?.attempts).toBe(1);
179
- });
180
-
181
- it("getTask returns specific task by id", async () => {
182
- const inbox = new TaskInbox({ inbox_dir: INBOX_DIR });
183
- await inbox.submit({ title: "Specific task", capability: "read" });
184
- const [task] = await inbox.getPending();
185
- const found = await inbox.getTask(task.id);
186
- expect(found?.id).toBe(task.id);
187
- });
188
-
189
- it("returns null for non-existent task", async () => {
190
- const inbox = new TaskInbox({ inbox_dir: INBOX_DIR });
191
- const found = await inbox.getTask("nonexistent");
192
- expect(found).toBeNull();
193
- });
194
- });
@@ -1,80 +0,0 @@
1
- /**
2
- * Tests for TaskInbox (filesystem-backed).
3
- */
4
-
5
- import { describe, it, expect, beforeEach, afterEach } from "bun:test";
6
- import { mkdtempSync, rmSync, existsSync } from "node:fs";
7
- import { join } from "node:path";
8
- import { tmpdir } from "node:os";
9
- import { TaskInbox } from "../src/inbox.js";
10
-
11
- const DIR = mkdtempSync(join(tmpdir(), "pi-inbox-test-"));
12
-
13
- // Set env so inbox uses our temp dir
14
- process.env.PI_HARNESS_RUNTIME_ROOT = DIR;
15
-
16
- const inbox = new TaskInbox({ rootDir: DIR });
17
-
18
- afterEach(async () => {
19
- await inbox.clear?.();
20
- });
21
-
22
- afterAll(() => {
23
- try { rmSync(DIR, { recursive: true }); } catch { /* ignore */ }
24
- });
25
-
26
- describe("TaskInbox.submit", () => {
27
- it("accepts and returns a pending task", async () => {
28
- const spec = {
29
- id: "test-1",
30
- kind: "once" as const,
31
- priority: 5,
32
- spec: {},
33
- };
34
- const record = await inbox.submit(spec);
35
- expect(record.id).toBe("test-1");
36
- expect(record.status).toBe("pending");
37
- expect(record.submittedAt).toBeDefined();
38
- });
39
-
40
- it("rejects duplicate ids", async () => {
41
- const spec = {
42
- id: "dup-test",
43
- kind: "once" as const,
44
- priority: 5,
45
- spec: {},
46
- };
47
- await inbox.submit(spec);
48
- await expect(inbox.submit(spec)).rejects.toThrow();
49
- });
50
- });
51
-
52
- describe("TaskInbox.list", () => {
53
- it("returns all submitted tasks", async () => {
54
- await inbox.submit({ id: "l1", kind: "once", priority: 1, spec: {} });
55
- await inbox.submit({ id: "l2", kind: "once", priority: 2, spec: {} });
56
- const records = await inbox.list();
57
- expect(records.length).toBeGreaterThanOrEqual(2);
58
- });
59
-
60
- it("filters by status", async () => {
61
- await inbox.submit({ id: "f1", kind: "once", priority: 1, spec: {} });
62
- const pending = await inbox.list({ status: "pending" });
63
- expect(pending.every((r) => r.status === "pending")).toBe(true);
64
- });
65
- });
66
-
67
- describe("TaskInbox.claim", () => {
68
- it("returns null when nothing is pending", async () => {
69
- const lease = await inbox.claim({ workerId: "w1", heartbeatIntervalMs: 30_000 });
70
- expect(lease).toBeNull();
71
- });
72
-
73
- it("returns a lease for a pending task", async () => {
74
- await inbox.submit({ id: "claim-1", kind: "once", priority: 5, spec: {} });
75
- const lease = await inbox.claim({ workerId: "w1", heartbeatIntervalMs: 30_000 });
76
- expect(lease).not.toBeNull();
77
- expect(lease!.taskId).toBe("claim-1");
78
- expect(lease!.status).toBe("active");
79
- });
80
- });