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

@@ -401,4 +401,112 @@ describe("OperationRegistry access control", () => {
expect((error as CallError).message).toContain("identity required");
}
});
});
describe("OperationRegistry subscription handler validation", () => {
it("rejects non-async-generator handler for SUBSCRIPTION via registerHandler", () => {
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 regularAsyncFn = async () => "not a generator";
expect(() => registry.registerHandler("test.badSub", regularAsyncFn as any)).toThrow(
/must be an async generator function/i,
);
});
it("rejects synchronous function handler for SUBSCRIPTION via registerHandler", () => {
const registry = new OperationRegistry();
registry.registerSpec({
name: "syncSub",
namespace: "test",
version: "1.0.0",
type: OperationType.SUBSCRIPTION,
description: "sync sub",
inputSchema: Type.Object({}),
outputSchema: Type.Unknown(),
accessControl: { requiredScopes: [] },
});
expect(() => registry.registerHandler("test.syncSub", (() => {}) as any)).toThrow(
/must be an async generator function/i,
);
});
it("allows async generator function handler for SUBSCRIPTION via registerHandler", () => {
const registry = new OperationRegistry();
registry.registerSpec({
name: "goodSub",
namespace: "test",
version: "1.0.0",
type: OperationType.SUBSCRIPTION,
description: "good sub",
inputSchema: Type.Object({}),
outputSchema: Type.Unknown(),
accessControl: { requiredScopes: [] },
});
async function* goodHandler(_input: unknown, _context: any) {
yield "event";
}
expect(() => registry.registerHandler("test.goodSub", goodHandler as any)).not.toThrow();
});
it("rejects non-async-generator handler for SUBSCRIPTION via register", () => {
const registry = new OperationRegistry();
expect(() =>
registry.register({
name: "badRegSub",
namespace: "test",
version: "1.0.0",
type: OperationType.SUBSCRIPTION,
description: "bad sub via register",
inputSchema: Type.Object({}),
outputSchema: Type.Unknown(),
accessControl: { requiredScopes: [] },
handler: async () => "not a generator" as any,
}),
).toThrow(/must be an async generator function/i);
});
it("allows async generator handler for SUBSCRIPTION via register", () => {
const registry = new OperationRegistry();
async function* handler(_input: unknown, _context: any) {
yield "event";
}
expect(() =>
registry.register({
name: "goodRegSub",
namespace: "test",
version: "1.0.0",
type: OperationType.SUBSCRIPTION,
description: "good sub via register",
inputSchema: Type.Object({}),
outputSchema: Type.Unknown(),
accessControl: { requiredScopes: [] },
handler,
}),
).not.toThrow();
});
it("allows regular async handler for QUERY via registerHandler", () => {
const registry = new OperationRegistry();
registry.registerSpec({
name: "queryOp",
namespace: "test",
version: "1.0.0",
type: OperationType.QUERY,
description: "query op",
inputSchema: Type.Object({}),
outputSchema: Type.Unknown(),
accessControl: { requiredScopes: [] },
});
const handler = async (_input: unknown, _context: any) => ({ result: "ok" });
expect(() => registry.registerHandler("test.queryOp", handler as any)).not.toThrow();
});
});