From d74b750ecbd3854ed1155cc07e8790564fd93e39 Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Mon, 11 May 2026 02:37:44 +0000 Subject: [PATCH] feat(env): complete envelope integration for buildEnv - propagate identity in call protocol mode and add comprehensive tests - Pass context.identity through to callMap.call() in call protocol mode for proper identity propagation in nested operation calls - Add identity propagation test coverage for both with-identity and without-identity scenarios - Add test for parentRequestId propagation through callMap - Add test for PendingRequestMap as callMap integration - Add test for pre-built ResponseEnvelope pass-through in direct mode - Add test for Value.Cast normalization via execute in direct mode - Add test for empty registry, namespace grouping, and local source metadata verification - All 216 tests passing, build and lint clean --- src/env.ts | 1 + test/env.test.ts | 218 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 217 insertions(+), 2 deletions(-) diff --git a/src/env.ts b/src/env.ts index 71c7a7d..02e9c2b 100644 --- a/src/env.ts +++ b/src/env.ts @@ -43,6 +43,7 @@ export function buildEnv(options: EnvOptions): OperationEnv { logger.debug(`Call protocol: ${operationId}`); return await callMap.call(operationId, input, { parentRequestId: context.requestId, + identity: context.identity, }); }; } else { diff --git a/test/env.test.ts b/test/env.test.ts index b0ab153..3c5af00 100644 --- a/test/env.test.ts +++ b/test/env.test.ts @@ -1,8 +1,9 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi } from "vitest"; import { OperationRegistry, OperationType, buildEnv, type IOperationDefinition, type OperationContext } from "../src/index.js"; import * as Type from "@alkdev/typebox"; import { PendingRequestMap } from "../src/call.js"; -import { localEnvelope, isResponseEnvelope, type ResponseEnvelope } from "../src/response-envelope.js"; +import { localEnvelope, httpEnvelope, isResponseEnvelope, type ResponseEnvelope } from "../src/response-envelope.js"; +import type { Identity } from "../src/types.js"; function makeOperation(name: string, handler?: any): IOperationDefinition { return { @@ -39,6 +40,55 @@ describe("buildEnv", () => { expect(result.data).toEqual({ result: "test" }); }); + it("returns ResponseEnvelope with local source from direct mode", async () => { + const registry = new OperationRegistry(); + registry.register(makeOperation("op1")); + + const env = buildEnv({ + registry, + context: {} as OperationContext, + }); + + const result = await env.test.op1({ value: "hello" }); + expect(isResponseEnvelope(result)).toBe(true); + expect(result.meta.source).toBe("local"); + if (result.meta.source === "local") { + expect(result.meta.operationId).toBe("test.op1"); + expect(typeof result.meta.timestamp).toBe("number"); + } + expect(result.data).toEqual({ result: "hello" }); + }); + + it("passes pre-built ResponseEnvelope through from handler in direct mode", async () => { + const registry = new OperationRegistry(); + const httpEnv = httpEnvelope({ items: [1, 2, 3] }, { + statusCode: 200, + headers: { "content-type": "application/json" }, + contentType: "application/json", + }); + registry.register({ + name: "httpOp", + namespace: "test", + version: "1.0.0", + type: OperationType.QUERY, + description: "http op", + inputSchema: Type.Object({ value: Type.String() }), + outputSchema: Type.Unknown(), + accessControl: { requiredScopes: [] }, + handler: async () => httpEnv, + }); + + const env = buildEnv({ + registry, + context: {} as OperationContext, + }); + + const result = await env.test.httpOp({ value: "x" }); + expect(isResponseEnvelope(result)).toBe(true); + expect(result.meta.source).toBe("http"); + expect(result.data).toEqual({ items: [1, 2, 3] }); + }); + it("filters out SUBSCRIPTION operations", () => { const registry = new OperationRegistry(); registry.register(makeOperation("query")); @@ -94,4 +144,168 @@ describe("buildEnv", () => { expect(isResponseEnvelope(result)).toBe(true); expect(result.data).toEqual({ result: "routed: test.readFile" }); }); + + it("passes parentRequestId through callMap in call protocol mode", async () => { + const registry = new OperationRegistry(); + registry.register(makeOperation("op1")); + + let capturedOptions: any = null; + const callMap = { + call: async (opId: string, input: unknown, opts?: any): Promise => { + capturedOptions = opts; + return localEnvelope({ result: "ok" }, opId); + }, + }; + + const context: OperationContext = { + requestId: "parent-req-123", + }; + + const env = buildEnv({ + registry, + context, + callMap, + }); + + await env.test.op1({ value: "test" }); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions.parentRequestId).toBe("parent-req-123"); + }); + + it("passes identity through callMap in call protocol mode", async () => { + const registry = new OperationRegistry(); + registry.register(makeOperation("op1")); + + let capturedOptions: any = null; + const callMap = { + call: async (opId: string, input: unknown, opts?: any): Promise => { + capturedOptions = opts; + return localEnvelope({ result: "ok" }, opId); + }, + }; + + const identity: Identity = { id: "user1", scopes: ["read"] }; + const context: OperationContext = { + requestId: "parent-req-456", + identity, + }; + + const env = buildEnv({ + registry, + context, + callMap, + }); + + await env.test.op1({ value: "test" }); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions.parentRequestId).toBe("parent-req-456"); + expect(capturedOptions.identity).toEqual(identity); + }); + + it("does not pass identity when context has no identity in call protocol mode", async () => { + const registry = new OperationRegistry(); + registry.register(makeOperation("op1")); + + let capturedOptions: any = null; + const callMap = { + call: async (opId: string, input: unknown, opts?: any): Promise => { + capturedOptions = opts; + return localEnvelope({ result: "ok" }, opId); + }, + }; + + const context: OperationContext = { + requestId: "parent-req-789", + }; + + const env = buildEnv({ + registry, + context, + callMap, + }); + + await env.test.op1({ value: "test" }); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions.parentRequestId).toBe("parent-req-789"); + expect(capturedOptions.identity).toBeUndefined(); + }); + + it("works with PendingRequestMap as callMap", async () => { + const registry = new OperationRegistry(); + registry.register(makeOperation("echo")); + + const callMap = new PendingRequestMap(); + const env = buildEnv({ + registry, + context: {} as OperationContext, + callMap, + }); + + const callPromise = env.test.echo({ value: "hello" }); + + const requestId = [...(callMap as any).requests.keys()][0]; + callMap.respond(requestId, localEnvelope({ result: "echoed" }, "test.echo")); + + const result = await callPromise; + expect(isResponseEnvelope(result)).toBe(true); + expect(result.data).toEqual({ result: "echoed" }); + expect(result.meta.source).toBe("local"); + }); + + it("returns empty env when registry has no specs", () => { + const registry = new OperationRegistry(); + + const env = buildEnv({ + registry, + context: {} as OperationContext, + }); + + expect(Object.keys(env)).toHaveLength(0); + }); + + it("groups operations by namespace", () => { + const registry = new OperationRegistry(); + registry.register(makeOperation("op1")); + registry.register({ + ...makeOperation("op2"), + namespace: "other", + }); + + const env = buildEnv({ + registry, + context: {} as OperationContext, + }); + + expect(env.test).toBeDefined(); + expect(env.other).toBeDefined(); + expect(env.test.op1).toBeDefined(); + expect(env.other.op2).toBeDefined(); + }); + + it("Value.Cast normalization applies in direct mode via execute", async () => { + const registry = new OperationRegistry(); + registry.register({ + name: "withDefaults", + namespace: "test", + version: "1.0.0", + type: OperationType.QUERY, + description: "op with default fields", + inputSchema: Type.Object({ value: Type.String() }), + outputSchema: Type.Object({ name: Type.String(), count: Type.Number({ default: 0 }) }), + accessControl: { requiredScopes: [] }, + handler: async () => ({ name: "test" }), + }); + + const env = buildEnv({ + registry, + context: {} as OperationContext, + }); + + const result = await env.test.withDefaults({ value: "x" }); + expect(isResponseEnvelope(result)).toBe(true); + expect(result.data).toEqual({ name: "test", count: 0 }); + }); }); \ No newline at end of file