mob-coordinator 0.3.2 → 0.3.3
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/dist/server/server/__tests__/instance-manager-utils.test.js +25 -0
- package/dist/server/server/__tests__/scrollback-buffer.test.js +56 -0
- package/dist/server/server/__tests__/terminal-state-detector.test.js +43 -0
- package/dist/server/server/instance-manager.js +1 -1
- package/dist/server/shared/__tests__/settings.test.js +34 -0
- package/package.json +1 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { extractJiraKey } from '../instance-manager.js';
|
|
3
|
+
describe('extractJiraKey', () => {
|
|
4
|
+
it('extracts key from branch name', () => {
|
|
5
|
+
expect(extractJiraKey('feature/PROJ-123-add-login')).toBe('PROJ-123');
|
|
6
|
+
});
|
|
7
|
+
it('extracts key from bare key', () => {
|
|
8
|
+
expect(extractJiraKey('PROJ-456')).toBe('PROJ-456');
|
|
9
|
+
});
|
|
10
|
+
it('returns null for no match', () => {
|
|
11
|
+
expect(extractJiraKey('main')).toBe(null);
|
|
12
|
+
});
|
|
13
|
+
it('returns null for undefined', () => {
|
|
14
|
+
expect(extractJiraKey(undefined)).toBe(null);
|
|
15
|
+
});
|
|
16
|
+
it('returns null for empty string', () => {
|
|
17
|
+
expect(extractJiraKey('')).toBe(null);
|
|
18
|
+
});
|
|
19
|
+
it('extracts first key if multiple', () => {
|
|
20
|
+
expect(extractJiraKey('PROJ-1-PROJ-2')).toBe('PROJ-1');
|
|
21
|
+
});
|
|
22
|
+
it('handles multi-char project prefix', () => {
|
|
23
|
+
expect(extractJiraKey('AB-1')).toBe('AB-1');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ScrollbackBuffer } from '../scrollback-buffer.js';
|
|
3
|
+
function createBuffer() {
|
|
4
|
+
// Don't call start() — avoids timers and filesystem flushes
|
|
5
|
+
return new ScrollbackBuffer();
|
|
6
|
+
}
|
|
7
|
+
describe('ScrollbackBuffer', () => {
|
|
8
|
+
it('getTail returns empty for unknown instance', () => {
|
|
9
|
+
const buf = createBuffer();
|
|
10
|
+
expect(buf.getTail('unknown', 100)).toBe('');
|
|
11
|
+
});
|
|
12
|
+
it('append + getTail returns data', () => {
|
|
13
|
+
const buf = createBuffer();
|
|
14
|
+
buf.append('a', 'hello');
|
|
15
|
+
expect(buf.getTail('a', 5)).toBe('hello');
|
|
16
|
+
});
|
|
17
|
+
it('getTail truncates to requested chars', () => {
|
|
18
|
+
const buf = createBuffer();
|
|
19
|
+
buf.append('a', 'abcdef');
|
|
20
|
+
expect(buf.getTail('a', 3)).toBe('def');
|
|
21
|
+
});
|
|
22
|
+
it('multiple appends concatenate', () => {
|
|
23
|
+
const buf = createBuffer();
|
|
24
|
+
buf.append('a', 'abc');
|
|
25
|
+
buf.append('a', 'def');
|
|
26
|
+
expect(buf.getTail('a', 6)).toBe('abcdef');
|
|
27
|
+
});
|
|
28
|
+
it('getTail with more chars than available returns all', () => {
|
|
29
|
+
const buf = createBuffer();
|
|
30
|
+
buf.append('a', 'hi');
|
|
31
|
+
expect(buf.getTail('a', 100)).toBe('hi');
|
|
32
|
+
});
|
|
33
|
+
it('trims oldest chunks when over max bytes', () => {
|
|
34
|
+
const buf = createBuffer();
|
|
35
|
+
// SCROLLBACK_MAX_BYTES is 512KB. Append chunks that exceed it.
|
|
36
|
+
const chunkSize = 100_000; // 100KB per chunk
|
|
37
|
+
const chunk = 'x'.repeat(chunkSize);
|
|
38
|
+
for (let i = 0; i < 7; i++) {
|
|
39
|
+
buf.append('a', chunk);
|
|
40
|
+
}
|
|
41
|
+
// 700KB appended, should be trimmed to ~512KB
|
|
42
|
+
// getTail should return data but less than 700KB
|
|
43
|
+
const tail = buf.getTail('a', 700_000);
|
|
44
|
+
expect(tail.length).toBeLessThan(700_000);
|
|
45
|
+
expect(tail.length).toBeGreaterThan(0);
|
|
46
|
+
// Recent data should still be present
|
|
47
|
+
expect(tail).toContain('x');
|
|
48
|
+
});
|
|
49
|
+
it('separate instances are independent', () => {
|
|
50
|
+
const buf = createBuffer();
|
|
51
|
+
buf.append('a', 'aaa');
|
|
52
|
+
buf.append('b', 'bbb');
|
|
53
|
+
expect(buf.getTail('a', 10)).toBe('aaa');
|
|
54
|
+
expect(buf.getTail('b', 10)).toBe('bbb');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { detectStateFromTerminal } from '../terminal-state-detector.js';
|
|
3
|
+
describe('detectStateFromTerminal', () => {
|
|
4
|
+
it('returns null for empty string', () => {
|
|
5
|
+
expect(detectStateFromTerminal('')).toBe(null);
|
|
6
|
+
});
|
|
7
|
+
it('returns null for null/undefined', () => {
|
|
8
|
+
expect(detectStateFromTerminal(null)).toBe(null);
|
|
9
|
+
});
|
|
10
|
+
it('detects waiting: y/n prompt', () => {
|
|
11
|
+
expect(detectStateFromTerminal('Allow this? (y/n)')).toBe('waiting');
|
|
12
|
+
});
|
|
13
|
+
it('detects waiting: approve', () => {
|
|
14
|
+
expect(detectStateFromTerminal('Please approve this action')).toBe('waiting');
|
|
15
|
+
});
|
|
16
|
+
it('detects waiting: allow', () => {
|
|
17
|
+
expect(detectStateFromTerminal('Allow tool use?')).toBe('waiting');
|
|
18
|
+
});
|
|
19
|
+
it('detects waiting: yes/no', () => {
|
|
20
|
+
expect(detectStateFromTerminal('Do you accept? yes or no')).toBe('waiting');
|
|
21
|
+
});
|
|
22
|
+
it('detects running: esc to interrupt', () => {
|
|
23
|
+
expect(detectStateFromTerminal('Working... esc to interrupt')).toBe('running');
|
|
24
|
+
});
|
|
25
|
+
it('detects running: spinner char', () => {
|
|
26
|
+
expect(detectStateFromTerminal('Processing ⠋ loading')).toBe('running');
|
|
27
|
+
});
|
|
28
|
+
it('detects idle: > prompt', () => {
|
|
29
|
+
expect(detectStateFromTerminal('some output\n> ')).toBe('idle');
|
|
30
|
+
});
|
|
31
|
+
it('detects idle: $ prompt', () => {
|
|
32
|
+
expect(detectStateFromTerminal('some output\n$ ')).toBe('idle');
|
|
33
|
+
});
|
|
34
|
+
it('returns null for unrecognized text', () => {
|
|
35
|
+
expect(detectStateFromTerminal('just some random text')).toBe(null);
|
|
36
|
+
});
|
|
37
|
+
it('waiting takes priority over running', () => {
|
|
38
|
+
expect(detectStateFromTerminal('approve? (y/n) ⠋')).toBe('waiting');
|
|
39
|
+
});
|
|
40
|
+
it('running takes priority over idle', () => {
|
|
41
|
+
expect(detectStateFromTerminal('⠋ working\n> ')).toBe('running');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -7,7 +7,7 @@ import { detectStateFromTerminal } from './terminal-state-detector.js';
|
|
|
7
7
|
import { createLogger } from './util/logger.js';
|
|
8
8
|
const logger = createLogger('instance-mgr');
|
|
9
9
|
const JIRA_KEY_RE = /([A-Z][A-Z0-9]+-\d+)/;
|
|
10
|
-
function extractJiraKey(branch) {
|
|
10
|
+
export function extractJiraKey(branch) {
|
|
11
11
|
if (!branch)
|
|
12
12
|
return null;
|
|
13
13
|
const m = branch.match(JIRA_KEY_RE);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mergeWithDefaults, DEFAULT_SETTINGS } from '../settings.js';
|
|
3
|
+
describe('mergeWithDefaults', () => {
|
|
4
|
+
it('returns defaults for empty object', () => {
|
|
5
|
+
expect(mergeWithDefaults({})).toEqual(DEFAULT_SETTINGS);
|
|
6
|
+
});
|
|
7
|
+
it('overrides a single field', () => {
|
|
8
|
+
const result = mergeWithDefaults({ terminal: { fontSize: 18 } });
|
|
9
|
+
expect(result.terminal.fontSize).toBe(18);
|
|
10
|
+
expect(result.terminal.cursorStyle).toBe('block');
|
|
11
|
+
expect(result.terminal.scrollbackLines).toBe(5000);
|
|
12
|
+
});
|
|
13
|
+
it('ignores unrecognized top-level sections', () => {
|
|
14
|
+
const result = mergeWithDefaults({ unknown: { x: 1 } });
|
|
15
|
+
expect(result.unknown).toBeUndefined();
|
|
16
|
+
expect(result.terminal).toEqual(DEFAULT_SETTINGS.terminal);
|
|
17
|
+
});
|
|
18
|
+
it('overrides nested jira fields', () => {
|
|
19
|
+
const result = mergeWithDefaults({ jira: { baseUrl: 'https://jira.example.com' } });
|
|
20
|
+
expect(result.jira.baseUrl).toBe('https://jira.example.com');
|
|
21
|
+
expect(result.jira.email).toBe('');
|
|
22
|
+
expect(result.jira.apiToken).toBe('');
|
|
23
|
+
});
|
|
24
|
+
it('does not mutate DEFAULT_SETTINGS', () => {
|
|
25
|
+
const before = structuredClone(DEFAULT_SETTINGS);
|
|
26
|
+
const result = mergeWithDefaults({ terminal: { fontSize: 99 } });
|
|
27
|
+
result.terminal.fontSize = 999;
|
|
28
|
+
expect(DEFAULT_SETTINGS).toEqual(before);
|
|
29
|
+
});
|
|
30
|
+
it('handles non-object section values gracefully', () => {
|
|
31
|
+
const result = mergeWithDefaults({ terminal: 'invalid' });
|
|
32
|
+
expect(result.terminal).toEqual(DEFAULT_SETTINGS.terminal);
|
|
33
|
+
});
|
|
34
|
+
});
|