@jamesaphoenix/tx-test-utils 0.5.9 → 0.6.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/README.md CHANGED
@@ -5,7 +5,14 @@
5
5
  Memory, tasks, and orchestration. You own the loop.
6
6
 
7
7
  ```bash
8
+ # Standalone binary (recommended — no runtime needed)
9
+ curl -fsSL https://raw.githubusercontent.com/jamesaphoenix/tx/main/install.sh | sh
10
+
11
+ # Or via npm (requires bun)
8
12
  npm install -g @jamesaphoenix/tx-cli
13
+ ```
14
+
15
+ ```bash
9
16
  tx init
10
17
  ```
11
18
 
@@ -41,7 +48,7 @@ Composable primitives that handle the hard parts. You keep control of the orches
41
48
  ├─────────────────────────────────────────────────────────┤
42
49
  │ tx primitives │
43
50
  │ │
44
- │ tx ready tx done tx context tx learn
51
+ │ tx ready tx done tx context tx memory
45
52
  │ tx claim tx block tx sync tx trace │
46
53
  │ │
47
54
  └─────────────────────────────────────────────────────────┘
@@ -53,12 +60,36 @@ Composable primitives that handle the hard parts. You keep control of the orches
53
60
 
54
61
  ### Memory
55
62
 
56
- Learnings that persist and surface when relevant.
63
+ Filesystem-backed knowledge that persists, links, and surfaces when relevant.
64
+
65
+ ```bash
66
+ # Register a directory of markdown files
67
+ tx memory source add ./docs
68
+
69
+ # Index and search your knowledge
70
+ tx memory index
71
+ tx memory search "authentication patterns"
72
+ tx memory search "auth" --semantic --expand # BM25 + vector + graph
73
+
74
+ # Create, tag, and link documents
75
+ tx memory add "JWT Guide" --tags auth,security
76
+ tx memory tag mem-a7f3bc12 reviewed
77
+ tx memory link mem-a7f3bc12 mem-b8e4cd56
78
+
79
+ # Navigate the knowledge graph
80
+ tx memory links mem-a7f3bc12 # Outgoing wikilinks + edges
81
+ tx memory backlinks mem-a7f3bc12 # What links to this?
82
+ ```
83
+
84
+ Three search modes: BM25 keyword search, vector similarity (`--semantic`), and graph expansion (`--expand`) via wikilinks and `frontmatter.related`. Combined with RRF fusion for best results.
85
+
86
+ ### Learnings
87
+
88
+ Structured insights that attach to tasks and file paths.
57
89
 
58
90
  ```bash
59
91
  # Store knowledge
60
92
  tx learning:add "Use bcrypt for passwords, not SHA256"
61
- tx learning:add "Redis cache invalidation has race conditions" -c database
62
93
 
63
94
  # Attach learnings to file paths
64
95
  tx learn "src/auth/*.ts" "Services must use Effect-TS patterns"
@@ -210,7 +241,7 @@ Detailed rollout, detached service setup, rollback, and troubleshooting:
210
241
  |---|---|---|---|
211
242
  | **Persistence** | Session-scoped | File grows forever | Git-native, branch-aware |
212
243
  | **Multi-agent** | Collisions | Manual coordination | Claim with lease expiry |
213
- | **Knowledge** | Lost each session | Static dump | Hybrid search, contextual retrieval |
244
+ | **Knowledge** | Lost each session | Static dump | Filesystem memory, hybrid search, graph links |
214
245
  | **Orchestration** | None | None | Primitives for any pattern |
215
246
 
216
247
  ---
@@ -263,6 +294,25 @@ tx unblock <id> <blocker> # Remove dependency
263
294
  tx children <id> # List child tasks
264
295
  tx tree <id> # Show hierarchy
265
296
 
297
+ # Memory (filesystem-backed .md search)
298
+ tx memory source add <dir> # Register directory
299
+ tx memory source rm <dir> # Unregister directory
300
+ tx memory source list # Show registered directories
301
+ tx memory add <title> # Create .md file (--content, --tags, --dir)
302
+ tx memory index # Index all sources (--incremental, --status)
303
+ tx memory search <query> # BM25 search (--semantic, --expand, --tags, --prop)
304
+ tx memory show <id> # Display document
305
+ tx memory tag <id> <tags> # Add tags to frontmatter
306
+ tx memory untag <id> <t> # Remove tags
307
+ tx memory relate <id> <t> # Add to frontmatter.related
308
+ tx memory set <id> <k> <v> # Set property
309
+ tx memory unset <id> <k> # Remove property
310
+ tx memory props <id> # Show properties
311
+ tx memory links <id> # Outgoing wikilinks + edges
312
+ tx memory backlinks <id> # Incoming links
313
+ tx memory list # List documents (--source, --tags)
314
+ tx memory link <src> <tgt> # Create explicit edge
315
+
266
316
  # Context & Learnings
267
317
  tx learning:add <content> # Store knowledge
268
318
  tx learning:search <query> # Search learnings
@@ -56,7 +56,7 @@ export interface SharedTestLayer<L> {
56
56
  * ```
