Skip to content

Commit 610bde6

Browse files
committed
feat: add tests for reference path suggestions in deeply nested and recursive data types
1 parent b66c02a commit 610bde6

1 file changed

Lines changed: 151 additions & 0 deletions

File tree

test/schema/schema.test.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,4 +899,155 @@ describe("Schema", () => {
899899
).toBe(true);
900900
});
901901

902+
it('offers all paths of a deeply nested non-recursive object as ReferenceValue suggestions', () => {
903+
// Node 1: custom::test::deeply_nested(): a 10-level nested object ending in TEXT.
904+
// Node 2: std::text::split(value: TEXT, delimiter: TEXT): LIST<TEXT>
905+
//
906+
// Reference path extraction caps traversal depth only for recursive data
907+
// types. A deeply nested but acyclic object must be traversed exhaustively,
908+
// so even the leaf 10 levels down is offered as a suggestion.
909+
const DEEPLY_NESTED_FN: FunctionDefinition = {
910+
id: "gid://sagittarius/FunctionDefinition/9004",
911+
identifier: "custom::test::deeply_nested",
912+
signature: "(): { l1: { l2: { l3: { l4: { l5: { l6: { l7: { l8: { l9: { l10: TEXT } } } } } } } } } }",
913+
};
914+
915+
const flow: Flow = {
916+
id: "gid://sagittarius/Flow/1",
917+
startingNodeId: "gid://sagittarius/NodeFunction/1",
918+
signature: "(): void",
919+
nodes: {
920+
nodes: [
921+
{
922+
id: "gid://sagittarius/NodeFunction/1",
923+
functionDefinition: {identifier: "custom::test::deeply_nested"},
924+
nextNodeId: "gid://sagittarius/NodeFunction/2",
925+
parameters: {
926+
nodes: [],
927+
},
928+
},
929+
{
930+
id: "gid://sagittarius/NodeFunction/2",
931+
functionDefinition: {identifier: "std::text::split"},
932+
parameters: {
933+
nodes: [
934+
{value: null},
935+
{value: {__typename: "LiteralValue", value: ","}},
936+
],
937+
},
938+
},
939+
],
940+
},
941+
};
942+
943+
const [valueSchema] = getSignatureSchema(
944+
flow,
945+
DATA_TYPES,
946+
[...FUNCTION_SIGNATURES, DEEPLY_NESTED_FN],
947+
"gid://sagittarius/NodeFunction/2",
948+
);
949+
950+
// value: TEXT → free-form text input.
951+
expect(valueSchema.schema.input).toBe("text");
952+
953+
const paths = ((valueSchema.schema.suggestions ?? []) as any[])
954+
.filter(
955+
(s) =>
956+
s.__typename === "ReferenceValue" &&
957+
s.nodeFunctionId === "gid://sagittarius/NodeFunction/1",
958+
)
959+
.map((s) => (s.referencePath ?? []).map((p: any) => p.path).join("."));
960+
961+
expect(paths).toContain("l1.l2.l3.l4.l5.l6.l7.l8.l9.l10");
962+
});
963+
964+
it('cuts cycles and depth-caps traversal of recursive data types', () => {
965+
// Two families of recursive data types, both returned by node 1:
966+
//
967+
// 1. A mutually recursive pair (RECURSIVE_ORDER ↔ RECURSIVE_CUSTOMER), the
968+
// same shape external actions ship for entity graphs (e.g. Shopware's
969+
// order → delivery → order). A branch must stop as soon as a type
970+
// already on it reappears, instead of overflowing the stack.
971+
//
972+
// 2. A cycle of 8 distinct types (CHAIN_1 → … → CHAIN_8 → CHAIN_1). The
973+
// per-branch cycle cut alone would allow simple paths through all 8
974+
// types, so recursive types are additionally depth-capped at 7 levels.
975+
const RECURSIVE_DATA_TYPES = [
976+
{identifier: "RECURSIVE_ORDER", genericKeys: [], type: "{ id: TEXT; customer?: RECURSIVE_CUSTOMER | null }"},
977+
{identifier: "RECURSIVE_CUSTOMER", genericKeys: [], type: "{ name: TEXT; lastOrder?: RECURSIVE_ORDER | null }"},
978+
{identifier: "CHAIN_1", genericKeys: [], type: "{ value: TEXT; next?: CHAIN_2 | null }"},
979+
{identifier: "CHAIN_2", genericKeys: [], type: "{ value: TEXT; next?: CHAIN_3 | null }"},
980+
{identifier: "CHAIN_3", genericKeys: [], type: "{ value: TEXT; next?: CHAIN_4 | null }"},
981+
{identifier: "CHAIN_4", genericKeys: [], type: "{ value: TEXT; next?: CHAIN_5 | null }"},
982+
{identifier: "CHAIN_5", genericKeys: [], type: "{ value: TEXT; next?: CHAIN_6 | null }"},
983+
{identifier: "CHAIN_6", genericKeys: [], type: "{ value: TEXT; next?: CHAIN_7 | null }"},
984+
{identifier: "CHAIN_7", genericKeys: [], type: "{ value: TEXT; next?: CHAIN_8 | null }"},
985+
{identifier: "CHAIN_8", genericKeys: [], type: "{ value: TEXT; next?: CHAIN_1 | null }"},
986+
];
987+
988+
const RECURSIVE_FN: FunctionDefinition = {
989+
id: "gid://sagittarius/FunctionDefinition/9005",
990+
identifier: "custom::test::recursive",
991+
signature: "(): { pair: RECURSIVE_ORDER; chain: CHAIN_1 }",
992+
};
993+
994+
const flow: Flow = {
995+
id: "gid://sagittarius/Flow/1",
996+
startingNodeId: "gid://sagittarius/NodeFunction/1",
997+
signature: "(): void",
998+
nodes: {
999+
nodes: [
1000+
{
1001+
id: "gid://sagittarius/NodeFunction/1",
1002+
functionDefinition: {identifier: "custom::test::recursive"},
1003+
nextNodeId: "gid://sagittarius/NodeFunction/2",
1004+
parameters: {
1005+
nodes: [],
1006+
},
1007+
},
1008+
{
1009+
id: "gid://sagittarius/NodeFunction/2",
1010+
functionDefinition: {identifier: "std::text::split"},
1011+
parameters: {
1012+
nodes: [
1013+
{value: null},
1014+
{value: {__typename: "LiteralValue", value: ","}},
1015+
],
1016+
},
1017+
},
1018+
],
1019+
},
1020+
};
1021+
1022+
const [valueSchema] = getSignatureSchema(
1023+
flow,
1024+
[...DATA_TYPES, ...RECURSIVE_DATA_TYPES],
1025+
[...FUNCTION_SIGNATURES, RECURSIVE_FN],
1026+
"gid://sagittarius/NodeFunction/2",
1027+
);
1028+
1029+
// value: TEXT → free-form text input.
1030+
expect(valueSchema.schema.input).toBe("text");
1031+
1032+
const paths = ((valueSchema.schema.suggestions ?? []) as any[])
1033+
.filter(
1034+
(s) =>
1035+
s.__typename === "ReferenceValue" &&
1036+
s.nodeFunctionId === "gid://sagittarius/NodeFunction/1",
1037+
)
1038+
.map((s) => (s.referencePath ?? []).map((p: any) => p.path).join("."));
1039+
1040+
// Properties inside the cycle are still reachable …
1041+
expect(paths).toContain("pair.id");
1042+
expect(paths).toContain("pair.customer.name");
1043+
// … but the branch stops where RECURSIVE_ORDER would reappear on it.
1044+
expect(paths).not.toContain("pair.customer.lastOrder.id");
1045+
1046+
// The chain is walked through distinct types up to the depth cap of 7
1047+
// (chain + 5×next + value) …
1048+
expect(paths).toContain("chain.next.next.next.next.next.value");
1049+
// … and no further, even though the next type would still be new to the branch.
1050+
expect(paths).not.toContain("chain.next.next.next.next.next.next.value");
1051+
});
1052+
9021053
})

0 commit comments

Comments
 (0)