groove-dev 0.27.207 → 0.27.209
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/axom-connector.js +50 -2
- package/node_modules/@groove-dev/daemon/src/axom-remote.js +16 -14
- package/node_modules/@groove-dev/daemon/src/axom-runtimes.js +498 -0
- package/node_modules/@groove-dev/daemon/src/axom-server.js +25 -4
- package/node_modules/@groove-dev/daemon/src/index.js +3 -1
- package/node_modules/@groove-dev/daemon/src/routes/axom.js +113 -0
- package/node_modules/@groove-dev/daemon/src/routes/watch.js +11 -0
- package/node_modules/@groove-dev/daemon/src/watcher.js +55 -2
- package/node_modules/@groove-dev/daemon/test/axom-connector.test.js +44 -1
- package/node_modules/@groove-dev/daemon/test/axom-runtimes.test.js +367 -0
- package/node_modules/@groove-dev/daemon/test/watcher.test.js +68 -2
- package/node_modules/@groove-dev/gui/dist/assets/{index-OzNfp6-Y.js → index-217ZVIOc.js} +242 -237
- package/node_modules/@groove-dev/gui/dist/assets/index-DdCadtGL.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/axom-connector.js +50 -2
- package/packages/daemon/src/axom-remote.js +16 -14
- package/packages/daemon/src/axom-runtimes.js +498 -0
- package/packages/daemon/src/axom-server.js +25 -4
- package/packages/daemon/src/index.js +3 -1
- package/packages/daemon/src/routes/axom.js +113 -0
- package/packages/daemon/src/routes/watch.js +11 -0
- package/packages/daemon/src/watcher.js +55 -2
- package/packages/gui/dist/assets/{index-OzNfp6-Y.js → index-217ZVIOc.js} +242 -237
- package/packages/gui/dist/assets/index-DdCadtGL.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-DG6yq4dB.css +0 -1
- package/packages/gui/dist/assets/index-DG6yq4dB.css +0 -1
|
@@ -143,9 +143,11 @@ describe('Watcher', () => {
|
|
|
143
143
|
assert.throws(() => watcher.create('nope', { command: 'x' }), /Agent not found/);
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
// Distinct commands — identical ones now re-attach to the running job rather
|
|
147
|
+
// than stacking up, so they would never reach the cap.
|
|
146
148
|
it('caps active watches per agent', () => {
|
|
147
|
-
for (let i = 0; i < 5; i++) watcher.create('a1', { command:
|
|
148
|
-
assert.throws(() => watcher.create('a1', { command: 'sleep 5' }), /already have 5/);
|
|
149
|
+
for (let i = 0; i < 5; i++) watcher.create('a1', { command: `sleep 5 # ${i}`, label: `w${i}` });
|
|
150
|
+
assert.throws(() => watcher.create('a1', { command: 'sleep 5 # 6' }), /already have 5/);
|
|
149
151
|
});
|
|
150
152
|
|
|
151
153
|
it('cancels a watch and stops its process', async () => {
|
|
@@ -206,6 +208,70 @@ describe('Watcher', () => {
|
|
|
206
208
|
watcher2.stop();
|
|
207
209
|
});
|
|
208
210
|
|
|
211
|
+
// ── no double-launch (regression: daemon rebuild re-ran a live job) ──
|
|
212
|
+
//
|
|
213
|
+
// A daemon restart resumes the agent mid-turn, so it re-issues the watch it
|
|
214
|
+
// believes never completed. Launching a second copy of a long job corrupts
|
|
215
|
+
// the first one's output and competes for the same hardware.
|
|
216
|
+
|
|
217
|
+
it('re-attaches instead of launching a second copy of a running command', async () => {
|
|
218
|
+
const marker = resolve(grooveDir, 'runs.txt');
|
|
219
|
+
const cmd = `echo run >> ${marker}; sleep 3`;
|
|
220
|
+
|
|
221
|
+
const first = watcher.create('a1', { command: cmd, label: 'champion v11' });
|
|
222
|
+
await settle(300);
|
|
223
|
+
|
|
224
|
+
const second = watcher.create('a1', { command: cmd, label: 'champion v11' });
|
|
225
|
+
assert.equal(second.id, first.id, 'the same watch is returned, not a new one');
|
|
226
|
+
assert.equal(second.reattached, true, 'the caller is told it re-attached');
|
|
227
|
+
assert.equal(
|
|
228
|
+
watcher.list().filter((w) => w.status === 'active').length, 1,
|
|
229
|
+
'only one active watch exists',
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
await settle(400);
|
|
233
|
+
const runs = readFileSync(marker, 'utf8').trim().split('\n').filter(Boolean);
|
|
234
|
+
assert.equal(runs.length, 1, 'the command executed exactly once');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('does not re-attach once the original job has exited', async () => {
|
|
238
|
+
const cmd = 'exit 0';
|
|
239
|
+
const first = watcher.create('a1', { command: cmd, label: 'short' });
|
|
240
|
+
await settle(500); // job finishes and the watch fires
|
|
241
|
+
|
|
242
|
+
const second = watcher.create('a1', { command: cmd, label: 'short again' });
|
|
243
|
+
assert.notEqual(second.id, first.id, 'a finished job may legitimately be re-run');
|
|
244
|
+
assert.ok(!second.reattached);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('reports a job that vanished while the daemon was down instead of polling to timeout', async () => {
|
|
248
|
+
const w = watcher.create('a1', { command: 'sleep 30', label: 'doomed' });
|
|
249
|
+
await settle(200);
|
|
250
|
+
watcher._killJob(watcher.watches.get(w.id)); // job dies during the outage
|
|
251
|
+
watcher.stop();
|
|
252
|
+
await settle(200);
|
|
253
|
+
|
|
254
|
+
const daemon2 = makeDaemon(grooveDir);
|
|
255
|
+
wireDelivery(daemon2);
|
|
256
|
+
const watcher2 = new Watcher(daemon2);
|
|
257
|
+
daemon2.watcher = watcher2;
|
|
258
|
+
daemon2.registry.add({ id: 'a1', name: 'fullstack-1', role: 'fullstack', provider: 'claude-code' });
|
|
259
|
+
|
|
260
|
+
watcher2.restore();
|
|
261
|
+
await settle(120);
|
|
262
|
+
assert.equal(daemon2.delivered.length, 1, 'the agent is told the job was lost');
|
|
263
|
+
assert.match(daemon2.delivered[0].message, /lost while the daemon was down/);
|
|
264
|
+
watcher2.stop();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('appends to the run log so a re-run cannot erase the record', () => {
|
|
268
|
+
const w = watcher.create('a1', { command: 'echo hi', label: 'log' });
|
|
269
|
+
const script = readFileSync(resolve(grooveDir, 'watch-runs', w.id, 'run.sh'), 'utf8');
|
|
270
|
+
const outLine = script.split('\n').find((l) => l.includes('out.log'));
|
|
271
|
+
assert.ok(outLine.includes('>>'), 'the output redirect appends');
|
|
272
|
+
assert.ok(!/[^>]>\s*'[^']*out\.log/.test(outLine), 'the output log is never truncated');
|
|
273
|
+
});
|
|
274
|
+
|
|
209
275
|
it('persists watches to disk', () => {
|
|
210
276
|
watcher.create('a1', { command: 'sleep 5', label: 'persisted' });
|
|
211
277
|
const raw = JSON.parse(readFileSync(resolve(grooveDir, 'watches.json'), 'utf8'));
|