57
57
  */
58
58
  export declare const createSharedTestLayer: () => Promise<{
59
- layer: Layer.Layer<import("@jamesaphoenix/tx-core").MigrationService | import("@jamesaphoenix/tx-core").SqliteClient | import("@jamesaphoenix/tx-core").TaskRepository | import("@jamesaphoenix/tx-core").DependencyRepository | import("@jamesaphoenix/tx-core").LearningRepository | import("@jamesaphoenix/tx-core").FileLearningRepository | import("@jamesaphoenix/tx-core").AttemptRepository | import("@jamesaphoenix/tx-core").RunRepository | import("@jamesaphoenix/tx-core").AnchorRepository | import("@jamesaphoenix/tx-core").EdgeRepository | import("@jamesaphoenix/tx-core").DeduplicationRepository | import("@jamesaphoenix/tx-core").CandidateRepository | import("@jamesaphoenix/tx-core").TrackedProjectRepository | import("@jamesaphoenix/tx-core").WorkerRepository | import("@jamesaphoenix/tx-core").ClaimRepository | import("@jamesaphoenix/tx-core").OrchestratorStateRepository | import("@jamesaphoenix/tx-core").TaskService | import("@jamesaphoenix/tx-core").DependencyService | import("@jamesaphoenix/tx-core").ReadyService | import("@jamesaphoenix/tx-core").HierarchyService | import("@jamesaphoenix/tx-core").EdgeService | import("@jamesaphoenix/tx-core").GraphExpansionService | import("@jamesaphoenix/tx-core").FeedbackTrackerService | import("@jamesaphoenix/tx-core").DiversifierService | import("@jamesaphoenix/tx-core").RetrieverService | import("@jamesaphoenix/tx-core").LearningService | import("@jamesaphoenix/tx-core").FileLearningService | import("@jamesaphoenix/tx-core").AttemptService | import("@jamesaphoenix/tx-core").AnchorVerificationService | import("@jamesaphoenix/tx-core").AnchorService | import("@jamesaphoenix/tx-core").DeduplicationService | import("@jamesaphoenix/tx-core").SyncService | import("@jamesaphoenix/tx-core").SwarmVerificationService | import("@jamesaphoenix/tx-core").PromotionService | import("@jamesaphoenix/tx-core").WorkerService | import("@jamesaphoenix/tx-core").RunHeartbeatService | import("@jamesaphoenix/tx-core").ClaimService | import("@jamesaphoenix/tx-core").OrchestratorService | import("@jamesaphoenix/tx-core").DaemonService | import("@jamesaphoenix/tx-core").TracingService | import("@jamesaphoenix/tx-core").CompactionRepository | import("@jamesaphoenix/tx-core").CompactionService | import("@jamesaphoenix/tx-core").ValidationService | import("@jamesaphoenix/tx-core").MessageRepository | import("@jamesaphoenix/tx-core").MessageService | import("@jamesaphoenix/tx-core").DocRepository | import("@jamesaphoenix/tx-core").DocService, import("@jamesaphoenix/tx-core").DatabaseError, never>;
59
+ layer: Layer.Layer<import("@jamesaphoenix/tx-core").MigrationService | import("@jamesaphoenix/tx-core").SqliteClient | import("@jamesaphoenix/tx-core").TaskRepository | import("@jamesaphoenix/tx-core").DependencyRepository | import("@jamesaphoenix/tx-core").LearningRepository | import("@jamesaphoenix/tx-core").FileLearningRepository | import("@jamesaphoenix/tx-core").AttemptRepository | import("@jamesaphoenix/tx-core").RunRepository | import("@jamesaphoenix/tx-core").AnchorRepository | import("@jamesaphoenix/tx-core").EdgeRepository | import("@jamesaphoenix/tx-core").DeduplicationRepository | import("@jamesaphoenix/tx-core").CandidateRepository | import("@jamesaphoenix/tx-core").TrackedProjectRepository | import("@jamesaphoenix/tx-core").WorkerRepository | import("@jamesaphoenix/tx-core").ClaimRepository | import("@jamesaphoenix/tx-core").OrchestratorStateRepository | import("@jamesaphoenix/tx-core").TaskService | import("@jamesaphoenix/tx-core").DependencyService | import("@jamesaphoenix/tx-core").ReadyService | import("@jamesaphoenix/tx-core").HierarchyService | import("@jamesaphoenix/tx-core").EdgeService | import("@jamesaphoenix/tx-core").GraphExpansionService | import("@jamesaphoenix/tx-core").FeedbackTrackerService | import("@jamesaphoenix/tx-core").DiversifierService | import("@jamesaphoenix/tx-core").RetrieverService | import("@jamesaphoenix/tx-core").LearningService | import("@jamesaphoenix/tx-core").FileLearningService | import("@jamesaphoenix/tx-core").AttemptService | import("@jamesaphoenix/tx-core").AnchorVerificationService | import("@jamesaphoenix/tx-core").AnchorService | import("@jamesaphoenix/tx-core").DeduplicationService | import("@jamesaphoenix/tx-core").PinRepository | import("@jamesaphoenix/tx-core").DocRepository | import("@jamesaphoenix/tx-core").SyncService | import("@jamesaphoenix/tx-core").SwarmVerificationService | import("@jamesaphoenix/tx-core").PromotionService | import("@jamesaphoenix/tx-core").WorkerService | import("@jamesaphoenix/tx-core").RunHeartbeatService | import("@jamesaphoenix/tx-core").ClaimService | import("@jamesaphoenix/tx-core").OrchestratorService | import("@jamesaphoenix/tx-core").DaemonService | import("@jamesaphoenix/tx-core").TracingService | import("@jamesaphoenix/tx-core").CompactionRepository | import("@jamesaphoenix/tx-core").CompactionService | import("@jamesaphoenix/tx-core").ValidationService | import("@jamesaphoenix/tx-core").MessageRepository | import("@jamesaphoenix/tx-core").MessageService | import("@jamesaphoenix/tx-core").DocService | import("@jamesaphoenix/tx-core").MemoryDocumentRepository | import("@jamesaphoenix/tx-core").MemoryLinkRepository | import("@jamesaphoenix/tx-core").MemoryPropertyRepository | import("@jamesaphoenix/tx-core").MemorySourceRepository | import("@jamesaphoenix/tx-core").MemoryService | import("@jamesaphoenix/tx-core").MemoryRetrieverService | import("@jamesaphoenix/tx-core").PinService, import("@jamesaphoenix/tx-core").DatabaseError, never>;
60
60
  reset: () => Promise<void>;
61
61
  close: () => Promise<void>;
62
62
  getDb: () => Database;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jamesaphoenix/tx-test-utils",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "description": "Test utilities, factories, fixtures, and helpers for tx monorepo",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -45,6 +45,7 @@
45
45
  },
