mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 07:29:42 +02:00
Merge master into refactor/envoverrides-tmux-export
Resolved conflict in src/web/public/session-ui.js by keeping this PR's buildEnvOverrides() helper — it already covers both CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS (this PR) and CLAUDE_CODE_EFFORT_LEVEL (added in #73), so the master-side inline block is fully replaced. Also fixed test/session-manager.test.ts MockSession to add a getEnvOverridesForPersist() stub — without it, SessionManager.updateSessionState's new call breaks 19 tests with "TypeError: session.getEnvOverridesForPersist is not a function". Verified: typecheck, lint, format:check, build, and test/{session-manager,session-state,tmux-manager,tmux-restart-recovery}.test.ts all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,10 @@ vi.mock('../src/session.js', () => {
|
||||
};
|
||||
}
|
||||
|
||||
getEnvOverridesForPersist() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getOutput() {
|
||||
return 'mock output';
|
||||
}
|
||||
@@ -147,19 +151,14 @@ describe('SessionManager', () => {
|
||||
it('should persist session to store', async () => {
|
||||
const session = await manager.createSession('/tmp/test');
|
||||
|
||||
expect(mockState.store.setSession).toHaveBeenCalledWith(
|
||||
session.id,
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(mockState.store.setSession).toHaveBeenCalledWith(session.id, expect.any(Object));
|
||||
});
|
||||
|
||||
it('should throw when max sessions reached', async () => {
|
||||
mockState.store.state.config.maxConcurrentSessions = 1;
|
||||
await manager.createSession('/tmp/test1');
|
||||
|
||||
await expect(manager.createSession('/tmp/test2')).rejects.toThrow(
|
||||
/Maximum concurrent sessions/
|
||||
);
|
||||
await expect(manager.createSession('/tmp/test2')).rejects.toThrow(/Maximum concurrent sessions/);
|
||||
});
|
||||
|
||||
it('should forward session output events', async () => {
|
||||
@@ -325,9 +324,7 @@ describe('SessionManager', () => {
|
||||
|
||||
describe('sendToSession', () => {
|
||||
it('should throw for non-existent session', async () => {
|
||||
await expect(manager.sendToSession('non-existent', 'test')).rejects.toThrow(
|
||||
/Session non-existent not found/
|
||||
);
|
||||
await expect(manager.sendToSession('non-existent', 'test')).rejects.toThrow(/Session non-existent not found/);
|
||||
});
|
||||
|
||||
it('should send input to session', async () => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { TmuxManager } from '../src/tmux-manager.js';
|
||||
import { TmuxManager, parsePaneList } from '../src/tmux-manager.js';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
// ============================================================================
|
||||
@@ -287,13 +287,27 @@ describe('TmuxManager (unit)', () => {
|
||||
});
|
||||
|
||||
it('should update respawn config', () => {
|
||||
const config = { enabled: true, idleTimeoutMs: 5000, updatePrompt: 'test', interStepDelayMs: 1000, sendClear: true, sendInit: true };
|
||||
const config = {
|
||||
enabled: true,
|
||||
idleTimeoutMs: 5000,
|
||||
updatePrompt: 'test',
|
||||
interStepDelayMs: 1000,
|
||||
sendClear: true,
|
||||
sendInit: true,
|
||||
};
|
||||
manager.updateRespawnConfig('meta-test', config);
|
||||
expect(manager.getSession('meta-test')?.respawnConfig).toEqual(config);
|
||||
});
|
||||
|
||||
it('should clear respawn config', () => {
|
||||
manager.updateRespawnConfig('meta-test', { enabled: true, idleTimeoutMs: 5000, updatePrompt: 'test', interStepDelayMs: 1000, sendClear: true, sendInit: true });
|
||||
manager.updateRespawnConfig('meta-test', {
|
||||
enabled: true,
|
||||
idleTimeoutMs: 5000,
|
||||
updatePrompt: 'test',
|
||||
interStepDelayMs: 1000,
|
||||
sendClear: true,
|
||||
sendInit: true,
|
||||
});
|
||||
manager.clearRespawnConfig('meta-test');
|
||||
expect(manager.getSession('meta-test')?.respawnConfig).toBeUndefined();
|
||||
});
|
||||
@@ -327,8 +341,8 @@ describe('TmuxManager (unit)', () => {
|
||||
|
||||
const sessions = manager.getSessions();
|
||||
expect(sessions).toHaveLength(2);
|
||||
expect(sessions.map(s => s.sessionId)).toContain('s1');
|
||||
expect(sessions.map(s => s.sessionId)).toContain('s2');
|
||||
expect(sessions.map((s) => s.sessionId)).toContain('s1');
|
||||
expect(sessions.map((s) => s.sessionId)).toContain('s2');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -342,3 +356,68 @@ describe('TmuxManager (unit)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Parser Tests — locks in the '|' separator contract for `tmux list-panes -F`
|
||||
// output, guarding against regressions in non-tty execution contexts where
|
||||
// `\t` in tmux FORMAT strings can be emitted as the literal two characters
|
||||
// `\` + `t` instead of a tab byte (launchd, systemd without TTYPath, docker
|
||||
// exec without TTY). See PR #71.
|
||||
// ============================================================================
|
||||
|
||||
describe('parsePaneList', () => {
|
||||
it('parses well-formed output into name → pid', () => {
|
||||
const out = 'codeman-aaaa|1234\ncodeman-bbbb|5678\nclaudeman-cccc|9999';
|
||||
const result = parsePaneList(out);
|
||||
expect(result.size).toBe(3);
|
||||
expect(result.get('codeman-aaaa')).toBe(1234);
|
||||
expect(result.get('codeman-bbbb')).toBe(5678);
|
||||
expect(result.get('claudeman-cccc')).toBe(9999);
|
||||
});
|
||||
|
||||
it('returns an empty map for empty output', () => {
|
||||
expect(parsePaneList('').size).toBe(0);
|
||||
});
|
||||
|
||||
it('skips blank lines', () => {
|
||||
const result = parsePaneList('\ncodeman-aaaa|100\n\n\ncodeman-bbbb|200\n');
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get('codeman-aaaa')).toBe(100);
|
||||
expect(result.get('codeman-bbbb')).toBe(200);
|
||||
});
|
||||
|
||||
it('skips lines without the separator', () => {
|
||||
const result = parsePaneList('codeman-aaaa 1234\ncodeman-bbbb|5678');
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.get('codeman-bbbb')).toBe(5678);
|
||||
});
|
||||
|
||||
it('skips lines with a non-numeric pid', () => {
|
||||
const result = parsePaneList('codeman-aaaa|notapid\ncodeman-bbbb|5678');
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.get('codeman-bbbb')).toBe(5678);
|
||||
});
|
||||
|
||||
it('skips lines with an empty session name', () => {
|
||||
const result = parsePaneList('|1234\ncodeman-bbbb|5678');
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.get('codeman-bbbb')).toBe(5678);
|
||||
});
|
||||
|
||||
it('treats a literal backslash-t in input as part of the session name, not a delimiter', () => {
|
||||
// Reproduces the launchd/systemd regression: under non-tty contexts tmux
|
||||
// was emitting FORMAT '\t' as the two characters `\` + `t` rather than a
|
||||
// tab byte. With the '|' separator, such literals must not be silently
|
||||
// treated as a delimiter — the line is discarded because there is no '|'.
|
||||
const literalBackslashT = 'codeman-aaaa\\t1234';
|
||||
const result = parsePaneList(literalBackslashT);
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
|
||||
it('splits on the first separator only', () => {
|
||||
// Numeric trailing junk after the pid is tolerated by parseInt — proves
|
||||
// that splitting on the first '|' leaves the pid extractable even if a
|
||||
// future tmux ever appended extra fields.
|
||||
const result = parsePaneList('codeman-aaaa|1234|extra-field');
|
||||
expect(result.get('codeman-aaaa')).toBe(1234);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user