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.
- package/harness/notification-settings.ts +130 -0
- package/harness/skill-remote-source.ts +157 -0
- package/harness/skill-sync.ts +74 -22
- package/harness/skills-startup.ts +202 -0
- package/harness/startup-report.ts +98 -56
- package/harness-startup-status.ts +31 -2
- package/index.ts +125 -31
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/autonomous-runtime/test/types.test.ts +59 -32
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cookie-sanitizer/test/sync.test.ts +7 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/dist/src/imports.js +3 -3
- package/packages/dependency-analyzer/dist/src/imports.js.map +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/dependency-analyzer/src/imports.ts +3 -3
- package/packages/dependency-analyzer/test/analyzer.test.ts +1 -0
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/durable-review/test/acceptance.test.ts +24 -2
- package/packages/durable-review/test/smoke.test.ts +85 -47
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-bus/test/bus.test.ts +16 -4
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/dist/client.d.ts.map +1 -1
- package/packages/qdrant-skills/dist/client.js +21 -8
- package/packages/qdrant-skills/dist/client.js.map +1 -1
- package/packages/qdrant-skills/dist/embedder.d.ts +37 -0
- package/packages/qdrant-skills/dist/embedder.d.ts.map +1 -1
- package/packages/qdrant-skills/dist/embedder.js +65 -1
- package/packages/qdrant-skills/dist/embedder.js.map +1 -1
- package/packages/qdrant-skills/dist/index.d.ts +1 -1
- package/packages/qdrant-skills/dist/index.d.ts.map +1 -1
- package/packages/qdrant-skills/dist/index.js +1 -1
- package/packages/qdrant-skills/dist/index.js.map +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/qdrant-skills/src/client.ts +19 -8
- package/packages/qdrant-skills/src/embedder.ts +103 -1
- package/packages/qdrant-skills/src/index.ts +5 -0
- package/packages/qdrant-skills/test/collection-info.test.ts +129 -0
- package/packages/qdrant-skills/test/embedder-batch.test.ts +184 -0
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/dist/index.d.ts +19 -1
- package/packages/skills/dist/index.d.ts.map +1 -1
- package/packages/skills/dist/index.js +7 -2
- package/packages/skills/dist/index.js.map +1 -1
- package/packages/skills/dist/loader.d.ts +273 -3
- package/packages/skills/dist/loader.d.ts.map +1 -1
- package/packages/skills/dist/loader.js +525 -36
- package/packages/skills/dist/loader.js.map +1 -1
- package/packages/skills/dist/remote-source.d.ts +107 -0
- package/packages/skills/dist/remote-source.d.ts.map +1 -0
- package/packages/skills/dist/remote-source.js +79 -0
- package/packages/skills/dist/remote-source.js.map +1 -0
- package/packages/skills/dist/vector-schema.d.ts +58 -0
- package/packages/skills/dist/vector-schema.d.ts.map +1 -0
- package/packages/skills/dist/vector-schema.js +55 -0
- package/packages/skills/dist/vector-schema.js.map +1 -0
- package/packages/skills/package.json +1 -1
- package/packages/skills/src/index.ts +28 -5
- package/packages/skills/src/loader.ts +688 -53
- package/packages/skills/src/remote-source.ts +148 -0
- package/packages/skills/src/vector-schema.ts +72 -0
- package/packages/skills/test/init-skills.test.ts +107 -111
- package/packages/skills/test/remote-skills.test.ts +242 -0
- package/packages/skills/test/remote-source.test.ts +248 -0
- package/packages/skills/test/vector-indexing.test.ts +205 -0
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tooling-preference/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/two-phase-review/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/workspace-scanner/test/scanner.test.ts +4 -3
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/sync-to-qdrant.ts +79 -14
- package/packages/autonomous-runtime/test/autonomous-runtime.test.ts +0 -194
- package/packages/autonomous-runtime/test/inbox.test.ts +0 -80
|
@@ -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 ||
|
|
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:
|
|
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
|
-
});
|