46
46
  "files": [
47
47
  "dist",
48
+ "!dist/**/*.test.*",
48
49
  "README.md"
49
50
  ],
50
51
  "scripts": {
@@ -1,9 +0,0 @@
1
- /**
2
- * Chaos Engineering Utilities Integration Tests
3
- *
4
- * Tests all chaos utilities using real in-memory SQLite per Rule 3.
5
- *
6
- * @module @tx/test-utils/chaos/chaos.test
7
- */
8
- export {};
9
- //# sourceMappingURL=chaos.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"chaos.test.d.ts","sourceRoot":"","sources":["../../src/chaos/chaos.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
@@ -1,498 +0,0 @@
1
- /**
2
- * Chaos Engineering Utilities Integration Tests
3
- *
4
- * Tests all chaos utilities using real in-memory SQLite per Rule 3.
5
- *
6
- * @module @tx/test-utils/chaos/chaos.test
7
- */
8
- import { describe, it, expect, beforeEach } from "vitest";
9
- import { Effect } from "effect";
10
- import { createTestDatabase } from "../database/index.js";
11
- import { fixtureId } from "../fixtures/index.js";
12
- import { crashAfter, CrashSimulationError, killHeartbeat, WorkerHeartbeatController, raceWorkers, corruptState, replayJSONL, doubleComplete, partialWrite, delayedClaim, stressLoad } from "./chaos-utilities.js";
13
- describe("Chaos Engineering Utilities Integration", () => {
14
- let db;
15
- beforeEach(async () => {
16
- db = await Effect.runPromise(createTestDatabase());
17
- });
18
- // ===========================================================================
19
- // crashAfter tests
20
- // ===========================================================================
21
- describe("crashAfter", () => {
22
- it("returns completed=true when operation finishes before timeout", async () => {
23
- const result = await crashAfter({ ms: 100 }, async () => {
24
- return "success";
25
- });
26
- expect(result.completed).toBe(true);
27
- expect(result.value).toBe("success");
28
- expect(result.elapsedMs).toBeLessThan(100);
29
- });
30
- it("returns completed=false when timeout occurs first", async () => {
31
- const result = await crashAfter({ ms: 50 }, async () => {
32
- await sleep(200);
33
- return "should not get here";
34
- });
35
- expect(result.completed).toBe(false);
36
- expect(result.value).toBeUndefined();
37
- expect(result.elapsedMs).toBeGreaterThanOrEqual(50);
38
- expect(result.elapsedMs).toBeLessThan(200);
39
- });
40
- it("calls beforeCrash callback when crash occurs", async () => {
41
- let callbackCalled = false;
42
- await crashAfter({
43
- ms: 50,
44
- beforeCrash: () => {
45
- callbackCalled = true;
46
- }
47
- }, async () => {
48
- await sleep(200);
49
- });
50
- expect(callbackCalled).toBe(true);
51
- });
52
- it("throws CrashSimulationError when throwOnCrash is true", async () => {
53
- await expect(crashAfter({ ms: 50, throwOnCrash: true }, async () => {
54
- await sleep(200);
55
- })).rejects.toBeInstanceOf(CrashSimulationError);
56
- });
57
- it("captures operation errors", async () => {
58
- const result = await crashAfter({ ms: 1000 }, async () => {
59
- throw new Error("operation failed");
60
- });
61
- expect(result.completed).toBe(false);
62
- expect(result.error).toBeDefined();
63
- expect(result.error?.message).toBe("operation failed");
64
- });
65
- });
66
- // ===========================================================================
67
- // killHeartbeat tests
68
- // ===========================================================================
69
- describe("killHeartbeat / WorkerHeartbeatController", () => {
70
- const workerId = fixtureId("heartbeat-worker");
71
- beforeEach(() => {
72
- // Create a worker for testing
73
- const now = new Date();
74
- db.run(`INSERT INTO workers (id, name, hostname, pid, status, registered_at, last_heartbeat_at, capabilities, metadata)
75
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [workerId, "Test Worker", "localhost", process.pid, "idle", now.toISOString(), now.toISOString(), "[]", "{}"]);
76
- });
77
- it("creates a controller with killHeartbeat", () => {
78
- const controller = killHeartbeat({ workerId, db });
79
- expect(controller).toBeInstanceOf(WorkerHeartbeatController);
80
- expect(controller.isKilled()).toBe(false);
81
- });
82
- it("kill sets heartbeat to past time", () => {
83
- const controller = killHeartbeat({ workerId, db });
84
- controller.kill(60);
85
- expect(controller.isKilled()).toBe(true);
86
- const worker = db.query("SELECT last_heartbeat_at FROM workers WHERE id = ?", [workerId])[0];
87
- const heartbeatTime = new Date(worker.last_heartbeat_at);
88
- const hourAgo = new Date(Date.now() - 60 * 60 * 1000);
89
- expect(heartbeatTime.getTime()).toBeLessThanOrEqual(hourAgo.getTime() + 60000);
90
- });
91
- it("restore restores original heartbeat", () => {
92
- const originalWorker = db.query("SELECT last_heartbeat_at FROM workers WHERE id = ?", [workerId])[0];
93
- const controller = killHeartbeat({ workerId, db });
94
- controller.kill();
95
- controller.restore();
96
- const restoredWorker = db.query("SELECT last_heartbeat_at FROM workers WHERE id = ?", [workerId])[0];
97
- expect(restoredWorker.last_heartbeat_at).toBe(originalWorker.last_heartbeat_at);
98
- expect(controller.isKilled()).toBe(false);
99
- });
100
- it("revive sets heartbeat to current time", () => {
101
- const controller = killHeartbeat({ workerId, db });
102
- controller.kill();
103
- const beforeRevive = Date.now();
104
- controller.revive();
105
- const afterRevive = Date.now();
106
- const worker = db.query("SELECT last_heartbeat_at FROM workers WHERE id = ?", [workerId])[0];
107
- const heartbeatTime = new Date(worker.last_heartbeat_at).getTime();
108
- expect(heartbeatTime).toBeGreaterThanOrEqual(beforeRevive - 1000);
109
- expect(heartbeatTime).toBeLessThanOrEqual(afterRevive + 1000);
110
- });
111
- });
112
- // ===========================================================================
113
- // raceWorkers tests
114
- // ===========================================================================
115
- describe("raceWorkers", () => {
116
- const taskId = fixtureId("race-task");
117
- beforeEach(() => {
118
- // Create a task for workers to claim
119
- db.run(`INSERT INTO tasks (id, title, description, status, score, created_at, updated_at, metadata)
120
- VALUES (?, ?, '', 'backlog', 500, datetime('now'), datetime('now'), '{}')`, [taskId, "Task to Race For"]);
121
- });
122
- it("only one worker wins the race", async () => {
123
- const result = await raceWorkers({
124
- count: 5,
125
- taskId,
126
- db
127
- });
128
- expect(result.successfulClaims).toBe(1);
129
- expect(result.winner).not.toBeNull();
130
- expect(result.workers.length).toBe(5);
131
- expect(result.losers.length).toBe(4);
132
- });
133
- it("registers all workers", async () => {
134
- const result = await raceWorkers({
135
- count: 3,
136
- taskId,
137
- db
138
- });
139
- const workers = db.query("SELECT id FROM workers");
140
- expect(workers.length).toBe(3);
141
- expect(workers.map(w => w.id)).toEqual(expect.arrayContaining(result.workers));
142
- });
143
- it("winner has active claim", async () => {
144
- const result = await raceWorkers({
145
- count: 3,
146
- taskId,
147
- db
148
- });
149
- const claim = db.query("SELECT worker_id, status FROM task_claims WHERE task_id = ? AND status = 'active'", [taskId])[0];
150
- expect(claim).toBeDefined();
151
- expect(claim.worker_id).toBe(result.winner);
152
- expect(claim.status).toBe("active");
153
- });
154
- it("respects delay between workers", async () => {
155
- const startTime = Date.now();
156
- await raceWorkers({
157
- count: 3,
158
- taskId,
159
- db,
160
- delayBetweenMs: 20
161
- });
162
- const elapsedMs = Date.now() - startTime;
163
- expect(elapsedMs).toBeGreaterThanOrEqual(30); // ~(n-1)*delay, with CI timer jitter tolerance
164
- });
165
- });
166
- // ===========================================================================
167
- // corruptState tests
168
- // ===========================================================================
169
- describe("corruptState", () => {
170
- const taskId = fixtureId("corrupt-task");
171
- beforeEach(() => {
172
- db.run(`INSERT INTO tasks (id, title, description, status, score, created_at, updated_at, metadata)
173
- VALUES (?, ?, '', 'backlog', 500, datetime('now'), datetime('now'), '{}')`, [taskId, "Task to Corrupt"]);
174
- });
175
- it("injects invalid_status corruption", () => {
176
- const result = corruptState({
177
- table: "tasks",
178
- type: "invalid_status",
179
- db,
180
- rowId: taskId
181
- });
182
- expect(result.corrupted).toBe(true);
183
- const task = db.query("SELECT status FROM tasks WHERE id = ?", [taskId])[0];
184
- expect(task.status).toBe("INVALID_STATUS");
185
- });
186
- it("injects invalid_json corruption", () => {
187
- const result = corruptState({
188
- table: "tasks",
189
- type: "invalid_json",
190
- db,
191
- rowId: taskId
192
- });
193
- expect(result.corrupted).toBe(true);
194
- const task = db.query("SELECT metadata FROM tasks WHERE id = ?", [taskId])[0];
195
- expect(() => JSON.parse(task.metadata)).toThrow();
196
- });
197
- it("injects negative_score corruption", () => {
198
- const result = corruptState({
199
- table: "tasks",
200
- type: "negative_score",
201
- db,
202
- rowId: taskId
203
- });
204
- expect(result.corrupted).toBe(true);
205
- const task = db.query("SELECT score FROM tasks WHERE id = ?", [taskId])[0];
206
- expect(task.score).toBe(-1000);
207
- });
208
- it("injects future_timestamp corruption", () => {
209
- const result = corruptState({
210
- table: "tasks",
211
- type: "future_timestamp",
212
- db,
213
- rowId: taskId
214
- });
215
- expect(result.corrupted).toBe(true);
216
- const task = db.query("SELECT created_at FROM tasks WHERE id = ?", [taskId])[0];
217
- const createdAt = new Date(task.created_at);
218
- expect(createdAt.getTime()).toBeGreaterThan(Date.now());
219
- });
220
- it("injects self_reference corruption", () => {
221
- const result = corruptState({
222
- table: "tasks",
223
- type: "self_reference",
224
- db,
225
- rowId: taskId
226
- });
227
- expect(result.corrupted).toBe(true);
228
- const task = db.query("SELECT parent_id FROM tasks WHERE id = ?", [taskId])[0];
229
- expect(task.parent_id).toBe(taskId);
230
- });
231
- it("creates new row when rowId not provided", () => {
232
- const result = corruptState({
233
- table: "tasks",
234
- type: "invalid_status",
235
- db
236
- });
237
- expect(result.corrupted).toBe(true);
238
- expect(result.rowId).toMatch(/^tx-/);
239
- const task = db.query("SELECT status FROM tasks WHERE id = ?", [result.rowId])[0];
240
- expect(task.status).toBe("INVALID_STATUS");
241
- });
242
- });
243
- // ===========================================================================
244
- // replayJSONL tests
245
- // ===========================================================================
246
- describe("replayJSONL", () => {
247
- it("replays task upsert operations", () => {
248
- const jsonl = `
249
- {"v":1,"op":"upsert","ts":"2024-01-01T00:00:00Z","id":"tx-replay1","data":{"title":"Task 1","status":"backlog","score":500,"description":"","parentId":null,"metadata":{}}}
250
- {"v":1,"op":"upsert","ts":"2024-01-02T00:00:00Z","id":"tx-replay2","data":{"title":"Task 2","status":"active","score":600,"description":"","parentId":null,"metadata":{}}}
251
- `;
252
- const result = replayJSONL({ db, content: jsonl });
253
- expect(result.opsReplayed).toBe(2);
254
- expect(result.tasksCreated).toBe(2);
255
- const tasks = db.query("SELECT id, title FROM tasks ORDER BY id");
256
- expect(tasks.length).toBe(2);
257
- });
258
- it("updates existing tasks on replay", () => {
259
- // Create initial task
260
- db.run(`INSERT INTO tasks (id, title, description, status, score, created_at, updated_at, metadata)
261
- VALUES ('tx-existing', 'Original Title', '', 'backlog', 100, '2024-01-01T00:00:00Z', '2024-01-01T00:00:00Z', '{}')`, []);
262
- const jsonl = `{"v":1,"op":"upsert","ts":"2024-01-02T00:00:00Z","id":"tx-existing","data":{"title":"Updated Title","status":"active","score":200,"description":"","parentId":null,"metadata":{}}}`;
263
- const result = replayJSONL({ db, content: jsonl });
264
- expect(result.tasksUpdated).toBe(1);
265
- const task = db.query("SELECT title, score FROM tasks WHERE id = 'tx-existing'")[0];
266
- expect(task.title).toBe("Updated Title");
267
- expect(task.score).toBe(200);
268
- });
269
- it("handles dependency operations", () => {
270
- // Create tasks first
271
- db.run(`INSERT INTO tasks (id, title, description, status, score, created_at, updated_at, metadata)
272
- VALUES ('tx-blocker', 'Blocker', '', 'active', 500, datetime('now'), datetime('now'), '{}')`, []);
273
- db.run(`INSERT INTO tasks (id, title, description, status, score, created_at, updated_at, metadata)
274
- VALUES ('tx-blocked', 'Blocked', '', 'backlog', 400, datetime('now'), datetime('now'), '{}')`, []);
275
- const jsonl = `{"v":1,"op":"dep_add","ts":"2024-01-01T00:00:00Z","blockerId":"tx-blocker","blockedId":"tx-blocked"}`;
276
- const result = replayJSONL({ db, content: jsonl });
277
- expect(result.depsAdded).toBe(1);
278
- const dep = db.query("SELECT blocker_id, blocked_id FROM task_dependencies WHERE blocker_id = 'tx-blocker'")[0];
279
- expect(dep.blocked_id).toBe("tx-blocked");
280
- });
281
- it("clears data when clearFirst is true", () => {
282
- // Create existing task
283
- db.run(`INSERT INTO tasks (id, title, description, status, score, created_at, updated_at, metadata)
284
- VALUES ('tx-oldtask', 'Old Task', '', 'backlog', 100, datetime('now'), datetime('now'), '{}')`, []);
285
- const jsonl = `{"v":1,"op":"upsert","ts":"2024-01-01T00:00:00Z","id":"tx-newtask","data":{"title":"New Task","status":"backlog","score":500,"description":"","parentId":null,"metadata":{}}}`;
286
- replayJSONL({ db, content: jsonl, clearFirst: true });
287
- const tasks = db.query("SELECT id FROM tasks");
288
- expect(tasks.length).toBe(1);
289
- expect(tasks[0].id).toBe("tx-newtask");
290
- });
291
- it("handles invalid JSON lines gracefully", () => {
292
- const jsonl = `
293
- {"v":1,"op":"upsert","ts":"2024-01-01T00:00:00Z","id":"tx-valid1","data":{"title":"Valid Task","status":"backlog","score":500,"description":"","parentId":null,"metadata":{}}}
294
- not valid json
295
- {"v":1,"op":"upsert","ts":"2024-01-02T00:00:00Z","id":"tx-valid2","data":{"title":"Valid Task 2","status":"backlog","score":600,"description":"","parentId":null,"metadata":{}}}
296
- `;
297
- const result = replayJSONL({ db, content: jsonl });
298
- expect(result.errors.length).toBe(1);
299
- expect(result.tasksCreated).toBe(2);
300
- });
301
- });
302
- // ===========================================================================
303
- // doubleComplete tests
304
- // ===========================================================================
305
- describe("doubleComplete", () => {
306
- const taskId = fixtureId("double-complete-task");
307
- beforeEach(() => {
308
- db.run(`INSERT INTO tasks (id, title, description, status, score, created_at, updated_at, metadata)
309
- VALUES (?, ?, '', 'active', 500, datetime('now'), datetime('now'), '{}')`, [taskId, "Task to Complete Twice"]);
310
- });
311
- it("first completion succeeds", () => {
312
- const result = doubleComplete({ taskId, db });
313
- expect(result.firstCompleted).toBe(true);
314
- expect(result.originalStatus).toBe("active");
315
- expect(result.finalStatus).toBe("done");
316
- });
317
- it("tracks original status", () => {
318
- const result = doubleComplete({ taskId, db });
319
- expect(result.originalStatus).toBe("active");
320
- });
321
- it("returns task not found error for missing task", () => {
322
- const result = doubleComplete({ taskId: "tx-nonexistent", db });
323
- expect(result.firstCompleted).toBe(false);
324
- expect(result.secondError).toBe("Task not found");
325
- });
326
- it("handles already-done tasks", () => {
327
- // Complete the task first
328
- db.run("UPDATE tasks SET status = 'done', completed_at = datetime('now') WHERE id = ?", [taskId]);
329
- const result = doubleComplete({ taskId, db });
330
- expect(result.firstCompleted).toBe(true); // Already done counts as completed
331
- expect(result.originalStatus).toBe("done");
332
- expect(result.finalStatus).toBe("done");
333
- });
334
- });
335
- // ===========================================================================
336
- // partialWrite tests
337
- // ===========================================================================
338
- describe("partialWrite", () => {
339
- it("writes rows up to failure point without transaction", () => {
340
- const result = partialWrite({
341
- table: "tasks",
342
- db,
343
- rowCount: 10,
344
- failAtRow: 5,
345
- useTransaction: false
346
- });
347
- expect(result.rowsWritten).toBe(4); // Rows 1-4 succeed
348
- expect(result.rowsFailed).toBe(6); // Rows 5-10 fail
349
- expect(result.rolledBack).toBe(false);
350
- expect(result.error).toContain("Simulated failure at row 5");
351
- const tasks = db.query("SELECT id FROM tasks WHERE title LIKE 'Partial Write%'");
352
- expect(tasks.length).toBe(4);
353
- });
354
- it("rolls back all rows with transaction", () => {
355
- const result = partialWrite({
356
- table: "tasks",
357
- db,
358
- rowCount: 10,
359
- failAtRow: 5,
360
- useTransaction: true
361
- });
362
- expect(result.rowsWritten).toBe(0);
363
- expect(result.rolledBack).toBe(true);
364
- expect(result.writtenIds.length).toBe(0);
365
- expect(result.error).toContain("Simulated failure at row 5");
366
- const tasks = db.query("SELECT id FROM tasks WHERE title LIKE 'Partial Write%'");
367
- expect(tasks.length).toBe(0);
368
- });
369
- it("succeeds when failAtRow > rowCount", () => {
370
- const result = partialWrite({
371
- table: "tasks",
372
- db,
373
- rowCount: 5,
374
- failAtRow: 10,
375
- useTransaction: false
376
- });
377
- expect(result.rowsWritten).toBe(5);
378
- expect(result.rowsFailed).toBe(0);
379
- expect(result.error).toBeUndefined();
380
- });
381
- });
382
- // ===========================================================================
383
- // delayedClaim tests
384
- // ===========================================================================
385
- describe("delayedClaim", () => {
386
- const taskId = fixtureId("delayed-claim-task");
387
- const slowWorker = fixtureId("slow-worker");
388
- beforeEach(() => {
389
- // Create task
390
- db.run(`INSERT INTO tasks (id, title, description, status, score, created_at, updated_at, metadata)
391
- VALUES (?, ?, '', 'backlog', 500, datetime('now'), datetime('now'), '{}')`, [taskId, "Task to Claim"]);
392
- // Create worker
393
- db.run(`INSERT INTO workers (id, name, hostname, pid, status, registered_at, last_heartbeat_at, capabilities, metadata)
394
- VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'), '[]', '{}')`, [slowWorker, "Slow Worker", "localhost", process.pid, "idle"]);
395
- });
396
- it("successfully claims when no competition", async () => {
397
- const result = await delayedClaim({
398
- taskId,
399
- workerId: slowWorker,
400
- db,
401
- delayMs: 50
402
- });
403
- expect(result.claimed).toBe(true);
404
- expect(result.claimedBy).toBe(slowWorker);
405
- expect(result.raceDetected).toBe(false);
406
- expect(result.waitedMs).toBeGreaterThanOrEqual(50);
407
- });
408
- it("detects race when another worker claims during delay", async () => {
409
- const fastWorker = fixtureId("fast-worker");
410
- // Register fast worker first (required for foreign key constraint)
411
- db.run(`INSERT INTO workers (id, name, hostname, pid, status, registered_at, last_heartbeat_at, capabilities, metadata)
412
- VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'), '[]', '{}')`, [fastWorker, "Fast Worker", "localhost", process.pid, "idle"]);
413
- // Start delayed claim
414
- const delayedPromise = delayedClaim({
415
- taskId,
416
- workerId: slowWorker,
417
- db,
418
- delayMs: 100,
419
- checkRace: true
420
- });
421
- // Wait a bit then claim immediately with fast worker
422
- await sleep(20);
423
- db.run(`INSERT INTO task_claims (task_id, worker_id, claimed_at, lease_expires_at, renewed_count, status)
424
- VALUES (?, ?, datetime('now'), datetime('now', '+30 minutes'), 0, 'active')`, [taskId, fastWorker]);
425
- const result = await delayedPromise;
426
- expect(result.raceDetected).toBe(true);
427
- expect(result.claimed).toBe(false);
428
- expect(result.claimedBy).toBe(fastWorker);
429
- });
430
- it("returns correct wait time", async () => {
431
- const result = await delayedClaim({
432
- taskId,
433
- workerId: slowWorker,
434
- db,
435
- delayMs: 100
436
- });
437
- expect(result.waitedMs).toBeGreaterThanOrEqual(100);
438
- expect(result.waitedMs).toBeLessThan(200);
439
- });
440
- });
441
- // ===========================================================================
442
- // stressLoad tests
443
- // ===========================================================================
444
- describe("stressLoad", () => {
445
- it("creates specified number of tasks", () => {
446
- const result = stressLoad({
447
- taskCount: 100,
448
- db
449
- });
450
- expect(result.tasksCreated).toBe(100);
451
- expect(result.taskIds.length).toBe(100);
452
- const count = db.query("SELECT COUNT(*) as count FROM tasks")[0];
453
- expect(count.count).toBe(100);
454
- });
455
- it("creates tasks with mixed statuses when enabled", () => {
456
- const result = stressLoad({
457
- taskCount: 70,
458
- db,
459
- mixedStatuses: true
460
- });
461
- expect(result.tasksCreated).toBe(70);
462
- const statuses = db.query("SELECT status, COUNT(*) as count FROM tasks GROUP BY status");
463
- // Should have multiple different statuses
464
- expect(statuses.length).toBeGreaterThan(1);
465
- });
466
- it("creates dependencies when requested", () => {
467
- const result = stressLoad({
468
- taskCount: 50,
469
- db,
470
- withDependencies: true,
471
- dependencyRatio: 0.3
472
- });
473
- expect(result.tasksCreated).toBe(50);
474
- expect(result.depsCreated).toBeGreaterThan(0);
475
- const depCount = db.query("SELECT COUNT(*) as count FROM task_dependencies")[0];
476
- expect(depCount.count).toBe(result.depsCreated);
477
- });
478
- it("reports performance metrics", () => {
479
- const result = stressLoad({
480
- taskCount: 100,
481
- db
482
- });
483
- expect(result.elapsedMs).toBeGreaterThan(0);
484
- expect(result.tasksPerSecond).toBeGreaterThan(0);
485
- });
486
- it("handles batch size correctly", () => {
487
- const result = stressLoad({
488
- taskCount: 250,
489
- db,
490
- batchSize: 100
491
- });
492
- expect(result.tasksCreated).toBe(250);
493
- });
494
- });
495
- });
496
- // Helper function
497
- const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
498
- //# sourceMappingURL=chaos.test.js.map