Skip to content

Commit be74193

Browse files
Copilotaarne
andauthored
Increase targeted coverage for parser, codegen, execution tree, and GraphQL transform edge paths (#92)
* Initial plan * test: add focused coverage tests for parser, codegen, core, and graphql Co-authored-by: aarne <82001+aarne@users.noreply.github.com> * test: tighten diagnostics and execution tree assertions Co-authored-by: aarne <82001+aarne@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: aarne <82001+aarne@users.noreply.github.com>
1 parent 4e03d3f commit be74193

File tree

4 files changed

+255
-0
lines changed

4 files changed

+255
-0
lines changed

packages/bridge-compiler/test/codegen.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,51 @@ bridge Query.det {
478478
});
479479
});
480480

481+
describe("AOT codegen: requestedFields matching", () => {
482+
const bridgeText = `version 1.5
483+
bridge Query.profile {
484+
with input as i
485+
with output as o
486+
487+
o.name <- i.name
488+
o.meta.age <- i.age
489+
o.meta.deep.city <- i.city
490+
}`;
491+
492+
test("parent field pattern includes nested child fields", async () => {
493+
const document = parseBridgeFormat(bridgeText);
494+
const { code } = compileBridge(document, {
495+
operation: "Query.profile",
496+
requestedFields: ["meta"],
497+
});
498+
const fn = buildAotFn(code);
499+
const data = await fn({ name: "Ada", age: 37, city: "Tallinn" }, {}, {});
500+
assert.deepEqual(data, { meta: { age: 37, deep: { city: "Tallinn" } } });
501+
});
502+
503+
test("nested field pattern keeps required parent container path", async () => {
504+
const document = parseBridgeFormat(bridgeText);
505+
const { code } = compileBridge(document, {
506+
operation: "Query.profile",
507+
requestedFields: ["meta.age"],
508+
});
509+
const fn = buildAotFn(code);
510+
const data = await fn({ name: "Ada", age: 37, city: "Tallinn" }, {}, {});
511+
assert.deepEqual(data, { meta: { age: 37 } });
512+
});
513+
514+
test("star wildcard only includes one level under the prefix", async () => {
515+
const document = parseBridgeFormat(bridgeText);
516+
const { code } = compileBridge(document, {
517+
operation: "Query.profile",
518+
requestedFields: ["meta.*"],
519+
});
520+
const fn = buildAotFn(code);
521+
const data = await fn({ name: "Ada", age: 37, city: "Tallinn" }, {}, {});
522+
assert.deepEqual(data, { meta: { age: 37 } });
523+
});
524+
});
525+
481526
// ── Ternary / conditional wires ──────────────────────────────────────────────
482527

