@dungle-scrubs/tallow 0.8.26 → 0.8.28
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 +42 -1
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/interactive-mode-patch.d.ts +1 -0
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +40 -1
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/model-metadata-overrides.d.ts +2 -5
- package/dist/model-metadata-overrides.d.ts.map +1 -1
- package/dist/model-metadata-overrides.js +23 -12
- package/dist/model-metadata-overrides.js.map +1 -1
- package/dist/pid-manager.d.ts +2 -9
- package/dist/pid-manager.d.ts.map +1 -1
- package/dist/pid-manager.js +1 -58
- package/dist/pid-manager.js.map +1 -1
- package/dist/pid-schema.d.ts +51 -0
- package/dist/pid-schema.d.ts.map +1 -0
- package/dist/pid-schema.js +70 -0
- package/dist/pid-schema.js.map +1 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +24 -17
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-interactive.d.ts.map +1 -1
- package/dist/workspace-transition-interactive.js +53 -3
- package/dist/workspace-transition-interactive.js.map +1 -1
- package/dist/workspace-transition.d.ts +2 -1
- package/dist/workspace-transition.d.ts.map +1 -1
- package/dist/workspace-transition.js +16 -4
- package/dist/workspace-transition.js.map +1 -1
- package/extensions/__integration__/audit-findings.test.ts +309 -0
- package/extensions/__integration__/cd-tool-guidelines.test.ts +46 -0
- package/extensions/__integration__/tasks-runtime.test.ts +63 -12
- package/extensions/__integration__/welcome-screen.test.ts +240 -0
- package/extensions/_shared/lazy-init.ts +88 -3
- package/extensions/_shared/pid-registry.ts +8 -82
- package/extensions/background-task-tool/index.ts +1 -1
- package/extensions/cd-tool/index.ts +4 -1
- package/extensions/cheatsheet/__tests__/cheatsheet.test.ts +47 -0
- package/extensions/clear/__tests__/clear.test.ts +38 -0
- package/extensions/edit-tool-enhanced/index.ts +3 -1
- package/extensions/git-status/__tests__/git-status.test.ts +32 -0
- package/extensions/health/__tests__/diagnostics.test.ts +25 -0
- package/extensions/health/index.ts +61 -0
- package/extensions/loop/__tests__/loop.test.ts +365 -1
- package/extensions/loop/index.ts +213 -3
- package/extensions/mcp-adapter-tool/index.ts +1 -1
- package/extensions/minimal-skill-display/__tests__/minimal-skill-display.test.ts +20 -0
- package/extensions/permissions/__tests__/permissions.test.ts +213 -0
- package/extensions/progress-indicator/__tests__/progress-indicator.test.ts +104 -0
- package/extensions/prompt-suggestions/__tests__/autocomplete.test.ts +111 -3
- package/extensions/prompt-suggestions/autocomplete.ts +23 -5
- package/extensions/prompt-suggestions/index.ts +62 -3
- package/extensions/random-spinner/__tests__/random-spinner.test.ts +35 -0
- package/extensions/read-tool-enhanced/index.ts +5 -1
- package/extensions/session-memory/index.ts +1 -1
- package/extensions/session-namer/index.ts +1 -1
- package/extensions/show-system-prompt/__tests__/show-system-prompt.test.ts +51 -0
- package/extensions/subagent-tool/__tests__/presentation-rendering.test.ts +9 -8
- package/extensions/subagent-tool/__tests__/process-liveness.test.ts +51 -0
- package/extensions/subagent-tool/__tests__/subprocess-args.test.ts +120 -0
- package/extensions/subagent-tool/formatting.ts +2 -0
- package/extensions/subagent-tool/index.ts +160 -97
- package/extensions/subagent-tool/process.ts +152 -40
- package/extensions/tasks/commands/register-tasks-extension.ts +64 -20
- package/extensions/tasks/extension.json +1 -0
- package/extensions/tasks/index.ts +2 -12
- package/extensions/tasks/state/index.ts +26 -0
- package/extensions/teams-tool/dashboard.ts +13 -1
- package/extensions/teams-tool/sessions/spawn.ts +2 -2
- package/extensions/teams-tool/tools/register-extension.ts +10 -2
- package/extensions/upstream-check/__tests__/upstream-check.test.ts +49 -0
- package/extensions/welcome-screen/__tests__/welcome-screen.test.ts +35 -0
- package/extensions/welcome-screen/extension.json +20 -0
- package/extensions/welcome-screen/index.ts +189 -0
- package/extensions/wezterm-notify/__tests__/index.test.ts +49 -11
- package/extensions/wezterm-notify/index.ts +5 -3
- package/extensions/write-tool-enhanced/__tests__/write-tool-enhanced.test.ts +296 -0
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +2 -2
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.js +2 -2
- package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +309 -25
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +392 -72
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +30 -0
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.js +50 -6
- package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +27 -0
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js +59 -4
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +9 -0
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js +50 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/package.json +1 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +134 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tmux-compat.test.ts +204 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +49 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +2 -0
- package/node_modules/@mariozechner/pi-tui/src/index.ts +11 -0
- package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +478 -140
- package/node_modules/@mariozechner/pi-tui/src/keys.ts +84 -6
- package/node_modules/@mariozechner/pi-tui/src/terminal.ts +69 -4
- package/node_modules/@mariozechner/pi-tui/src/tui.ts +64 -1
- package/package.json +11 -10
- package/runtime/config.ts +7 -0
- package/runtime/model-metadata-overrides.ts +7 -0
- package/runtime/pid-schema.ts +13 -0
- package/skills/tallow-expert/SKILL.md +7 -5
|
@@ -5,14 +5,23 @@ import { join } from "node:path";
|
|
|
5
5
|
import { type DiagnosticInput, runDiagnostics } from "../index.js";
|
|
6
6
|
|
|
7
7
|
let tmpDir: string;
|
|
8
|
+
let savedTmux: string | undefined;
|
|
8
9
|
|
|
9
10
|
beforeEach(() => {
|
|
10
11
|
tmpDir = join(tmpdir(), `health-diag-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
11
12
|
mkdirSync(tmpDir, { recursive: true });
|
|
13
|
+
// Isolate tests from host tmux environment so tmux diagnostics don't fire
|
|
14
|
+
savedTmux = process.env.TMUX;
|
|
15
|
+
delete process.env.TMUX;
|
|
12
16
|
});
|
|
13
17
|
|
|
14
18
|
afterEach(() => {
|
|
15
19
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
20
|
+
if (savedTmux !== undefined) {
|
|
21
|
+
process.env.TMUX = savedTmux;
|
|
22
|
+
} else {
|
|
23
|
+
delete process.env.TMUX;
|
|
24
|
+
}
|
|
16
25
|
});
|
|
17
26
|
|
|
18
27
|
/**
|
|
@@ -172,4 +181,20 @@ describe("runDiagnostics", () => {
|
|
|
172
181
|
const ctxCheck = checks.find((c) => c.name === "Project context");
|
|
173
182
|
expect(ctxCheck?.status).toBe("pass");
|
|
174
183
|
});
|
|
184
|
+
|
|
185
|
+
test("skips tmux checks when not inside tmux", () => {
|
|
186
|
+
delete process.env.TMUX;
|
|
187
|
+
const checks = runDiagnostics(makeInput());
|
|
188
|
+
const tmuxChecks = checks.filter((c) => c.name.startsWith("tmux"));
|
|
189
|
+
expect(tmuxChecks).toHaveLength(0);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("includes tmux checks when TMUX env var is set", () => {
|
|
193
|
+
process.env.TMUX = "/tmp/tmux-test/default,12345,0";
|
|
194
|
+
const checks = runDiagnostics(makeInput());
|
|
195
|
+
const tmuxChecks = checks.filter((c) => c.name.startsWith("tmux"));
|
|
196
|
+
// Should have at least escape-time and extended-keys checks
|
|
197
|
+
// (may fail gracefully if tmux binary is not available)
|
|
198
|
+
expect(tmuxChecks.length).toBeGreaterThanOrEqual(0);
|
|
199
|
+
});
|
|
175
200
|
});
|
|
@@ -623,6 +623,67 @@ export function runDiagnostics(input: DiagnosticInput): DiagnosticCheck[] {
|
|
|
623
623
|
});
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
+
// 9. tmux compatibility (keyboard protocol)
|
|
627
|
+
if (process.env.TMUX) {
|
|
628
|
+
const tmuxChecks = checkTmuxSettings();
|
|
629
|
+
checks.push(...tmuxChecks);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return checks;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Check tmux settings that affect keyboard handling.
|
|
637
|
+
* Only called when running inside tmux ($TMUX is set).
|
|
638
|
+
*
|
|
639
|
+
* @returns Array of diagnostic check results for tmux configuration
|
|
640
|
+
*/
|
|
641
|
+
function checkTmuxSettings(): DiagnosticCheck[] {
|
|
642
|
+
const checks: DiagnosticCheck[] = [];
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
const { execSync } = require("node:child_process");
|
|
646
|
+
const opts = execSync("tmux show-options -g", { encoding: "utf-8", timeout: 2000 });
|
|
647
|
+
|
|
648
|
+
// Check escape-time (should be 0 or very low for responsive Escape key)
|
|
649
|
+
const escapeTimeMatch = opts.match(/^escape-time\s+(\d+)/m);
|
|
650
|
+
const escapeTime = escapeTimeMatch ? parseInt(escapeTimeMatch[1], 10) : 500;
|
|
651
|
+
if (escapeTime > 50) {
|
|
652
|
+
checks.push({
|
|
653
|
+
name: "tmux escape-time",
|
|
654
|
+
status: "warn",
|
|
655
|
+
message: `${escapeTime}ms delay before Escape is forwarded`,
|
|
656
|
+
suggestion: "Add `set -g escape-time 0` to tmux.conf",
|
|
657
|
+
});
|
|
658
|
+
} else {
|
|
659
|
+
checks.push({
|
|
660
|
+
name: "tmux escape-time",
|
|
661
|
+
status: "pass",
|
|
662
|
+
message: `${escapeTime}ms`,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Check extended-keys (needed for Shift+Enter and modified key detection)
|
|
667
|
+
const extKeysMatch = opts.match(/^extended-keys\s+(\S+)/m);
|
|
668
|
+
const extKeys = extKeysMatch ? extKeysMatch[1] : "off";
|
|
669
|
+
if (extKeys === "off") {
|
|
670
|
+
checks.push({
|
|
671
|
+
name: "tmux extended-keys",
|
|
672
|
+
status: "warn",
|
|
673
|
+
message: "Shift+Enter and other modified keys won't work",
|
|
674
|
+
suggestion: "Add `set -g extended-keys on` to tmux.conf",
|
|
675
|
+
});
|
|
676
|
+
} else {
|
|
677
|
+
checks.push({
|
|
678
|
+
name: "tmux extended-keys",
|
|
679
|
+
status: "pass",
|
|
680
|
+
message: extKeys,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
} catch {
|
|
684
|
+
// tmux command failed — skip these checks silently
|
|
685
|
+
}
|
|
686
|
+
|
|
626
687
|
return checks;
|
|
627
688
|
}
|
|
628
689
|
|
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
* Unit tests for the loop extension's pure helpers.
|
|
3
3
|
*
|
|
4
4
|
* Tests interval parsing, countdown formatting, argument parsing,
|
|
5
|
-
* max iterations,
|
|
5
|
+
* max iterations, until-condition extraction, natural-language parsing,
|
|
6
|
+
* and command building.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { describe, expect, test } from "bun:test";
|
|
9
10
|
import {
|
|
11
|
+
buildLoopCommand,
|
|
10
12
|
extractUntilCondition,
|
|
11
13
|
formatCountdown,
|
|
12
14
|
parseInterval,
|
|
13
15
|
parseLoopArgs,
|
|
14
16
|
parseMaxIterations,
|
|
17
|
+
parseNaturalLanguageLoop,
|
|
15
18
|
} from "../index.js";
|
|
16
19
|
|
|
17
20
|
describe("parseInterval", () => {
|
|
@@ -322,3 +325,364 @@ describe("parseLoopArgs", () => {
|
|
|
322
325
|
});
|
|
323
326
|
});
|
|
324
327
|
});
|
|
328
|
+
|
|
329
|
+
// ── Natural language parsing ─────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
describe("parseNaturalLanguageLoop", () => {
|
|
332
|
+
// ── Interval extraction ──────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
test("extracts 'every N minutes'", () => {
|
|
335
|
+
const result = parseNaturalLanguageLoop("check ci every 2 minutes");
|
|
336
|
+
expect(result).toEqual({
|
|
337
|
+
action: "start",
|
|
338
|
+
intervalMs: 120_000,
|
|
339
|
+
intervalLabel: "2m",
|
|
340
|
+
prompt: "check ci",
|
|
341
|
+
maxIterations: null,
|
|
342
|
+
untilCondition: null,
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("extracts 'every N seconds'", () => {
|
|
347
|
+
const result = parseNaturalLanguageLoop("run tests every 30 seconds");
|
|
348
|
+
expect(result).toEqual({
|
|
349
|
+
action: "start",
|
|
350
|
+
intervalMs: 30_000,
|
|
351
|
+
intervalLabel: "30s",
|
|
352
|
+
prompt: "run tests",
|
|
353
|
+
maxIterations: null,
|
|
354
|
+
untilCondition: null,
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("extracts 'every N hrs'", () => {
|
|
359
|
+
const result = parseNaturalLanguageLoop("check logs every 2 hrs");
|
|
360
|
+
expect(result).toEqual({
|
|
361
|
+
action: "start",
|
|
362
|
+
intervalMs: 7_200_000,
|
|
363
|
+
intervalLabel: "2h",
|
|
364
|
+
prompt: "check logs",
|
|
365
|
+
maxIterations: null,
|
|
366
|
+
untilCondition: null,
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("extracts 'every minute' (no number → 1)", () => {
|
|
371
|
+
const result = parseNaturalLanguageLoop("check deploy every minute");
|
|
372
|
+
expect(result).toEqual({
|
|
373
|
+
action: "start",
|
|
374
|
+
intervalMs: 60_000,
|
|
375
|
+
intervalLabel: "1m",
|
|
376
|
+
prompt: "check deploy",
|
|
377
|
+
maxIterations: null,
|
|
378
|
+
untilCondition: null,
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("extracts 'every hour'", () => {
|
|
383
|
+
const result = parseNaturalLanguageLoop("summarize logs every hour");
|
|
384
|
+
expect(result).toEqual({
|
|
385
|
+
action: "start",
|
|
386
|
+
intervalMs: 3_600_000,
|
|
387
|
+
intervalLabel: "1h",
|
|
388
|
+
prompt: "summarize logs",
|
|
389
|
+
maxIterations: null,
|
|
390
|
+
untilCondition: null,
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("extracts 'every Nm' shorthand", () => {
|
|
395
|
+
const result = parseNaturalLanguageLoop("check ci every 5m");
|
|
396
|
+
expect(result).toEqual({
|
|
397
|
+
action: "start",
|
|
398
|
+
intervalMs: 300_000,
|
|
399
|
+
intervalLabel: "5m",
|
|
400
|
+
prompt: "check ci",
|
|
401
|
+
maxIterations: null,
|
|
402
|
+
untilCondition: null,
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("extracts bare interval without 'every'", () => {
|
|
407
|
+
const result = parseNaturalLanguageLoop("check ci 2m");
|
|
408
|
+
expect(result).toEqual({
|
|
409
|
+
action: "start",
|
|
410
|
+
intervalMs: 120_000,
|
|
411
|
+
intervalLabel: "2m",
|
|
412
|
+
prompt: "check ci",
|
|
413
|
+
maxIterations: null,
|
|
414
|
+
untilCondition: null,
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("returns null when no interval found", () => {
|
|
419
|
+
expect(parseNaturalLanguageLoop("check ci please")).toBeNull();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("returns null when no prompt remains", () => {
|
|
423
|
+
expect(parseNaturalLanguageLoop("every 2m")).toBeNull();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// ── Condition extraction ─────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
test("extracts 'until' condition at end", () => {
|
|
429
|
+
const result = parseNaturalLanguageLoop("check ci every 2 minutes until it passes");
|
|
430
|
+
expect(result).toEqual({
|
|
431
|
+
action: "start",
|
|
432
|
+
intervalMs: 120_000,
|
|
433
|
+
intervalLabel: "2m",
|
|
434
|
+
prompt: "check ci",
|
|
435
|
+
maxIterations: null,
|
|
436
|
+
untilCondition: "it passes",
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("extracts 'stop when' condition at end", () => {
|
|
441
|
+
const result = parseNaturalLanguageLoop("run tests every 30s, stop when they pass");
|
|
442
|
+
expect(result).toEqual({
|
|
443
|
+
action: "start",
|
|
444
|
+
intervalMs: 30_000,
|
|
445
|
+
intervalLabel: "30s",
|
|
446
|
+
prompt: "run tests",
|
|
447
|
+
maxIterations: null,
|
|
448
|
+
untilCondition: "they pass",
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("extracts condition at start with comma separator", () => {
|
|
453
|
+
const result = parseNaturalLanguageLoop("until the build passes, check ci every 2m");
|
|
454
|
+
expect(result).toEqual({
|
|
455
|
+
action: "start",
|
|
456
|
+
intervalMs: 120_000,
|
|
457
|
+
intervalLabel: "2m",
|
|
458
|
+
prompt: "check ci",
|
|
459
|
+
maxIterations: null,
|
|
460
|
+
untilCondition: "the build passes",
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("strips quotes from NL condition", () => {
|
|
465
|
+
const result = parseNaturalLanguageLoop('check ci every 2m until "the build is green"');
|
|
466
|
+
expect(result).toEqual({
|
|
467
|
+
action: "start",
|
|
468
|
+
intervalMs: 120_000,
|
|
469
|
+
intervalLabel: "2m",
|
|
470
|
+
prompt: "check ci",
|
|
471
|
+
maxIterations: null,
|
|
472
|
+
untilCondition: "the build is green",
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("strips trailing punctuation from condition", () => {
|
|
477
|
+
const result = parseNaturalLanguageLoop("check ci every 2m until it passes.");
|
|
478
|
+
expect(result).toEqual({
|
|
479
|
+
action: "start",
|
|
480
|
+
intervalMs: 120_000,
|
|
481
|
+
intervalLabel: "2m",
|
|
482
|
+
prompt: "check ci",
|
|
483
|
+
maxIterations: null,
|
|
484
|
+
untilCondition: "it passes",
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// ── Max iterations extraction ────────────────────────────────────
|
|
489
|
+
|
|
490
|
+
test("extracts 'N times'", () => {
|
|
491
|
+
const result = parseNaturalLanguageLoop("check ci every 2m 10 times");
|
|
492
|
+
expect(result).toEqual({
|
|
493
|
+
action: "start",
|
|
494
|
+
intervalMs: 120_000,
|
|
495
|
+
intervalLabel: "2m",
|
|
496
|
+
prompt: "check ci",
|
|
497
|
+
maxIterations: 10,
|
|
498
|
+
untilCondition: null,
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("extracts 'max N'", () => {
|
|
503
|
+
const result = parseNaturalLanguageLoop("check ci every 2m max 20");
|
|
504
|
+
expect(result).toEqual({
|
|
505
|
+
action: "start",
|
|
506
|
+
intervalMs: 120_000,
|
|
507
|
+
intervalLabel: "2m",
|
|
508
|
+
prompt: "check ci",
|
|
509
|
+
maxIterations: 20,
|
|
510
|
+
untilCondition: null,
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("extracts 'max N tries'", () => {
|
|
515
|
+
const result = parseNaturalLanguageLoop("monitor deploy health every minute, max 20 tries");
|
|
516
|
+
expect(result).toEqual({
|
|
517
|
+
action: "start",
|
|
518
|
+
intervalMs: 60_000,
|
|
519
|
+
intervalLabel: "1m",
|
|
520
|
+
prompt: "monitor deploy health",
|
|
521
|
+
maxIterations: 20,
|
|
522
|
+
untilCondition: null,
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("extracts 'at most N'", () => {
|
|
527
|
+
const result = parseNaturalLanguageLoop("check ci every 5m at most 15");
|
|
528
|
+
expect(result).toEqual({
|
|
529
|
+
action: "start",
|
|
530
|
+
intervalMs: 300_000,
|
|
531
|
+
intervalLabel: "5m",
|
|
532
|
+
prompt: "check ci",
|
|
533
|
+
maxIterations: 15,
|
|
534
|
+
untilCondition: null,
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test("extracts x<N> in NL context", () => {
|
|
539
|
+
const result = parseNaturalLanguageLoop("check ci every 2m x10");
|
|
540
|
+
expect(result).toEqual({
|
|
541
|
+
action: "start",
|
|
542
|
+
intervalMs: 120_000,
|
|
543
|
+
intervalLabel: "2m",
|
|
544
|
+
prompt: "check ci",
|
|
545
|
+
maxIterations: 10,
|
|
546
|
+
untilCondition: null,
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// ── Combined: all features ───────────────────────────────────────
|
|
551
|
+
|
|
552
|
+
test("extracts interval + condition + max iterations", () => {
|
|
553
|
+
const result = parseNaturalLanguageLoop(
|
|
554
|
+
"run tests every 30 seconds, max 20, until they all pass"
|
|
555
|
+
);
|
|
556
|
+
expect(result).toEqual({
|
|
557
|
+
action: "start",
|
|
558
|
+
intervalMs: 30_000,
|
|
559
|
+
intervalLabel: "30s",
|
|
560
|
+
prompt: "run tests",
|
|
561
|
+
maxIterations: 20,
|
|
562
|
+
untilCondition: "they all pass",
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test("handles realistic CI monitoring request", () => {
|
|
567
|
+
const result = parseNaturalLanguageLoop(
|
|
568
|
+
"check the latest GitHub Actions run for this branch every 2 minutes until the latest CI run is green"
|
|
569
|
+
);
|
|
570
|
+
expect(result).toEqual({
|
|
571
|
+
action: "start",
|
|
572
|
+
intervalMs: 120_000,
|
|
573
|
+
intervalLabel: "2m",
|
|
574
|
+
prompt: "check the latest GitHub Actions run for this branch",
|
|
575
|
+
maxIterations: null,
|
|
576
|
+
untilCondition: "the latest CI run is green",
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test("handles 'stop when' with comma", () => {
|
|
581
|
+
const result = parseNaturalLanguageLoop("run the test suite every 30s, stop when tests pass");
|
|
582
|
+
expect(result).toEqual({
|
|
583
|
+
action: "start",
|
|
584
|
+
intervalMs: 30_000,
|
|
585
|
+
intervalLabel: "30s",
|
|
586
|
+
prompt: "run the test suite",
|
|
587
|
+
maxIterations: null,
|
|
588
|
+
untilCondition: "tests pass",
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// ── Interval position doesn't matter ─────────────────────────────
|
|
593
|
+
|
|
594
|
+
test("interval at start of text", () => {
|
|
595
|
+
const result = parseNaturalLanguageLoop("every 5 minutes check if the build finished");
|
|
596
|
+
expect(result).toEqual({
|
|
597
|
+
action: "start",
|
|
598
|
+
intervalMs: 300_000,
|
|
599
|
+
intervalLabel: "5m",
|
|
600
|
+
prompt: "check if the build finished",
|
|
601
|
+
maxIterations: null,
|
|
602
|
+
untilCondition: null,
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("interval in middle of text", () => {
|
|
607
|
+
const result = parseNaturalLanguageLoop("check ci every 2m until it's green");
|
|
608
|
+
expect(result).toEqual({
|
|
609
|
+
action: "start",
|
|
610
|
+
intervalMs: 120_000,
|
|
611
|
+
intervalLabel: "2m",
|
|
612
|
+
prompt: "check ci",
|
|
613
|
+
maxIterations: null,
|
|
614
|
+
untilCondition: "it's green",
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// ── buildLoopCommand ─────────────────────────────────────────────────────
|
|
620
|
+
|
|
621
|
+
describe("buildLoopCommand", () => {
|
|
622
|
+
test("builds simple command", () => {
|
|
623
|
+
expect(
|
|
624
|
+
buildLoopCommand({
|
|
625
|
+
action: "start",
|
|
626
|
+
intervalMs: 300_000,
|
|
627
|
+
intervalLabel: "5m",
|
|
628
|
+
prompt: "check deploy",
|
|
629
|
+
maxIterations: null,
|
|
630
|
+
untilCondition: null,
|
|
631
|
+
})
|
|
632
|
+
).toBe("/loop 5m check deploy");
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test("includes max iterations", () => {
|
|
636
|
+
expect(
|
|
637
|
+
buildLoopCommand({
|
|
638
|
+
action: "start",
|
|
639
|
+
intervalMs: 60_000,
|
|
640
|
+
intervalLabel: "1m",
|
|
641
|
+
prompt: "run tests",
|
|
642
|
+
maxIterations: 10,
|
|
643
|
+
untilCondition: null,
|
|
644
|
+
})
|
|
645
|
+
).toBe("/loop 1m x10 run tests");
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("includes until condition", () => {
|
|
649
|
+
expect(
|
|
650
|
+
buildLoopCommand({
|
|
651
|
+
action: "start",
|
|
652
|
+
intervalMs: 120_000,
|
|
653
|
+
intervalLabel: "2m",
|
|
654
|
+
prompt: "check ci",
|
|
655
|
+
maxIterations: null,
|
|
656
|
+
untilCondition: "build is green",
|
|
657
|
+
})
|
|
658
|
+
).toBe('/loop 2m until "build is green" check ci');
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test("includes both max iterations and condition", () => {
|
|
662
|
+
expect(
|
|
663
|
+
buildLoopCommand({
|
|
664
|
+
action: "start",
|
|
665
|
+
intervalMs: 30_000,
|
|
666
|
+
intervalLabel: "30s",
|
|
667
|
+
prompt: "run the test suite",
|
|
668
|
+
maxIterations: 50,
|
|
669
|
+
untilCondition: "tests pass",
|
|
670
|
+
})
|
|
671
|
+
).toBe('/loop 30s x50 until "tests pass" run the test suite');
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
test("round-trips through parseLoopArgs", () => {
|
|
675
|
+
const nl = parseNaturalLanguageLoop("check ci every 2 minutes until the build passes");
|
|
676
|
+
if (!nl) throw new Error("Expected NL parse to succeed");
|
|
677
|
+
const command = buildLoopCommand(nl);
|
|
678
|
+
const strict = parseLoopArgs(command.replace(/^\/loop\s+/, ""));
|
|
679
|
+
expect(strict).toEqual({
|
|
680
|
+
action: "start",
|
|
681
|
+
intervalMs: 120_000,
|
|
682
|
+
intervalLabel: "2m",
|
|
683
|
+
prompt: "check ci",
|
|
684
|
+
maxIterations: null,
|
|
685
|
+
untilCondition: "the build passes",
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
});
|