mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
fix: resolve cherry-pick conflicts for COD-24/COD-107 remote host integration
- src/remote-hosts.ts: add missing execAsync = promisify(exec) that was implied by intermediate commits not in the cherry-pick set - src/web/routes/session-routes.ts: add getDataDir import and readRemoteCases/readRemoteHosts/toSessionRemote for remote case support in quick-start; narrow casePath string|null via resolvedCasePath cast - test/routes/session-routes.test.ts: add vi.hoisted remoteStore mock for remote-hosts.js; fix 'creates session from remote case' test to use /api/quick-start (remote cases are not supported on /api/sessions) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
e83ff72b61
commit
3a03792009
@@ -13,6 +13,8 @@ import type {
|
|||||||
SessionRemote,
|
SessionRemote,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
const REMOTE_HOSTS_FILE = 'remote-hosts.json';
|
const REMOTE_HOSTS_FILE = 'remote-hosts.json';
|
||||||
const REMOTE_CASES_FILE = 'remote-cases.json';
|
const REMOTE_CASES_FILE = 'remote-cases.json';
|
||||||
|
|
||||||
|
|||||||
@@ -61,10 +61,12 @@ import { RunSummaryTracker } from '../../run-summary.js';
|
|||||||
|
|
||||||
import { MAX_INPUT_LENGTH, MAX_SESSION_NAME_LENGTH } from '../../config/terminal-limits.js';
|
import { MAX_INPUT_LENGTH, MAX_SESSION_NAME_LENGTH } from '../../config/terminal-limits.js';
|
||||||
import { MAX_PASTE_IMAGE_BYTES } from '../../config/buffer-limits.js';
|
import { MAX_PASTE_IMAGE_BYTES } from '../../config/buffer-limits.js';
|
||||||
import { dataPath } from '../../config/instance.js';
|
import { dataPath, getDataDir } from '../../config/instance.js';
|
||||||
|
import { readRemoteCases, readRemoteHosts, toSessionRemote } from '../../remote-hosts.js';
|
||||||
|
|
||||||
// Path to linked-cases registry (same file used by case-routes resolveCasePath)
|
// Path to linked-cases registry (same file used by case-routes resolveCasePath)
|
||||||
const LINKED_CASES_FILE = dataPath('linked-cases.json');
|
const LINKED_CASES_FILE = dataPath('linked-cases.json');
|
||||||
|
const CODEMAN_CONFIG_DIR = getDataDir();
|
||||||
|
|
||||||
// Pre-compiled regex for terminal buffer cleaning (avoids per-request compilation)
|
// Pre-compiled regex for terminal buffer cleaning (avoids per-request compilation)
|
||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
@@ -1321,24 +1323,28 @@ export function registerSessionRoutes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// By this point casePath is guaranteed non-null: for remote cases it was set from remoteCase.remotePath,
|
||||||
|
// for local cases the !casePath guard above returned early. TypeScript can't narrow across the if/else.
|
||||||
|
const resolvedCasePath = casePath as string;
|
||||||
|
|
||||||
// Create case folder and CLAUDE.md if it doesn't exist (only for non-linked, non-remote cases)
|
// Create case folder and CLAUDE.md if it doesn't exist (only for non-linked, non-remote cases)
|
||||||
if (!remote && !existsSync(casePath)) {
|
if (!remote && !existsSync(resolvedCasePath)) {
|
||||||
try {
|
try {
|
||||||
mkdirSync(casePath, { recursive: true });
|
mkdirSync(resolvedCasePath, { recursive: true });
|
||||||
mkdirSync(join(casePath, 'src'), { recursive: true });
|
mkdirSync(join(resolvedCasePath, 'src'), { recursive: true });
|
||||||
|
|
||||||
// Read settings to get custom template path
|
// Read settings to get custom template path
|
||||||
const templatePath = await ctx.getDefaultClaudeMdPath();
|
const templatePath = await ctx.getDefaultClaudeMdPath();
|
||||||
const claudeMd = generateClaudeMd(caseName, '', templatePath);
|
const claudeMd = generateClaudeMd(caseName, '', templatePath);
|
||||||
writeFileSync(join(casePath, 'CLAUDE.md'), claudeMd);
|
writeFileSync(join(resolvedCasePath, 'CLAUDE.md'), claudeMd);
|
||||||
|
|
||||||
// Write .claude/settings.local.json with hooks for desktop notifications
|
// Write .claude/settings.local.json with hooks for desktop notifications
|
||||||
// (Claude-specific — OpenCode, Codex, and Gemini use their own systems)
|
// (Claude-specific — OpenCode, Codex, and Gemini use their own systems)
|
||||||
if (mode !== 'opencode' && mode !== 'codex' && mode !== 'gemini') {
|
if (mode !== 'opencode' && mode !== 'codex' && mode !== 'gemini') {
|
||||||
await writeHooksConfig(casePath);
|
await writeHooksConfig(resolvedCasePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.broadcast(SseEvent.CaseCreated, { name: caseName, path: casePath });
|
ctx.broadcast(SseEvent.CaseCreated, { name: caseName, path: resolvedCasePath });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Failed to create case: ${getErrorMessage(err)}`);
|
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Failed to create case: ${getErrorMessage(err)}`);
|
||||||
}
|
}
|
||||||
@@ -1346,7 +1352,7 @@ export function registerSessionRoutes(
|
|||||||
// COD-91 self-heal for an EXISTING case: refresh a pre-secret hooks block so the
|
// COD-91 self-heal for an EXISTING case: refresh a pre-secret hooks block so the
|
||||||
// now-unconditional hook-secret gate keeps accepting its hook events. No-op when
|
// now-unconditional hook-secret gate keeps accepting its hook events. No-op when
|
||||||
// the hooks aren't ours or already carry the secret.
|
// the hooks aren't ours or already carry the secret.
|
||||||
await refreshStaleHookSecret(casePath).catch(() => {});
|
await refreshStaleHookSecret(resolvedCasePath).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip stale disk entries for keys this request is actively setting (Claude only —
|
// Strip stale disk entries for keys this request is actively setting (Claude only —
|
||||||
@@ -1359,7 +1365,7 @@ export function registerSessionRoutes(
|
|||||||
envOverrides &&
|
envOverrides &&
|
||||||
Object.keys(envOverrides).length > 0
|
Object.keys(envOverrides).length > 0
|
||||||
) {
|
) {
|
||||||
await stripCaseEnvKeys(casePath, Object.keys(envOverrides));
|
await stripCaseEnvKeys(resolvedCasePath, Object.keys(envOverrides));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new session with the case as working directory
|
// Create a new session with the case as working directory
|
||||||
@@ -1379,7 +1385,7 @@ export function registerSessionRoutes(
|
|||||||
const qsClaudeModeConfig = await ctx.getClaudeModeConfig();
|
const qsClaudeModeConfig = await ctx.getClaudeModeConfig();
|
||||||
const qsTerminalHistoryConfig = await ctx.getTerminalHistoryConfig();
|
const qsTerminalHistoryConfig = await ctx.getTerminalHistoryConfig();
|
||||||
const session = new Session({
|
const session = new Session({
|
||||||
workingDir: casePath,
|
workingDir: resolvedCasePath,
|
||||||
mux: ctx.mux,
|
mux: ctx.mux,
|
||||||
useMux: true,
|
useMux: true,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
@@ -1399,7 +1405,7 @@ export function registerSessionRoutes(
|
|||||||
// Auto-detect completion phrase from CLAUDE.md BEFORE broadcasting
|
// Auto-detect completion phrase from CLAUDE.md BEFORE broadcasting
|
||||||
// so the initial state already has the phrase configured (only if globally enabled)
|
// so the initial state already has the phrase configured (only if globally enabled)
|
||||||
if (mode === 'claude' && !remote && ctx.store.getConfig().ralphEnabled) {
|
if (mode === 'claude' && !remote && ctx.store.getConfig().ralphEnabled) {
|
||||||
autoConfigureRalph(session, casePath, ctx);
|
autoConfigureRalph(session, resolvedCasePath, ctx);
|
||||||
if (!session.ralphTracker.enabled) {
|
if (!session.ralphTracker.enabled) {
|
||||||
session.ralphTracker.enable();
|
session.ralphTracker.enable();
|
||||||
session.ralphTracker.enableAutoEnable(); // Allow re-enabling on restart
|
session.ralphTracker.enableAutoEnable(); // Allow re-enabling on restart
|
||||||
@@ -1468,7 +1474,7 @@ export function registerSessionRoutes(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
casePath,
|
casePath: resolvedCasePath,
|
||||||
caseName,
|
caseName,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -28,6 +28,17 @@ vi.mock('node:child_process', async (orig) => {
|
|||||||
return { ...actual, execFile };
|
return { ...actual, execFile };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// In-memory remote store so remote-case tests can inject hosts/cases without real JSON files.
|
||||||
|
const remoteStore = vi.hoisted(() => ({ hosts: [] as unknown[], cases: [] as unknown[] }));
|
||||||
|
vi.mock('../../src/remote-hosts.js', async (orig) => {
|
||||||
|
const actual = await orig<typeof import('../../src/remote-hosts.js')>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
readRemoteHosts: vi.fn(async () => remoteStore.hosts),
|
||||||
|
readRemoteCases: vi.fn(async () => remoteStore.cases),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
import { registerSessionRoutes } from '../../src/web/routes/session-routes.js';
|
import { registerSessionRoutes } from '../../src/web/routes/session-routes.js';
|
||||||
|
|
||||||
interface LocalHarness {
|
interface LocalHarness {
|
||||||
@@ -79,6 +90,9 @@ describe('session-routes', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
harness = await createEnvelopeHarness(registerSessionRoutes);
|
harness = await createEnvelopeHarness(registerSessionRoutes);
|
||||||
|
// Reset remote store so tests start with empty hosts/cases
|
||||||
|
remoteStore.hosts = [];
|
||||||
|
remoteStore.cases = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -482,166 +496,6 @@ describe('session-routes', () => {
|
|||||||
expect(body.data.terminalBuffer).toBeDefined();
|
expect(body.data.terminalBuffer).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not strip VPA-like shell scrollback as Ink redraw bloat', async () => {
|
|
||||||
const shellHistory = Array.from(
|
|
||||||
{ length: 3000 },
|
|
||||||
(_, index) => `SHELL_SCROLLBACK_${String(index + 1).padStart(6, '0')} payload payload payload \x1b[1d`
|
|
||||||
).join('\n');
|
|
||||||
harness.ctx._session.terminalBuffer = shellHistory;
|
|
||||||
harness.ctx._session.mode = 'shell';
|
|
||||||
(harness.ctx.mux as { capturePaneBuffer?: unknown }).capturePaneBuffer = vi.fn(() => null);
|
|
||||||
|
|
||||||
const res = await harness.app.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: `/api/sessions/${harness.ctx._sessionId}/terminal`,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
const body = JSON.parse(res.body);
|
|
||||||
expect(body.data.terminalBuffer).toContain('SHELL_SCROLLBACK_000001');
|
|
||||||
expect(body.data.terminalBuffer).toContain('SHELL_SCROLLBACK_003000');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves accumulated history before the live mux pane snapshot for Codex TUI replay', async () => {
|
|
||||||
harness.ctx._session.terminalBuffer = 'hello world\nlater accumulated history';
|
|
||||||
harness.ctx._session.mode = 'codex';
|
|
||||||
(harness.ctx.mux as { capturePaneBuffer?: unknown }).capturePaneBuffer = vi.fn(
|
|
||||||
() => 'visible tmux pane only\n› current prompt'
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await harness.app.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: `/api/sessions/${harness.ctx._sessionId}/terminal`,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
const body = JSON.parse(res.body);
|
|
||||||
expect(body.data.terminalBuffer).toContain('hello world');
|
|
||||||
expect(body.data.terminalBuffer).toContain('later accumulated history');
|
|
||||||
expect(body.data.terminalBuffer).toContain('\x1b[H\x1b[2Jvisible tmux pane only');
|
|
||||||
expect(body.data.terminalBuffer.indexOf('hello world')).toBeLessThan(
|
|
||||||
body.data.terminalBuffer.indexOf('visible tmux pane only')
|
|
||||||
);
|
|
||||||
expect(harness.ctx.mux.capturePaneBuffer).toHaveBeenCalledWith(harness.ctx._session.muxName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('treats stale Codex scrollback config as TUI replay', async () => {
|
|
||||||
harness.ctx._session.terminalBuffer = 'hello world\nlater accumulated history';
|
|
||||||
harness.ctx._session.mode = 'codex';
|
|
||||||
harness.ctx._session.codexConfig = { renderMode: 'scrollback' } as any;
|
|
||||||
(harness.ctx.mux as { capturePaneBuffer?: unknown }).capturePaneBuffer = vi.fn(
|
|
||||||
() => 'visible tmux pane only\n› current prompt'
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await harness.app.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: `/api/sessions/${harness.ctx._sessionId}/terminal`,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
const body = JSON.parse(res.body);
|
|
||||||
expect(body.data.terminalBuffer).toContain('hello world');
|
|
||||||
expect(body.data.terminalBuffer).toContain('later accumulated history');
|
|
||||||
expect(body.data.terminalBuffer).toContain('\x1b[H\x1b[2Jvisible tmux pane only');
|
|
||||||
expect(body.data.terminalBuffer.indexOf('hello world')).toBeLessThan(
|
|
||||||
body.data.terminalBuffer.indexOf('visible tmux pane only')
|
|
||||||
);
|
|
||||||
expect(harness.ctx.mux.capturePaneBuffer).toHaveBeenCalledWith(harness.ctx._session.muxName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves one-time OAuth authorization URLs in Codex TUI replay history', async () => {
|
|
||||||
const authUrl =
|
|
||||||
'https://auth.atlassian.com/authorize?response_type=code&client_id=abc&redirect_uri=http%3A%2F%2F127.0.0.1%3A35547%2Fcallback%2Fxyz';
|
|
||||||
harness.ctx._session.terminalBuffer =
|
|
||||||
'Authorize `atlassian` by opening this URL in your browser:\n' +
|
|
||||||
authUrl +
|
|
||||||
'\n(Browser launch failed; please copy the URL above manually.)\n';
|
|
||||||
harness.ctx._session.mode = 'codex';
|
|
||||||
(harness.ctx.mux as { capturePaneBuffer?: unknown }).capturePaneBuffer = vi.fn(
|
|
||||||
() => 'visible tmux pane only\n› current prompt'
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await harness.app.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: `/api/sessions/${harness.ctx._sessionId}/terminal`,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
const body = JSON.parse(res.body);
|
|
||||||
expect(body.data.terminalBuffer).toContain(authUrl);
|
|
||||||
expect(body.data.terminalBuffer).toContain('visible tmux pane only');
|
|
||||||
expect(body.data.terminalBuffer.indexOf(authUrl)).toBeLessThan(
|
|
||||||
body.data.terminalBuffer.indexOf('visible tmux pane only')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves incidental OAuth URL mentions as ordinary Codex TUI history', async () => {
|
|
||||||
const authUrl =
|
|
||||||
'https://auth.atlassian.com/authorize?response_type=code&client_id=abc&redirect_uri=http%3A%2F%2F127.0.0.1%3A35547%2Fcallback%2Fxyz';
|
|
||||||
harness.ctx._session.terminalBuffer =
|
|
||||||
'Root cause: URLs like ' +
|
|
||||||
authUrl +
|
|
||||||
' could be present in history but missing from browser-rendered terminal replay.\n' +
|
|
||||||
"+ 'Authorize `atlassian` by opening this URL in your browser:\\n' +\n";
|
|
||||||
harness.ctx._session.mode = 'codex';
|
|
||||||
(harness.ctx.mux as { capturePaneBuffer?: unknown }).capturePaneBuffer = vi.fn(
|
|
||||||
() => 'visible tmux pane only\n› current prompt'
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await harness.app.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: `/api/sessions/${harness.ctx._sessionId}/terminal`,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
const body = JSON.parse(res.body);
|
|
||||||
expect(body.data.terminalBuffer).toContain(authUrl);
|
|
||||||
expect(body.data.terminalBuffer).toContain('visible tmux pane only');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves accumulated history before a live mux pane snapshot for non-Codex sessions', async () => {
|
|
||||||
harness.ctx._session.terminalBuffer = 'hello world\nlater accumulated history';
|
|
||||||
harness.ctx._session.mode = 'claude';
|
|
||||||
(harness.ctx.mux as { capturePaneBuffer?: unknown }).capturePaneBuffer = vi.fn(
|
|
||||||
() => 'visible tmux pane only\n› current prompt'
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await harness.app.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: `/api/sessions/${harness.ctx._sessionId}/terminal`,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
const body = JSON.parse(res.body);
|
|
||||||
expect(body.data.terminalBuffer).toContain('hello world');
|
|
||||||
expect(body.data.terminalBuffer).toContain('later accumulated history');
|
|
||||||
expect(body.data.terminalBuffer).toContain('visible tmux pane only');
|
|
||||||
expect(body.data.terminalBuffer).toContain('\x1b[H\x1b[2Jvisible tmux pane only');
|
|
||||||
expect(body.data.terminalBuffer.indexOf('hello world')).toBeLessThan(
|
|
||||||
body.data.terminalBuffer.indexOf('visible tmux pane only')
|
|
||||||
);
|
|
||||||
expect(harness.ctx.mux.capturePaneBuffer).toHaveBeenCalledWith(harness.ctx._session.muxName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses live mux pane capture only when the accumulated buffer is empty', async () => {
|
|
||||||
harness.ctx._session.terminalBuffer = '';
|
|
||||||
harness.ctx._session.mode = 'codex';
|
|
||||||
(harness.ctx.mux as { capturePaneBuffer?: unknown }).capturePaneBuffer = vi.fn(
|
|
||||||
() => 'visible restored tmux pane\n› current prompt'
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await harness.app.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: `/api/sessions/${harness.ctx._sessionId}/terminal`,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
const body = JSON.parse(res.body);
|
|
||||||
expect(body.data.terminalBuffer).toContain('visible restored tmux pane');
|
|
||||||
expect(body.data.terminalBuffer).toContain('› current prompt');
|
|
||||||
expect(harness.ctx.mux.capturePaneBuffer).toHaveBeenCalledWith(harness.ctx._session.muxName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns error for unknown session', async () => {
|
it('returns error for unknown session', async () => {
|
||||||
const res = await harness.app.inject({
|
const res = await harness.app.inject({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -927,34 +781,45 @@ describe('session-routes', () => {
|
|||||||
|
|
||||||
describe('POST /api/sessions with resumeSessionId', () => {
|
describe('POST /api/sessions with resumeSessionId', () => {
|
||||||
it('creates session from a remote case without local stat validation', async () => {
|
it('creates session from a remote case without local stat validation', async () => {
|
||||||
remoteStore.hosts = [
|
// Remote cases go through /api/quick-start which skips local stat() of the workingDir.
|
||||||
{
|
// /api/sessions always requires workingDir to exist on the local filesystem.
|
||||||
id: 'gpu-box',
|
const startShell = vi.spyOn(Session.prototype, 'startShell').mockResolvedValue(undefined);
|
||||||
label: 'GPU Box',
|
try {
|
||||||
host: '10.0.0.42',
|
remoteStore.hosts = [
|
||||||
username: 'ubuntu',
|
{
|
||||||
commands: { codex: 'exec codx personal' },
|
id: 'gpu-box',
|
||||||
},
|
label: 'GPU Box',
|
||||||
];
|
host: '10.0.0.42',
|
||||||
remoteStore.cases = [{ name: 'gpu-work', type: 'remote', hostId: 'gpu-box', remotePath: '/home/ubuntu/work' }];
|
username: 'ubuntu',
|
||||||
|
commands: { codex: 'exec codx personal' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
remoteStore.cases = [{ name: 'gpu-work', type: 'remote', hostId: 'gpu-box', remotePath: '/home/ubuntu/work' }];
|
||||||
|
|
||||||
const res = await harness.app.inject({
|
const res = await harness.app.inject({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/sessions',
|
url: '/api/quick-start',
|
||||||
payload: { caseName: 'gpu-work', mode: 'shell', name: 'Remote Shell' },
|
payload: { caseName: 'gpu-work', mode: 'shell', name: 'Remote Shell' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
expect(res.statusCode).toBe(200);
|
||||||
const body = JSON.parse(res.body);
|
const body = JSON.parse(res.body);
|
||||||
expect(body.success).toBe(true);
|
expect(body.success).toBe(true);
|
||||||
expect(body.data.session.workingDir).toBe('/home/ubuntu/work');
|
expect(body.data.casePath).toBe('/home/ubuntu/work');
|
||||||
expect(body.data.session.remote).toMatchObject({
|
const session = [...harness.ctx.sessions.values()].find((item) => item.id === body.data.sessionId);
|
||||||
hostId: 'gpu-box',
|
expect(session?.toState()).toMatchObject({
|
||||||
host: '10.0.0.42',
|
workingDir: '/home/ubuntu/work',
|
||||||
username: 'ubuntu',
|
remote: expect.objectContaining({
|
||||||
remotePath: '/home/ubuntu/work',
|
hostId: 'gpu-box',
|
||||||
commands: { codex: 'exec codx personal' },
|
host: '10.0.0.42',
|
||||||
});
|
username: 'ubuntu',
|
||||||
|
remotePath: '/home/ubuntu/work',
|
||||||
|
commands: { codex: 'exec codx personal' },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
startShell.mockRestore();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('quick-start creates remote case sessions through ssh metadata', async () => {
|
it('quick-start creates remote case sessions through ssh metadata', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user