78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
|
|
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||
|
|
import { z } from 'zod';
|
||
|
|
import { setupFileRig, type FileTestRig } from './helpers/rig.js';
|
||
|
|
import { CustomOpRejectedError, NotImplementedError } from '../../src/index.js';
|
||
|
|
|
||
|
|
// Module augmentation: register a typed custom op for the test.
|
||
|
|
declare module '../../src/index.js' {
|
||
|
|
interface CustomOpsMap {
|
||
|
|
'test.echo': { args: { message: string }; response: { echoed: string } };
|
||
|
|
'test.add': { args: { a: number; b: number }; response: { sum: number } };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('Custom ops — registry + Zod validation + typed I/O', () => {
|
||
|
|
let rig: FileTestRig;
|
||
|
|
const callLog: string[] = [];
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
rig = await setupFileRig({
|
||
|
|
custom: {
|
||
|
|
'test.echo': {
|
||
|
|
args: z.object({ message: z.string().min(1).max(64) }),
|
||
|
|
response: z.object({ echoed: z.string() }),
|
||
|
|
handler: async (args, ctx) => {
|
||
|
|
callLog.push(`echo:${ctx.sender}:${args.message}`);
|
||
|
|
return { echoed: args.message.toUpperCase() };
|
||
|
|
},
|
||
|
|
},
|
||
|
|
'test.add': {
|
||
|
|
args: z.object({ a: z.number(), b: z.number() }),
|
||
|
|
response: z.object({ sum: z.number() }),
|
||
|
|
handler: async (args) => ({ sum: args.a + args.b }),
|
||
|
|
},
|
||
|
|
'test.bad-response': {
|
||
|
|
args: z.object({}),
|
||
|
|
response: z.object({ x: z.number() }),
|
||
|
|
// Returns wrong shape on purpose.
|
||
|
|
handler: async () => ({ y: 'not-a-number' }) as unknown as { x: number },
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(async () => {
|
||
|
|
await rig.teardown();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('typed echo round-trips through registered Zod schemas', async () => {
|
||
|
|
const result = await rig.fs.custom('test.echo', { message: 'hello' });
|
||
|
|
expect(result.echoed).toBe('HELLO');
|
||
|
|
expect(callLog).toContain('echo:alice:hello');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('typed add', async () => {
|
||
|
|
const result = await rig.fs.custom('test.add', { a: 3, b: 4 });
|
||
|
|
expect(result.sum).toBe(7);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('invalid args (Zod-rejected payload) → InvalidArgsError', async () => {
|
||
|
|
await expect(
|
||
|
|
// message: '' violates min(1) — TypeScript still allows it since string
|
||
|
|
rig.fs.custom('test.echo', { message: '' }),
|
||
|
|
).rejects.toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('unknown custom op name → NotImplementedError', async () => {
|
||
|
|
await expect(
|
||
|
|
rig.fs.custom('test.unknown' as never, {} as never),
|
||
|
|
).rejects.toBeInstanceOf(NotImplementedError);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('handler returns wrong shape → CustomOpRejectedError', async () => {
|
||
|
|
await expect(
|
||
|
|
rig.fs.custom('test.bad-response' as never, {} as never),
|
||
|
|
).rejects.toBeInstanceOf(CustomOpRejectedError);
|
||
|
|
});
|
||
|
|
});
|