Files
stack/packages/mosaic/src/commands/promote.spec.ts
T

291 lines
8.6 KiB
TypeScript

import { Command } from 'commander';
import { describe, expect, it, vi } from 'vitest';
import type { FleetRoster } from './fleet.js';
import { TmuxPromotionTransport } from '../fleet/promotion-transport.js';
import {
promoteSeat,
registerPromoteCommand,
type PromotionBreadcrumbStore,
type PromotionResult,
type PromotionTransport,
} from './promote.js';
const attemptId = 'a'.repeat(64);
const target = {
bundle: 'local',
seat: 'claude-seat',
sessionId: 'b'.repeat(64),
};
function transport(): PromotionTransport {
return {
resolve: vi.fn(async () => target),
sendPromotion: vi.fn(async () => {}),
};
}
describe('mosaic promote', () => {
it('accepts only a fresh result correlated to this attempt', async () => {
const promotionTransport = transport();
const store: PromotionBreadcrumbStore = {
readAttemptId: vi.fn().mockResolvedValueOnce(null).mockResolvedValue(attemptId),
readResult: vi
.fn()
.mockResolvedValueOnce({
attempt_id: 'c'.repeat(64),
expires_at_wallclock: 4_600,
reason: null,
session_id: target.sessionId,
ts: 1_001,
verified: true,
})
.mockResolvedValueOnce({
attempt_id: attemptId,
expires_at_wallclock: 4_600,
reason: null,
session_id: target.sessionId,
ts: 1_001,
verified: true,
}),
};
const result = await promoteSeat('claude-seat', {
clock: () => 1_000,
sleep: async () => {},
store,
timeoutMs: 1,
transport: promotionTransport,
});
expect(result).toEqual({
bundle: 'local',
expiresAtWallclock: 4_600,
reason: null,
seat: 'claude-seat',
sessionId: 'b'.repeat(64),
status: 'VERIFIED',
});
expect(promotionTransport.sendPromotion).toHaveBeenCalledWith(target);
expect(store.readResult).toHaveBeenCalledTimes(2);
});
it('accepts a fresh result for this session when completion consumed the pending nonce', async () => {
const promotionTransport = transport();
let now = 1_000;
const store: PromotionBreadcrumbStore = {
readAttemptId: vi.fn(async () => null),
readResult: vi.fn(async () => ({
attempt_id: attemptId,
expires_at_wallclock: 4_600,
reason: null,
session_id: target.sessionId,
ts: 1_001,
verified: true,
})),
};
const result = await promoteSeat('claude-seat', {
clock: () => now,
sleep: async () => {
now += 10;
},
store,
timeoutMs: 10,
transport: promotionTransport,
});
expect(result.status).toBe('VERIFIED');
});
it('returns UNVERIFIED within the command bound when a tmux runner wedges', async () => {
vi.useFakeTimers();
const roster: FleetRoster = {
agents: [{ className: 'worker', name: 'claude-seat', runtime: 'claude' }],
defaults: { workingDirectory: '~/src' },
runtimes: {},
tmux: { holderSession: '_holder', socketName: 'mosaic-fleet' },
transport: 'tmux',
version: 1,
};
const promotionTransport = new TmuxPromotionTransport({
mosaicHome: '/mosaic',
rosterLoader: async () => roster,
runner: async () => new Promise(() => {}),
});
const store: PromotionBreadcrumbStore = {
readAttemptId: vi.fn(async () => null),
readResult: vi.fn(async () => null),
};
try {
let observedResult: PromotionResult | undefined;
void promoteSeat('claude-seat', {
store,
transport: promotionTransport,
}).then((result) => {
observedResult = result;
});
await vi.advanceTimersByTimeAsync(5_000);
expect(observedResult).toMatchObject({
reason: 'RESOLVE_FAILED: Promotion transport command timed out after 5000ms.',
seat: 'claude-seat',
status: 'UNVERIFIED',
});
} finally {
vi.useRealTimers();
}
});
it('prints VERIFIED with the resolved seat, session, bundle, and wall-clock expiry', async () => {
const promotionTransport = transport();
const store: PromotionBreadcrumbStore = {
readAttemptId: vi.fn().mockResolvedValueOnce(null).mockResolvedValue(attemptId),
readResult: vi.fn(async () => ({
attempt_id: attemptId,
expires_at_wallclock: 4_600,
reason: null,
session_id: target.sessionId,
ts: Number.MAX_SAFE_INTEGER,
verified: true,
})),
};
const output = vi.spyOn(console, 'log').mockImplementation(() => {});
const program = new Command().exitOverride();
registerPromoteCommand(program, { store, transport: promotionTransport });
try {
await program.parseAsync(['node', 'mosaic', 'promote', 'claude-seat']);
expect(output).toHaveBeenCalledWith(
`VERIFIED seat=claude-seat session=${target.sessionId} bundle=local expiry=1970-01-01T01:16:40.000Z`,
);
expect(process.exitCode).not.toBe(1);
} finally {
output.mockRestore();
process.exitCode = undefined;
}
});
it('prints UNVERIFIED and exits 1 when delivery fails', async () => {
const promotionTransport: PromotionTransport = {
resolve: vi.fn(async () => target),
sendPromotion: vi.fn(async () => {
throw new Error('tmux unavailable');
}),
};
const store: PromotionBreadcrumbStore = {
readAttemptId: vi.fn(async () => null),
readResult: vi.fn(async () => null),
};
const output = vi.spyOn(console, 'log').mockImplementation(() => {});
const program = new Command().exitOverride();
registerPromoteCommand(program, { store, transport: promotionTransport });
try {
process.exitCode = undefined;
await program.parseAsync(['node', 'mosaic', 'promote', 'claude-seat']);
expect(output).toHaveBeenCalledWith(
`UNVERIFIED seat=claude-seat session=${target.sessionId} bundle=local expiry=none reason=DELIVERY_FAILED: tmux unavailable`,
);
expect(process.exitCode).toBe(1);
} finally {
output.mockRestore();
process.exitCode = undefined;
}
});
it('rejects a stale result even when its nonce matches', async () => {
const promotionTransport = transport();
let now = 1_000;
const store: PromotionBreadcrumbStore = {
readAttemptId: vi.fn().mockResolvedValueOnce(null).mockResolvedValue(attemptId),
readResult: vi.fn(async () => ({
attempt_id: attemptId,
expires_at_wallclock: 4_600,
reason: null,
session_id: target.sessionId,
ts: 1_000,
verified: true,
})),
};
const result = await promoteSeat('claude-seat', {
clock: () => now,
sleep: async () => {
now += 10;
},
store,
timeoutMs: 10,
transport: promotionTransport,
});
expect(result).toEqual({
bundle: 'local',
expiresAtWallclock: null,
reason: 'PROMOTION_TIMEOUT',
seat: 'claude-seat',
sessionId: 'b'.repeat(64),
status: 'UNVERIFIED',
});
});
it('does not accept a result for a pending attempt that existed before send', async () => {
const promotionTransport = transport();
let now = 1_000;
const store: PromotionBreadcrumbStore = {
readAttemptId: vi.fn(async () => attemptId),
readResult: vi.fn(async () => ({
attempt_id: attemptId,
expires_at_wallclock: 4_600,
reason: null,
session_id: target.sessionId,
ts: 1_001,
verified: true,
})),
};
const result = await promoteSeat('claude-seat', {
clock: () => now,
sleep: async () => {
now += 10;
},
store,
timeoutMs: 10,
transport: promotionTransport,
});
expect(result.status).toBe('UNVERIFIED');
expect(store.readResult).not.toHaveBeenCalled();
});
it('returns UNVERIFIED after a bounded timeout without reading stdin', async () => {
const promotionTransport = transport();
let now = 1_000;
const store: PromotionBreadcrumbStore = {
readAttemptId: vi.fn(async () => null),
readResult: vi.fn(async () => null),
};
const result = await promoteSeat('claude-seat', {
clock: () => now,
sleep: async () => {
now += 10;
},
store,
timeoutMs: 10,
transport: promotionTransport,
});
expect(result).toEqual({
bundle: 'local',
expiresAtWallclock: null,
reason: 'PROMOTION_TIMEOUT',
seat: 'claude-seat',
sessionId: 'b'.repeat(64),
status: 'UNVERIFIED',
});
expect(promotionTransport.sendPromotion).toHaveBeenCalledOnce();
expect(store.readResult).toHaveBeenCalledTimes(2);
});
});