switchman-dev 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +27 -10
  2. package/package.json +5 -5
  3. package/src/cli/index.js +227 -28
  4. package/src/core/db.js +452 -68
  5. package/src/core/git.js +8 -1
  6. package/src/mcp/server.js +170 -22
  7. package/CLAUDE.md +0 -98
  8. package/examples/taskapi/.switchman/switchman.db +0 -0
  9. package/examples/taskapi/package-lock.json +0 -4736
  10. package/examples/taskapi/tests/api.test.js +0 -112
  11. package/examples/worktrees/agent-rate-limiting/package-lock.json +0 -4736
  12. package/examples/worktrees/agent-rate-limiting/package.json +0 -18
  13. package/examples/worktrees/agent-rate-limiting/src/db.js +0 -179
  14. package/examples/worktrees/agent-rate-limiting/src/middleware/auth.js +0 -96
  15. package/examples/worktrees/agent-rate-limiting/src/middleware/validate.js +0 -133
  16. package/examples/worktrees/agent-rate-limiting/src/routes/tasks.js +0 -65
  17. package/examples/worktrees/agent-rate-limiting/src/routes/users.js +0 -38
  18. package/examples/worktrees/agent-rate-limiting/src/server.js +0 -7
  19. package/examples/worktrees/agent-rate-limiting/tests/api.test.js +0 -112
  20. package/examples/worktrees/agent-tests/package-lock.json +0 -4736
  21. package/examples/worktrees/agent-tests/package.json +0 -18
  22. package/examples/worktrees/agent-tests/src/db.js +0 -179
  23. package/examples/worktrees/agent-tests/src/middleware/auth.js +0 -96
  24. package/examples/worktrees/agent-tests/src/middleware/validate.js +0 -133
  25. package/examples/worktrees/agent-tests/src/routes/tasks.js +0 -65
  26. package/examples/worktrees/agent-tests/src/routes/users.js +0 -38
  27. package/examples/worktrees/agent-tests/src/server.js +0 -7
  28. package/examples/worktrees/agent-tests/tests/api.test.js +0 -112
  29. package/examples/worktrees/agent-validation/package-lock.json +0 -4736
  30. package/examples/worktrees/agent-validation/package.json +0 -18
  31. package/examples/worktrees/agent-validation/src/db.js +0 -179
  32. package/examples/worktrees/agent-validation/src/middleware/auth.js +0 -96
  33. package/examples/worktrees/agent-validation/src/middleware/validate.js +0 -133
  34. package/examples/worktrees/agent-validation/src/routes/tasks.js +0 -65
  35. package/examples/worktrees/agent-validation/src/routes/users.js +0 -38
  36. package/examples/worktrees/agent-validation/src/server.js +0 -7
  37. package/examples/worktrees/agent-validation/tests/api.test.js +0 -112
  38. package/tests/test.js +0 -259