483528
describe("AOT codegen: conditional wires", () => {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import assert from "node:assert/strict";
2+
import { describe, test } from "node:test";
3+
import {
4+
BridgeAbortError,
5+
BridgePanicError,
6+
ExecutionTree,
7+
type BridgeDocument,
8+
type NodeRef,
9+
} from "../src/index.ts";
10+
11+
const DOC: BridgeDocument = { version: "1.5", instructions: [] };
12+
const TRUNK = { module: "_", type: "Query", field: "test" };
13+
14+
function ref(path: string[], rootSafe = false): NodeRef {
15+
return { module: "_", type: "Query", field: "test", path, rootSafe };
16+
}
17+
18+
describe("ExecutionTree edge cases", () => {
19+
test("constructor rejects parent depth beyond hard recursion limit", () => {
20+
const parent = { depth: 30 } as unknown as ExecutionTree;
21+
assert.throws(
22+
() => new ExecutionTree(TRUNK, DOC, {}, undefined, parent),
23+
BridgePanicError,
24+
);
25+
});
26+
27+
test("createShadowArray aborts when signal is already aborted", () => {
28+
const tree = new ExecutionTree(TRUNK, DOC);
29+
const controller = new AbortController();
30+
controller.abort();
31+
tree.signal = controller.signal;
32+
33+
assert.throws(
34+
() => (tree as any).createShadowArray([{}]),
35+
BridgeAbortError,
36+
);
37+
});
38+
39+
test("applyPath respects rootSafe and throws when not rootSafe", () => {
40+
const tree = new ExecutionTree(TRUNK, DOC);
41+
assert.equal((tree as any).applyPath(null, ref(["x"], true)), undefined);
42+
assert.throws(() => (tree as any).applyPath(null, ref(["x"])), TypeError);
43+
});
44+
45+
test("applyPath warns when using object-style access on arrays", () => {
46+
const tree = new ExecutionTree(TRUNK, DOC);
47+
let warning = "";
48+
tree.logger = { warn: (msg: string) => (warning = msg) };
49+
50+
assert.equal((tree as any).applyPath([{ x: 1 }], ref(["x"])), undefined);
51+
assert.equal((tree as any).applyPath([{ x: 1 }], ref(["0", "x"])), 1);
52+
assert.match(warning, /Accessing "\.x" on an array/);
53+
});
54+
});
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { buildHTTPExecutor } from "@graphql-tools/executor-http";
2+
import { parse } from "graphql";
3+
import { createSchema, createYoga } from "graphql-yoga";
4+
import assert from "node:assert/strict";
5+
import { describe, test } from "node:test";
6+
import { parseBridgeFormat as parseBridge } from "@stackables/bridge-parser";
7+
import {
8+
bridgeTransform,
9+
getBridgeTraces,
10+
useBridgeTracing,
11+
} from "../src/index.ts";
12+
13+
describe("bridgeTransform coverage branches", () => {
14+
test("supports contextMapper with per-request document selection", async () => {
15+
const typeDefs = /* GraphQL */ `
16+
type Query {
17+
pick: PickResult
18+
}
19+
type PickResult {
20+
value: String
21+
secret: String
22+
}
23+
`;
24+
25+
const v1 = parseBridge(`version 1.5
26+
bridge Query.pick {
27+
with context as c
28+
with output as o
29+
o.value <- c.allowed
30+
o.secret <- c.secret
31+
}`);
32+
const v2 = parseBridge(`version 1.5
33+
bridge Query.pick {
34+
with output as o
35+
o.value = "v2"
36+
}`);
37+
38+
const rawSchema = createSchema({ typeDefs });
39+
const schema = bridgeTransform(
40+
rawSchema,
41+
(ctx) => (ctx.version === "v2" ? v2 : v1),
42+
{
43+
contextMapper: (ctx) => ({ allowed: ctx.allowed }),
44+
},
45+
);
46+
const yoga = createYoga({
47+
schema,
48+
graphqlEndpoint: "*",
49+
context: () => ({ allowed: "mapped", secret: "hidden", version: "v1" }),
50+
});
51+
const executor = buildHTTPExecutor({ fetch: yoga.fetch as any });
52+
53+
const result: any = await executor({
54+
document: parse(`{ pick { value secret } }`),
55+
});
56+
assert.equal(result.data.pick.value, "mapped");
57+
assert.equal(result.data.pick.secret, null);
58+
});
59+
60+
test("applies toolTimeoutMs and maxDepth options to root execution tree", async () => {
61+
const typeDefs = /* GraphQL */ `
62+
type Query {
63+
slow: SlowResult
64+
}
65+
type SlowResult {
66+
value: String
67+
}
68+
`;
69+
const instructions = parseBridge(`version 1.5
70+
bridge Query.slow {
71+
with waitTool as w
72+
with output as o
73+
o.value <- w.value
74+
}`);
75+
const rawSchema = createSchema({ typeDefs });
76+
const schema = bridgeTransform(rawSchema, instructions, {
77+
tools: {
78+
waitTool: async () =>
79+
new Promise((resolve) => setTimeout(() => resolve({ value: "ok" }), 30)),
80+
},
81+
toolTimeoutMs: 1,
82+
maxDepth: 3,
83+
});
84+
const yoga = createYoga({ schema, graphqlEndpoint: "*" });
85+
const executor = buildHTTPExecutor({ fetch: yoga.fetch as any });
86+
const result: any = await executor({ document: parse(`{ slow { value } }`) });
87+
assert.ok(result.errors?.length > 0, JSON.stringify(result));
88+
});
89+
});
90+
91+
describe("bridge tracing helpers", () => {
92+
test("getBridgeTraces returns empty array when tracer is absent", () => {
93+
assert.deepEqual(getBridgeTraces(undefined), []);
94+
assert.deepEqual(getBridgeTraces({}), []);
95+
});
96+
97+
test("useBridgeTracing adds traces into GraphQL extensions", () => {
98+
const traces = [{ tool: "a", fn: "a", startedAt: 1, durationMs: 1 }];
99+
const plugin = useBridgeTracing();
100+
const execHooks = plugin.onExecute({
101+
args: { contextValue: { __bridgeTracer: { traces } } },
102+
} as any);
103+
let updated: any;
104+
105+
execHooks?.onExecuteDone?.({
106+
result: { data: { ok: true } },
107+
setResult: (r: any) => {
108+
updated = r;
109+
},
110+
});
111+
112+
assert.deepEqual(updated.extensions.traces, traces);
113+
});
114+
});

packages/bridge/test/bridge-format.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
22
import { describe, test } from "node:test";
33
import {
44
parseBridgeFormat as parseBridge,
5+
parseBridgeDiagnostics,
56
parsePath,
67
serializeBridge,
78
} from "../src/index.ts";
@@ -1282,3 +1283,44 @@ bridge Query.test {
12821283
assert.doesNotThrow(() => parseBridge(serialized));
12831284
});
12841285
});
1286+
1287+
describe("parser diagnostics and serializer edge cases", () => {
1288+
test("parseBridgeDiagnostics reports lexer errors with a range", () => {
1289+
const result = parseBridgeDiagnostics("version 1.5\nbridge Query.x {\n with output as o\n o.x = \"ok\"\n}\n§");
1290+
assert.ok(result.diagnostics.length > 0);
1291+
assert.equal(result.diagnostics[0]?.severity, "error");
1292+
assert.equal(result.diagnostics[0]?.range.start.line, 5);
1293+
assert.equal(result.diagnostics[0]?.range.start.character, 0);
1294+
});
1295+
1296+
test("reserved source identifier is rejected as const name", () => {
1297+
assert.throws(
1298+
() => parseBridge('version 1.5\nconst input = "x"'),
1299+
/reserved source identifier.*const name/i,
1300+
);
1301+
});
1302+
1303+
test("serializeBridge keeps passthrough shorthand", () => {
1304+
const src = "version 1.5\nbridge Query.upper with std.str.toUpperCase";
1305+
const serialized = serializeBridge(parseBridge(src));
1306+
assert.ok(
1307+
serialized.includes("bridge Query.upper with std.str.toUpperCase"),
1308+
serialized,
1309+
);
1310+
});
1311+
1312+
test("serializeBridge uses compact default handle bindings", () => {
1313+
const src = `version 1.5
1314+
bridge Query.defaults {
1315+
with input
1316+
with output
1317+
with const
1318+
1319+
output.value <- input.name
1320+
}`;
1321+
const serialized = serializeBridge(parseBridge(src));
1322+
assert.ok(serialized.includes(" with input\n"), serialized);
1323+
assert.ok(serialized.includes(" with output\n"), serialized);
1324+
assert.ok(serialized.includes(" with const\n"), serialized);
1325+
});
1326+
});

0 commit comments

Comments
 (0)