@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 +54 -4
- package/dist/helpers/shared-test-layer.d.ts +1 -1
- package/package.json +2 -1
- package/dist/chaos/chaos.test.d.ts +0 -9
- package/dist/chaos/chaos.test.d.ts.map +0 -1
- package/dist/chaos/chaos.test.js +0 -498
- package/dist/chaos/chaos.test.js.map +0 -1
- package/dist/factories/factories.test.d.ts +0 -8
- package/dist/factories/factories.test.d.ts.map +0 -1
- package/dist/factories/factories.test.js +0 -419
- package/dist/factories/factories.test.js.map +0 -1
- package/dist/helpers/effect.test.d.ts +0 -7
- package/dist/helpers/effect.test.d.ts.map +0 -1
- package/dist/helpers/effect.test.js +0 -271
- package/dist/helpers/effect.test.js.map +0 -1
- package/dist/llm-cache/cache.test.d.ts +0 -7
- package/dist/llm-cache/cache.test.d.ts.map +0 -1
- package/dist/llm-cache/cache.test.js +0 -310
- package/dist/llm-cache/cache.test.js.map +0 -1
- package/dist/mocks/mocks.test.d.ts +0 -10
- package/dist/mocks/mocks.test.d.ts.map +0 -1
- package/dist/mocks/mocks.test.js +0 -961
- package/dist/mocks/mocks.test.js.map +0 -1
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
|
|
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
|
-
|
|
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 |
|
|
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").
|
|
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.
|
|
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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"chaos.test.d.ts","sourceRoot":"","sources":["../../src/chaos/chaos.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|
package/dist/chaos/chaos.test.js
DELETED
|
@@ -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
|