package/tests/test.js DELETED
@@ -1,259 +0,0 @@
1
- /**
2
- * switchman - Basic test suite
3
- * Tests core DB and git functions without needing a real git repo
4
- */
5
-
6
- import { execSync } from 'child_process';
7
- import { mkdirSync, rmSync, existsSync, realpathSync } from 'fs';
8
- import { join } from 'path';
9
- import { tmpdir } from 'os';
10
-
11
- import { findRepoRoot } from '../src/core/git.js';
12
-
13
- const TEST_DIR = join(tmpdir(), `switchman-test-${Date.now()}`);
14
-
15
- // Import modules
16
- import {
17
- initDb,
18
- createTask,
19
- assignTask,
20
- completeTask,
21
- listTasks,
22
- getNextPendingTask,
23
- registerWorktree,
24
- listWorktrees,
25
- claimFiles,
26
- releaseFileClaims,
27
- checkFileConflicts,
28
- getActiveFileClaims,
29
- } from '../src/core/db.js';
30
-
31
- let passed = 0;
32
- let failed = 0;
33
-
34
- function assert(condition, message) {
35
- if (condition) {
36
- console.log(` ✓ ${message}`);
37
- passed++;
38
- } else {
39
- console.log(` ✗ ${message}`);
40
- failed++;
41
- }
42
- }
43
-
44
- function test(name, fn) {
45
- console.log(`\n${name}`);
46
- try {
47
- fn();
48
- } catch (err) {
49
- console.log(` ✗ THREW: ${err.message}`);
50
- failed++;
51
- }
52
- }
53
-
54
- // Setup
55
- mkdirSync(TEST_DIR, { recursive: true });
56
-
57
- // Initialize a fake git repo for testing
58
- execSync('git init', { cwd: TEST_DIR });
59
- execSync('git config user.email "test@test.com"', { cwd: TEST_DIR });
60
- execSync('git config user.name "Test"', { cwd: TEST_DIR });
61
-
62
- // ─── Tests ────────────────────────────────────────────────────────────────────
63
-
64
- test('DB initialization', () => {
65
- const db = initDb(TEST_DIR);
66
- assert(db !== null, 'Database created successfully');
67
- db.close();
68
- assert(existsSync(join(TEST_DIR, '.switchman', 'switchman.db')), 'Database file exists on disk');
69
- });
70
-
71
- let db;
72
- test('Task creation', () => {
73
- db = initDb(TEST_DIR);
74
- const id1 = createTask(db, { title: 'Fix authentication bug', priority: 8 });
75
- const id2 = createTask(db, { title: 'Add rate limiting', priority: 6, description: 'Use Redis' });
76
- const id3 = createTask(db, { title: 'Update docs', priority: 3 });
77
-
78
- assert(id1.startsWith('task-'), 'Task ID auto-generated');
79
- const tasks = listTasks(db);
80
- assert(tasks.length === 3, 'Three tasks created');
81
- assert(tasks[0].title === 'Fix authentication bug', 'Highest priority task is first');
82
- });
83
-
84
- test('Task assignment and status flow', () => {
85
- const next = getNextPendingTask(db);
86
- assert(next.title === 'Fix authentication bug', 'Next task is highest priority');
87
-
88
- const ok = assignTask(db, next.id, 'worktree-feature-auth', 'claude-code');
89
- assert(ok, 'Task assigned successfully');
90
-
91
- const tasks = listTasks(db, 'in_progress');
92
- assert(tasks.length === 1, 'One task in progress');
93
- assert(tasks[0].worktree === 'worktree-feature-auth', 'Worktree correctly set');
94
-
95
- // Cannot re-assign a non-pending task
96
- const fail = assignTask(db, next.id, 'worktree-other');
97
- assert(!fail, 'Cannot re-assign in-progress task');
98
-
99
- completeTask(db, next.id);
100
- const doneTasks = listTasks(db, 'done');
101
- assert(doneTasks.length === 1, 'Task marked as done');
102
- });
103
-
104
- test('Worktree registration', () => {
105
- registerWorktree(db, { name: 'main', path: TEST_DIR, branch: 'main' });
106
- registerWorktree(db, { name: 'feature-auth', path: '/tmp/repo-feature-auth', branch: 'feature/auth', agent: 'claude-code' });
107
- registerWorktree(db, { name: 'feature-api', path: '/tmp/repo-feature-api', branch: 'feature/api', agent: 'cursor' });
108
-
109
- const wts = listWorktrees(db);
110
- assert(wts.length === 3, 'Three worktrees registered');
111
- assert(wts.find(w => w.name === 'feature-auth')?.agent === 'claude-code', 'Agent correctly stored');
112
-
113
- // Re-registering same worktree should update it (upsert)
114
- registerWorktree(db, { name: 'feature-auth', path: '/tmp/repo-feature-auth', branch: 'feature/auth', agent: 'claude-code-2' });
115
- const wts2 = listWorktrees(db);
116
- assert(wts2.length === 3, 'Still 3 worktrees after upsert');
117
- });
118
-
119
- test('File claims - happy path', () => {
120
- const taskId = createTask(db, { title: 'Refactor auth module' });
121
- assignTask(db, taskId, 'feature-auth');
122
-
123
- claimFiles(db, taskId, 'feature-auth', [
124
- 'src/auth/login.js',
125
- 'src/auth/token.js',
126
- 'tests/auth.test.js',
127
- ]);
128
-
129
- const claims = getActiveFileClaims(db);
130
- assert(claims.length === 3, 'Three files claimed');
131
- assert(claims[0].worktree === 'feature-auth', 'Claims associated with correct worktree');
132
- });
133
-
134
- test('File claims - conflict detection', () => {
135
- const taskId2 = createTask(db, { title: 'Update auth middleware' });
136
- assignTask(db, taskId2, 'feature-api');
137
-
138
- // Try to claim a file already claimed by feature-auth
139
- const conflicts = checkFileConflicts(db, [
140
- 'src/auth/login.js', // CONFLICT - claimed by feature-auth
141
- 'src/api/routes.js', // OK - not claimed
142
- ], 'feature-api');
143
-
144
- assert(conflicts.length === 1, 'One conflict detected');
145
- assert(conflicts[0].file === 'src/auth/login.js', 'Correct conflicting file identified');
146
- assert(conflicts[0].claimedBy.worktree === 'feature-auth', 'Conflict correctly attributed to feature-auth');
147
- });
148
-
149
- test('File claims - release', () => {
150
- const activeBefore = getActiveFileClaims(db);
151
- const countBefore = activeBefore.length;
152
-
153
- // Find a task with claims
154
- const tasks = listTasks(db, 'in_progress');
155
- if (tasks.length > 0) {
156
- releaseFileClaims(db, tasks[0].id);
157
- const activeAfter = getActiveFileClaims(db);
158
- assert(activeAfter.length < countBefore, 'Claims released successfully');
159
- } else {
160
- assert(true, 'Skipped (no in_progress tasks)');
161
- }
162
- });
163
-
164
- test('Task queue ordering', () => {
165
- // Clear and re-add tasks with different priorities
166
- const t1 = createTask(db, { title: 'Low priority', priority: 2 });
167
- const t2 = createTask(db, { title: 'High priority', priority: 9 });
168
- const t3 = createTask(db, { title: 'Medium priority', priority: 5 });
169
-
170
- const next = getNextPendingTask(db);
171
- assert(next.title === 'High priority', 'Queue returns highest priority first');
172
- });
173
-
174
- // ─── Fix regression tests ─────────────────────────────────────────────────────
175
-
176
- test('Fix 1: busy_timeout is set on new connections', () => {
177
- // Open a fresh connection and check that busy_timeout is active.
178
- // SQLite's PRAGMA busy_timeout returns the current timeout in ms.
179
- const freshDb = initDb(join(tmpdir(), `sw-busytimeout-${Date.now()}`));
180
- const row = freshDb.prepare('PRAGMA busy_timeout').get();
181
- // node:sqlite returns the value as a plain object keyed by column name
182
- const timeout = Object.values(row)[0];
183
- assert(timeout >= 5000, `busy_timeout is set to ${timeout}ms (expected ≥ 5000)`);
184
- freshDb.close();
185
- });
186
-
187
- test('Fix 2: SWITCHMAN_DIR constant (no stale AGENTQ_DIR)', () => {
188
- // Verify the database is created at the correct path using the renamed constant
189
- const fixDir = join(tmpdir(), `sw-const-${Date.now()}`);
190
- const fixDb = initDb(fixDir);
191
- fixDb.close();
192
- const expectedPath = join(fixDir, '.switchman', 'switchman.db');
193
- assert(existsSync(expectedPath), `.switchman/switchman.db created at correct path`);
194
- });
195
-
196
- test('Fix 3: claimFiles transaction is atomic (partial failure rolls back)', () => {
197
- // Create a duplicate-constraint scenario: claim a file twice in one call.
198
- // The second insert on the same unique key should fail.
199
- // With the old manual BEGIN/COMMIT this could leave partial data;
200
- // with db.transaction() the whole batch rolls back cleanly.
201
- const txDb = initDb(join(tmpdir(), `sw-tx-${Date.now()}`));
202
- const txTask = createTask(txDb, { title: 'tx test task' });
203
-
204
- let threw = false;
205
- try {
206
- // Insert same file path twice — SQLite will throw on the second one
207
- // because file_claims has no unique constraint but we can force an error
208
- // by using a bad value that violates NOT NULL on worktree
209
- claimFiles(txDb, txTask, null /* worktree NOT NULL violation */, ['src/a.js']);
210
- } catch {
211
- threw = true;
212
- }
213
- // Whether or not it threw, no partial claims should linger for this task
214
- const claims = txDb.prepare(`SELECT * FROM file_claims WHERE task_id=? AND released_at IS NULL`).all(txTask);
215
- assert(claims.length === 0, 'Transaction rolled back cleanly — no partial claims');
216
- txDb.close();
217
- });
218
-
219
- test('Fix 4: findRepoRoot resolves main repo root from worktree dir', () => {
220
- // Set up a mini repo with a linked worktree, then verify findRepoRoot()
221
- // returns the main repo root from inside the linked worktree path.
222
- const mainRepo = join(tmpdir(), `sw-rootfix-main-${Date.now()}`);
223
- mkdirSync(mainRepo, { recursive: true });
224
- execSync('git init', { cwd: mainRepo });
225
- execSync('git config user.email "test@test.com"', { cwd: mainRepo });
226
- execSync('git config user.name "Test"', { cwd: mainRepo });
227
- // Need at least one commit before adding a worktree
228
- execSync('git commit --allow-empty -m "init"', { cwd: mainRepo });
229
-
230
- const wtPath = join(tmpdir(), `sw-rootfix-wt-${Date.now()}`);
231
- execSync(`git worktree add -b fix-test "${wtPath}"`, { cwd: mainRepo });
232
-
233
- const rootFromMain = findRepoRoot(mainRepo);
234
- const rootFromWorktree = findRepoRoot(wtPath);
235
-
236
- // On macOS, /tmp is a symlink to /private/tmp — resolve both sides before comparing
237
- const realMain = realpathSync(mainRepo);
238
- assert(realpathSync(rootFromMain) === realMain, `Root from main worktree is correct`);
239
- assert(realpathSync(rootFromWorktree) === realMain, `Root from linked worktree resolves to main repo`);
240
-
241
- // Cleanup
242
- execSync(`git worktree remove "${wtPath}" --force`, { cwd: mainRepo });
243
- rmSync(mainRepo, { recursive: true, force: true });
244
- });
245
-
246
- // ─── Cleanup & Results ────────────────────────────────────────────────────────
247
-
248
- if (db) db.close();
249
- rmSync(TEST_DIR, { recursive: true, force: true });
250
-
251
- console.log(`\n${'─'.repeat(40)}`);
252
- console.log(`Results: ${passed} passed, ${failed} failed`);
253
-
254
- if (failed > 0) {
255
- console.log('\n⚠ Some tests failed');
256
- process.exit(1);
257
- } else {
258
- console.log('\n✓ All tests passed');
259
- }