Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions packages/bridge-compiler/test/codegen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
54 changes: 54 additions & 0 deletions packages/bridge-core/test/execution-tree.test.ts
Original file line number Diff line number Diff line change
@@ -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/);
});
});
114 changes: 114 additions & 0 deletions packages/bridge-graphql/test/bridge-transform.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
42 changes: 42 additions & 0 deletions packages/bridge/test/bridge-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
});
});