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:
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user