fix: resolve M-01..M-02, M-05..M-08, L-01..L-03, L-05 from pre-release review

M-01: Compose OperationDefinitionSchema from OperationSpecSchema via Type.Intersect
M-02: Extract from_openapi to subpath export, remove from main entry
M-05: Fix mapError fragile includes() matching — use startsWith(code+':') or exact
M-06: Replace any casts with MCPClientLike/MCPToolResult interfaces in from_mcp
M-07: Add injectable fetch to HTTPServiceConfig for from_openapi
M-08: Add OpenAPIServiceRegistry with lifecycle methods (add, remove, registerAll)
L-01: Validate subscription handler type at registration and runtime
L-02: Strengthen isResponseEnvelope with source-specific field validation
L-03: Add logger.warn on FromSchema fallback to Type.Unknown
L-04: Noted as intentional (SSE GET body handling)
L-05: Add registerAll to MCPClientLoader and OpenAPIServiceRegistry
This commit is contained in:
2026-05-16 14:56:13 +00:00
parent 2b72289635
commit ca2021bd3d
17 changed files with 424 additions and 72 deletions

View File

@@ -380,4 +380,85 @@ describe("subscribe", () => {
expect(results).toHaveLength(1);
expect(results[0].data).toBe("secret-event");
});
it("throws CallError when handler returns a non-async-iterable (plain async function)", async () => {
const registry = new OperationRegistry();
registry.registerSpec({
name: "badSub",
namespace: "test",
version: "1.0.0",
type: OperationType.SUBSCRIPTION,
description: "bad sub",
inputSchema: Type.Object({}),
outputSchema: Type.Unknown(),
accessControl: { requiredScopes: [] },
});
const plainAsyncFn = async (_input: unknown, _context: OperationContext) => "not a generator";
(registry as any).handlers.set("test.badSub", plainAsyncFn);
try {
for await (const _ of subscribe(registry, "test.badSub", {}, makeContext())) {
expect.fail("Should have thrown");
}
expect.fail("Should have thrown");
} catch (error) {
expect(error).toBeInstanceOf(CallError);
expect((error as CallError).code).toBe(InfrastructureErrorCode.EXECUTION_ERROR);
expect((error as CallError).message).toContain("must return an async iterable");
}
});
it("throws CallError when handler returns null", async () => {
const registry = new OperationRegistry();
registry.registerSpec({
name: "nullSub",
namespace: "test",
version: "1.0.0",
type: OperationType.SUBSCRIPTION,
description: "null sub",
inputSchema: Type.Object({}),
outputSchema: Type.Unknown(),
accessControl: { requiredScopes: [] },
});
const nullFn = (_input: unknown, _context: OperationContext): any => null;
(registry as any).handlers.set("test.nullSub", nullFn);
try {
for await (const _ of subscribe(registry, "test.nullSub", {}, makeContext())) {
expect.fail("Should have thrown");
}
expect.fail("Should have thrown");
} catch (error) {
expect(error).toBeInstanceOf(CallError);
expect((error as CallError).code).toBe(InfrastructureErrorCode.EXECUTION_ERROR);
expect((error as CallError).message).toContain("must return an async iterable");
}
});
it("throws CallError when handler returns a plain object (non-iterable)", async () => {
const registry = new OperationRegistry();
registry.registerSpec({
name: "objSub",
namespace: "test",
version: "1.0.0",
type: OperationType.SUBSCRIPTION,
description: "obj sub",
inputSchema: Type.Object({}),
outputSchema: Type.Unknown(),
accessControl: { requiredScopes: [] },
});
const objFn = (_input: unknown, _context: OperationContext): any => ({ not: "iterable" });
(registry as any).handlers.set("test.objSub", objFn);
try {
for await (const _ of subscribe(registry, "test.objSub", {}, makeContext())) {
expect.fail("Should have thrown");
}
expect.fail("Should have thrown");
} catch (error) {
expect(error).toBeInstanceOf(CallError);
expect((error as CallError).code).toBe(InfrastructureErrorCode.EXECUTION_ERROR);
expect((error as CallError).message).toContain("must return an async iterable");
}
});
});