fix(mosaic): bound promotion transport delivery
This commit is contained in:
@@ -17,10 +17,11 @@ function result(stdout = '', exitCode = 0, stderr = ''): CommandResult {
|
||||
}
|
||||
|
||||
describe('TmuxPromotionTransport', () => {
|
||||
it('resolves the exact roster seat and sends through the maintained tmux sender', async () => {
|
||||
it('resolves the exact roster seat and sends the registered command literally', async () => {
|
||||
const runner = vi
|
||||
.fn<CommandRunner>()
|
||||
.mockResolvedValueOnce(result('1234 claude 0 0 0 0\n'))
|
||||
.mockResolvedValueOnce(result())
|
||||
.mockResolvedValueOnce(result());
|
||||
const environmentReader = vi.fn(async () => `MOSAIC_LEASE_SESSION_ID=${sessionId}\0`);
|
||||
const transport = new TmuxPromotionTransport({
|
||||
@@ -39,15 +40,22 @@ describe('TmuxPromotionTransport', () => {
|
||||
sessionId,
|
||||
});
|
||||
expect(environmentReader).toHaveBeenCalledWith(1234);
|
||||
expect(runner).toHaveBeenNthCalledWith(2, '/mosaic/tools/tmux/agent-send.sh', [
|
||||
expect(runner).toHaveBeenNthCalledWith(2, 'tmux', [
|
||||
'-L',
|
||||
'mosaic-fleet',
|
||||
'-S',
|
||||
expect.stringMatching(/:operator$/),
|
||||
'-s',
|
||||
'claude-seat',
|
||||
'-m',
|
||||
'send-keys',
|
||||
'-t',
|
||||
'=claude-seat:0.0',
|
||||
'-l',
|
||||
'/mosaic-promote',
|
||||
]);
|
||||
expect(runner).toHaveBeenNthCalledWith(3, 'tmux', [
|
||||
'-L',
|
||||
'mosaic-fleet',
|
||||
'send-keys',
|
||||
'-t',
|
||||
'=claude-seat:0.0',
|
||||
'Enter',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import {
|
||||
buildAgentSendCommand,
|
||||
buildTmuxListPanesCommand,
|
||||
getDefaultOperatorSourceLabel,
|
||||
getRosterAgent,
|
||||
parseTmuxListPanes,
|
||||
resolveFleetPaths,
|
||||
@@ -10,11 +8,13 @@ import {
|
||||
type CommandRunner,
|
||||
type FleetRoster,
|
||||
RUNTIME_ACCEPTABLE_COMMANDS,
|
||||
socketArgs,
|
||||
} from '../commands/fleet.js';
|
||||
import { loadFleetRoster } from './fleet-roster-v1.js';
|
||||
|
||||
const PROMOTION_COMMAND = '/mosaic-promote';
|
||||
const SESSION_ID_PATTERN = /^[a-f0-9]{64}$/;
|
||||
const TRANSPORT_COMMAND_TIMEOUT_MS = 5_000;
|
||||
|
||||
export interface PromotionTarget {
|
||||
bundle: string;
|
||||
@@ -81,16 +81,34 @@ export class TmuxPromotionTransport implements PromotionTransport {
|
||||
}
|
||||
|
||||
async sendPromotion(target: PromotionTarget): Promise<void> {
|
||||
const command = buildAgentSendCommand(
|
||||
resolveFleetPaths(this.options.mosaicHome),
|
||||
target.seat,
|
||||
const targetPane = `=${target.seat}:0.0`;
|
||||
const socketName = target.bundle === 'default' ? '' : target.bundle;
|
||||
// Registered Claude commands must arrive as their exact literal text; the
|
||||
// fleet agent sender prepends an identity envelope, so it cannot carry this
|
||||
// command without preventing the UserPromptSubmit matcher from recognizing it.
|
||||
await this.runPromotionCommand([
|
||||
'tmux',
|
||||
...socketArgs(socketName),
|
||||
'send-keys',
|
||||
'-t',
|
||||
targetPane,
|
||||
'-l',
|
||||
PROMOTION_COMMAND,
|
||||
target.bundle === 'default' ? '' : target.bundle,
|
||||
getDefaultOperatorSourceLabel(),
|
||||
);
|
||||
]);
|
||||
await this.runPromotionCommand([
|
||||
'tmux',
|
||||
...socketArgs(socketName),
|
||||
'send-keys',
|
||||
'-t',
|
||||
targetPane,
|
||||
'Enter',
|
||||
]);
|
||||
}
|
||||
|
||||
private async runPromotionCommand(command: string[]): Promise<void> {
|
||||
const result = await this.run(command);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Promotion command delivery failed: ${target.seat}.`);
|
||||
throw new Error('Promotion command delivery failed.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,10 +117,28 @@ export class TmuxPromotionTransport implements PromotionTransport {
|
||||
if (executable === undefined) {
|
||||
throw new Error('Promotion transport command is empty.');
|
||||
}
|
||||
return this.options.runner(executable, args);
|
||||
return await withTimeout(this.options.runner(executable, args), TRANSPORT_COMMAND_TIMEOUT_MS);
|
||||
}
|
||||
}
|
||||
|
||||
function withTimeout<T>(operation: Promise<T>, timeoutMs: number): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error(`Promotion transport command timed out after ${timeoutMs}ms.`));
|
||||
}, timeoutMs);
|
||||
void operation.then(
|
||||
(value) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(value);
|
||||
},
|
||||
(error: unknown) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function readPaneEnvironment(pid: number): Promise<string> {
|
||||
return readFile(`/proc/${pid}/environ`, 'utf8');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user