291 lines
8.6 KiB
TypeScript
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);
|
|
});
|
|
});
|