ci/woodpecker/push/publish Pipeline failed
Co-authored-by: shaggy <[email protected]>
171 lines
5.0 KiB
TypeScript
171 lines
5.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type {
|
|
AttachConversation,
|
|
ConversationSnapshot,
|
|
DetachConversation,
|
|
HarnessActorContext,
|
|
HarnessConversationService,
|
|
HarnessEventEnvelope,
|
|
HarnessSelection,
|
|
SendHarnessTurn,
|
|
TurnReceipt,
|
|
} from '@mosaicstack/types';
|
|
import { HarnessChatRuntime } from './harness-chat.runtime.js';
|
|
|
|
/**
|
|
* Task Five, Step One (harness runtime). Proves the `pi-rpc` runtime executes
|
|
* exclusively through the {@link HarnessConversationService} RPC boundary and
|
|
* forwards the caller's exact selection tuple and idempotency key without
|
|
* substitution. Red-first: the runtime is an unimplemented stub, so every
|
|
* delegation assertion fails until Step Three.
|
|
*/
|
|
|
|
const context: HarnessActorContext = {
|
|
actorId: 'actor-1',
|
|
tenantId: 'tenant-1',
|
|
seatId: 'seat-1',
|
|
correlationId: 'corr-1',
|
|
};
|
|
|
|
const selection: HarnessSelection = {
|
|
harnessId: 'pi',
|
|
providerId: 'anthropic',
|
|
modelId: 'claude-opus-4-8',
|
|
};
|
|
|
|
const conversationId = '11111111-1111-4111-8111-111111111111';
|
|
const idempotencyKey = '22222222-2222-4222-8222-222222222222';
|
|
|
|
const sendInput: SendHarnessTurn & { idempotencyKey: string } = {
|
|
context,
|
|
conversationId,
|
|
selection,
|
|
turnId: 'turn-abc',
|
|
correlationId: 'corr-1',
|
|
content: 'hello',
|
|
idempotencyKey,
|
|
};
|
|
|
|
const attachInput: AttachConversation & { afterSequence?: number } = {
|
|
context,
|
|
conversationId,
|
|
clientId: 'client-1',
|
|
selection,
|
|
afterSequence: 0,
|
|
};
|
|
|
|
const detachInput: DetachConversation = {
|
|
context,
|
|
conversationId,
|
|
clientId: 'client-1',
|
|
};
|
|
|
|
interface RecordedCalls {
|
|
attach: (AttachConversation & { afterSequence?: number })[];
|
|
detach: DetachConversation[];
|
|
send: (SendHarnessTurn & { idempotencyKey: string })[];
|
|
subscribeFrom: { conversationId: string; afterSequence: number }[];
|
|
}
|
|
|
|
const snapshot: ConversationSnapshot = {
|
|
session: {
|
|
conversationId,
|
|
nativeSessionId: 'native-1',
|
|
seatId: 'seat-1',
|
|
selection,
|
|
state: 'idle',
|
|
attachedClientIds: ['client-1'],
|
|
},
|
|
lastSequence: 0,
|
|
replay: [],
|
|
};
|
|
|
|
function build(): { runtime: HarnessChatRuntime; calls: RecordedCalls } {
|
|
const calls: RecordedCalls = { attach: [], detach: [], send: [], subscribeFrom: [] };
|
|
const service: HarnessConversationService = {
|
|
attach: (input) => {
|
|
calls.attach.push(input);
|
|
return Promise.resolve(snapshot);
|
|
},
|
|
detach: (input) => {
|
|
calls.detach.push(input);
|
|
return Promise.resolve();
|
|
},
|
|
send: (input) => {
|
|
calls.send.push(input);
|
|
// The service echoes only the requested tuple; there is no representable substitute.
|
|
const receipt: TurnReceipt = {
|
|
conversationId: input.conversationId,
|
|
turnId: 'turn-server',
|
|
correlationId: input.correlationId,
|
|
state: 'accepted',
|
|
selection: input.selection,
|
|
};
|
|
return Promise.resolve(receipt);
|
|
},
|
|
subscribeFrom: (id, afterSequence) => {
|
|
calls.subscribeFrom.push({ conversationId: id, afterSequence });
|
|
|
|
return (async function* (): AsyncIterable<HarnessEventEnvelope> {
|
|
return;
|
|
})();
|
|
},
|
|
};
|
|
return { runtime: new HarnessChatRuntime(service), calls };
|
|
}
|
|
|
|
describe('HarnessChatRuntime', () => {
|
|
it('is the harness runtime kind and needs only a HarnessConversationService', () => {
|
|
const { runtime } = build();
|
|
expect(runtime.kind).toBe('harness');
|
|
});
|
|
|
|
it('delegates send to the conversation service with the exact tuple and idempotency key', async () => {
|
|
const { runtime, calls } = build();
|
|
|
|
const receipt = await runtime.send(sendInput);
|
|
|
|
expect(calls.send).toHaveLength(1);
|
|
const firstSend = calls.send[0]!;
|
|
expect(firstSend).toEqual(sendInput);
|
|
expect(firstSend.idempotencyKey).toBe(idempotencyKey);
|
|
expect(firstSend.selection).toEqual(selection);
|
|
// The runtime must not substitute an effective tuple onto the receipt.
|
|
expect(receipt.selection).toEqual(selection);
|
|
});
|
|
|
|
it('delegates attach to the conversation service and returns its snapshot', async () => {
|
|
const { runtime, calls } = build();
|
|
|
|
const result = await runtime.attach(attachInput);
|
|
|
|
expect(calls.attach).toHaveLength(1);
|
|
expect(calls.attach[0]).toEqual(attachInput);
|
|
expect(result).toBe(snapshot);
|
|
});
|
|
|
|
it('delegates detach to the conversation service', async () => {
|
|
const { runtime, calls } = build();
|
|
|
|
await runtime.detach(detachInput);
|
|
|
|
expect(calls.detach).toHaveLength(1);
|
|
expect(calls.detach[0]).toEqual(detachInput);
|
|
});
|
|
|
|
it('delegates subscribeFrom to the conversation service journal replay', async () => {
|
|
const { runtime, calls } = build();
|
|
|
|
const iterable = runtime.subscribeFrom(conversationId, 7);
|
|
// Drain to prove it is the service-backed async iterable, not a fabricated one.
|
|
const drained: unknown[] = [];
|
|
for await (const event of iterable) {
|
|
drained.push(event);
|
|
}
|
|
expect(drained).toHaveLength(0);
|
|
|
|
expect(calls.subscribeFrom).toHaveLength(1);
|
|
expect(calls.subscribeFrom[0]).toEqual({ conversationId, afterSequence: 7 });
|
|
});
|
|
});
|