diff --git a/packages/bridge-compiler/test/codegen.test.ts b/packages/bridge-compiler/test/codegen.test.ts index 52a31d5..4cd838e 100644 --- a/packages/bridge-compiler/test/codegen.test.ts +++ b/packages/bridge-compiler/test/codegen.test.ts @@ -478,6 +478,51 @@ bridge Query.det { }); }); +describe("AOT codegen: requestedFields matching", () => { + const bridgeText = `version 1.5 +bridge Query.profile { + with input as i + with output as o + + o.name <- i.name + o.meta.age <- i.age + o.meta.deep.city <- i.city +}`; + + test("parent field pattern includes nested child fields", async () => { + const document = parseBridgeFormat(bridgeText); + const { code } = compileBridge(document, { + operation: "Query.profile", + requestedFields: ["meta"], + }); + const fn = buildAotFn(code); + const data = await fn({ name: "Ada", age: 37, city: "Tallinn" }, {}, {}); + assert.deepEqual(data, { meta: { age: 37, deep: { city: "Tallinn" } } }); + }); + + test("nested field pattern keeps required parent container path", async () => { + const document = parseBridgeFormat(bridgeText); + const { code } = compileBridge(document, { + operation: "Query.profile", + requestedFields: ["meta.age"], + }); + const fn = buildAotFn(code); + const data = await fn({ name: "Ada", age: 37, city: "Tallinn" }, {}, {}); + assert.deepEqual(data, { meta: { age: 37 } }); + }); + + test("star wildcard only includes one level under the prefix", async () => { + const document = parseBridgeFormat(bridgeText); + const { code } = compileBridge(document, { + operation: "Query.profile", + requestedFields: ["meta.*"], + }); + const fn = buildAotFn(code); + const data = await fn({ name: "Ada", age: 37, city: "Tallinn" }, {}, {}); + assert.deepEqual(data, { meta: { age: 37 } }); + }); +}); + // ── Ternary / conditional wires ────────────────────────────────────────────── describe("AOT codegen: conditional wires", () => { diff --git a/packages/bridge-core/test/execution-tree.test.ts b/packages/bridge-core/test/execution-tree.test.ts new file mode 100644 index 0000000..943e43b --- /dev/null +++ b/packages/bridge-core/test/execution-tree.test.ts @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import { describe, test } from "node:test"; +import { + BridgeAbortError, + BridgePanicError, + ExecutionTree, + type BridgeDocument, + type NodeRef, +} from "../src/index.ts"; + +const DOC: BridgeDocument = { version: "1.5", instructions: [] }; +const TRUNK = { module: "_", type: "Query", field: "test" }; + +function ref(path: string[], rootSafe = false): NodeRef { + return { module: "_", type: "Query", field: "test", path, rootSafe }; +} + +describe("ExecutionTree edge cases", () => { + test("constructor rejects parent depth beyond hard recursion limit", () => { + const parent = { depth: 30 } as unknown as ExecutionTree; + assert.throws( + () => new ExecutionTree(TRUNK, DOC, {}, undefined, parent), + BridgePanicError, + ); + }); + + test("createShadowArray aborts when signal is already aborted", () => { + const tree = new ExecutionTree(TRUNK, DOC); + const controller = new AbortController(); + controller.abort(); + tree.signal = controller.signal; + + assert.throws( + () => (tree as any).createShadowArray([{}]), + BridgeAbortError, + ); + }); + + test("applyPath respects rootSafe and throws when not rootSafe", () => { + const tree = new ExecutionTree(TRUNK, DOC); + assert.equal((tree as any).applyPath(null, ref(["x"], true)), undefined); + assert.throws(() => (tree as any).applyPath(null, ref(["x"])), TypeError); + }); + + test("applyPath warns when using object-style access on arrays", () => { + const tree = new ExecutionTree(TRUNK, DOC); + let warning = ""; + tree.logger = { warn: (msg: string) => (warning = msg) }; + + assert.equal((tree as any).applyPath([{ x: 1 }], ref(["x"])), undefined); + assert.equal((tree as any).applyPath([{ x: 1 }], ref(["0", "x"])), 1); + assert.match(warning, /Accessing "\.x" on an array/); + }); +}); diff --git a/packages/bridge-graphql/test/bridge-transform.test.ts b/packages/bridge-graphql/test/bridge-transform.test.ts new file mode 100644 index 0000000..0991441 --- /dev/null +++ b/packages/bridge-graphql/test/bridge-transform.test.ts @@ -0,0 +1,114 @@ +import { buildHTTPExecutor } from "@graphql-tools/executor-http"; +import { parse } from "graphql"; +import { createSchema, createYoga } from "graphql-yoga"; +import assert from "node:assert/strict"; +import { describe, test } from "node:test"; +import { parseBridgeFormat as parseBridge } from "@stackables/bridge-parser"; +import { + bridgeTransform, + getBridgeTraces, + useBridgeTracing, +} from "../src/index.ts"; + +describe("bridgeTransform coverage branches", () => { + test("supports contextMapper with per-request document selection", async () => { + const typeDefs = /* GraphQL */ ` + type Query { + pick: PickResult + } + type PickResult { + value: String + secret: String + } + `; + + const v1 = parseBridge(`version 1.5 +bridge Query.pick { + with context as c + with output as o + o.value <- c.allowed + o.secret <- c.secret +}`); + const v2 = parseBridge(`version 1.5 +bridge Query.pick { + with output as o + o.value = "v2" +}`); + + const rawSchema = createSchema({ typeDefs }); + const schema = bridgeTransform( + rawSchema, + (ctx) => (ctx.version === "v2" ? v2 : v1), + { + contextMapper: (ctx) => ({ allowed: ctx.allowed }), + }, + ); + const yoga = createYoga({ + schema, + graphqlEndpoint: "*", + context: () => ({ allowed: "mapped", secret: "hidden", version: "v1" }), + }); + const executor = buildHTTPExecutor({ fetch: yoga.fetch as any }); + + const result: any = await executor({ + document: parse(`{ pick { value secret } }`), + }); + assert.equal(result.data.pick.value, "mapped"); + assert.equal(result.data.pick.secret, null); + }); + + test("applies toolTimeoutMs and maxDepth options to root execution tree", async () => { + const typeDefs = /* GraphQL */ ` + type Query { + slow: SlowResult + } + type SlowResult { + value: String + } + `; + const instructions = parseBridge(`version 1.5 +bridge Query.slow { + with waitTool as w + with output as o + o.value <- w.value +}`); + const rawSchema = createSchema({ typeDefs }); + const schema = bridgeTransform(rawSchema, instructions, { + tools: { + waitTool: async () => + new Promise((resolve) => setTimeout(() => resolve({ value: "ok" }), 30)), + }, + toolTimeoutMs: 1, + maxDepth: 3, + }); + const yoga = createYoga({ schema, graphqlEndpoint: "*" }); + const executor = buildHTTPExecutor({ fetch: yoga.fetch as any }); + const result: any = await executor({ document: parse(`{ slow { value } }`) }); + assert.ok(result.errors?.length > 0, JSON.stringify(result)); + }); +}); + +describe("bridge tracing helpers", () => { + test("getBridgeTraces returns empty array when tracer is absent", () => { + assert.deepEqual(getBridgeTraces(undefined), []); + assert.deepEqual(getBridgeTraces({}), []); + }); + + test("useBridgeTracing adds traces into GraphQL extensions", () => { + const traces = [{ tool: "a", fn: "a", startedAt: 1, durationMs: 1 }]; + const plugin = useBridgeTracing(); + const execHooks = plugin.onExecute({ + args: { contextValue: { __bridgeTracer: { traces } } }, + } as any); + let updated: any; + + execHooks?.onExecuteDone?.({ + result: { data: { ok: true } }, + setResult: (r: any) => { + updated = r; + }, + }); + + assert.deepEqual(updated.extensions.traces, traces); + }); +}); diff --git a/packages/bridge/test/bridge-format.test.ts b/packages/bridge/test/bridge-format.test.ts index 442acb2..3437680 100644 --- a/packages/bridge/test/bridge-format.test.ts +++ b/packages/bridge/test/bridge-format.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { describe, test } from "node:test"; import { parseBridgeFormat as parseBridge, + parseBridgeDiagnostics, parsePath, serializeBridge, } from "../src/index.ts"; @@ -1282,3 +1283,44 @@ bridge Query.test { assert.doesNotThrow(() => parseBridge(serialized)); }); }); + +describe("parser diagnostics and serializer edge cases", () => { + test("parseBridgeDiagnostics reports lexer errors with a range", () => { + const result = parseBridgeDiagnostics("version 1.5\nbridge Query.x {\n with output as o\n o.x = \"ok\"\n}\n§"); + assert.ok(result.diagnostics.length > 0); + assert.equal(result.diagnostics[0]?.severity, "error"); + assert.equal(result.diagnostics[0]?.range.start.line, 5); + assert.equal(result.diagnostics[0]?.range.start.character, 0); + }); + + test("reserved source identifier is rejected as const name", () => { + assert.throws( + () => parseBridge('version 1.5\nconst input = "x"'), + /reserved source identifier.*const name/i, + ); + }); + + test("serializeBridge keeps passthrough shorthand", () => { + const src = "version 1.5\nbridge Query.upper with std.str.toUpperCase"; + const serialized = serializeBridge(parseBridge(src)); + assert.ok( + serialized.includes("bridge Query.upper with std.str.toUpperCase"), + serialized, + ); + }); + + test("serializeBridge uses compact default handle bindings", () => { + const src = `version 1.5 +bridge Query.defaults { + with input + with output + with const + + output.value <- input.name +}`; + const serialized = serializeBridge(parseBridge(src)); + assert.ok(serialized.includes(" with input\n"), serialized); + assert.ok(serialized.includes(" with output\n"), serialized); + assert.ok(serialized.includes(" with const\n"), serialized); + }); +});