switchman-dev 0.1.0 → 0.1.1
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/package.json +5 -5
- package/CLAUDE.md +0 -98
- package/examples/taskapi/.switchman/switchman.db +0 -0
- package/examples/taskapi/package-lock.json +0 -4736
- package/examples/taskapi/tests/api.test.js +0 -112
- package/examples/worktrees/agent-rate-limiting/package-lock.json +0 -4736
- package/examples/worktrees/agent-rate-limiting/package.json +0 -18
- package/examples/worktrees/agent-rate-limiting/src/db.js +0 -179
- package/examples/worktrees/agent-rate-limiting/src/middleware/auth.js +0 -96
- package/examples/worktrees/agent-rate-limiting/src/middleware/validate.js +0 -133
- package/examples/worktrees/agent-rate-limiting/src/routes/tasks.js +0 -65
- package/examples/worktrees/agent-rate-limiting/src/routes/users.js +0 -38
- package/examples/worktrees/agent-rate-limiting/src/server.js +0 -7
- package/examples/worktrees/agent-rate-limiting/tests/api.test.js +0 -112
- package/examples/worktrees/agent-tests/package-lock.json +0 -4736
- package/examples/worktrees/agent-tests/package.json +0 -18
- package/examples/worktrees/agent-tests/src/db.js +0 -179
- package/examples/worktrees/agent-tests/src/middleware/auth.js +0 -96
- package/examples/worktrees/agent-tests/src/middleware/validate.js +0 -133
- package/examples/worktrees/agent-tests/src/routes/tasks.js +0 -65
- package/examples/worktrees/agent-tests/src/routes/users.js +0 -38
- package/examples/worktrees/agent-tests/src/server.js +0 -7
- package/examples/worktrees/agent-tests/tests/api.test.js +0 -112
- package/examples/worktrees/agent-validation/package-lock.json +0 -4736
- package/examples/worktrees/agent-validation/package.json +0 -18
- package/examples/worktrees/agent-validation/src/db.js +0 -179
- package/examples/worktrees/agent-validation/src/middleware/auth.js +0 -96
- package/examples/worktrees/agent-validation/src/middleware/validate.js +0 -133
- package/examples/worktrees/agent-validation/src/routes/tasks.js +0 -65
- package/examples/worktrees/agent-validation/src/routes/users.js +0 -38
- package/examples/worktrees/agent-validation/src/server.js +0 -7
- package/examples/worktrees/agent-validation/tests/api.test.js +0 -112
- 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
|
-
}
|