feat(mosaic): add correlated lease promotion CLI
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
import { Command } from 'commander';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
promoteSeat,
|
||||
registerPromoteCommand,
|
||||
type PromotionBreadcrumbStore,
|
||||
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('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('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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user