Skip to content

Commit 2e3f1c0

Browse files
committed
feat: add tests for nullable references in flow validation and schema suggestions
1 parent 8176615 commit 2e3f1c0

2 files changed

Lines changed: 253 additions & 2 deletions

File tree

test/flowValidation.test.ts

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {describe, expect, it} from 'vitest';
22
import {getFlowValidation} from '../src/validation/getFlowValidation';
3-
import {Flow} from "@code0-tech/sagittarius-graphql-types"; // Pfad ggf. anpassen
3+
import {Flow, FunctionDefinition} from "@code0-tech/sagittarius-graphql-types"; // Pfad ggf. anpassen
44
// @ts-ignore
55
import {DATA_TYPES, FUNCTION_SIGNATURES} from "./data";
66

@@ -1326,4 +1326,118 @@ describe('getFlowValidation - Integrationstest', () => {
13261326
]);
13271327
});
13281328

1329+
describe('nullable references into non-nullable parameters', () => {
1330+
1331+
// custom::text::nullable(): TEXT | null — no parameters, returns a nullable string.
1332+
const NULLABLE_TEXT_FN: FunctionDefinition = {
1333+
id: "gid://sagittarius/FunctionDefinition/9001",
1334+
identifier: "custom::text::nullable",
1335+
signature: "(): TEXT | null",
1336+
};
1337+
1338+
// custom::text::nullable_object(): {text?: TEXT | null} — returns an object whose
1339+
// `text` property is optional and nullable.
1340+
const NULLABLE_OBJECT_FN: FunctionDefinition = {
1341+
id: "gid://sagittarius/FunctionDefinition/9002",
1342+
identifier: "custom::text::nullable_object",
1343+
signature: "(): {text?: TEXT | null}",
1344+
};
1345+
1346+
// custom::number::nullable(): NUMBER | null — nullable, but the base type is
1347+
// a number, so it must stay incompatible with a TEXT parameter.
1348+
const NULLABLE_NUMBER_FN: FunctionDefinition = {
1349+
id: "gid://sagittarius/FunctionDefinition/9003",
1350+
identifier: "custom::number::nullable",
1351+
signature: "(): NUMBER | null",
1352+
};
1353+
1354+
const CUSTOM_FUNCTIONS = [
1355+
...FUNCTION_SIGNATURES,
1356+
NULLABLE_TEXT_FN,
1357+
NULLABLE_OBJECT_FN,
1358+
NULLABLE_NUMBER_FN,
1359+
];
1360+
1361+
// Builds a two-node flow: node 1 runs `firstIdentifier` (no parameters),
1362+
// node 2 runs std::text::split(value: TEXT, delimiter: TEXT) with `valueRef`
1363+
// as its `value` argument.
1364+
const buildFlow = (firstIdentifier: string, valueRef: any): Flow => ({
1365+
id: "gid://sagittarius/Flow/1",
1366+
startingNodeId: "gid://sagittarius/NodeFunction/1",
1367+
signature: "(): void",
1368+
nodes: {
1369+
nodes: [
1370+
{
1371+
id: "gid://sagittarius/NodeFunction/1",
1372+
functionDefinition: {identifier: firstIdentifier},
1373+
nextNodeId: "gid://sagittarius/NodeFunction/2",
1374+
parameters: {
1375+
nodes: [],
1376+
},
1377+
},
1378+
{
1379+
id: "gid://sagittarius/NodeFunction/2",
1380+
functionDefinition: {identifier: "std::text::split"},
1381+
parameters: {
1382+
nodes: [
1383+
{value: valueRef},
1384+
{value: {__typename: "LiteralValue", value: ","}},
1385+
],
1386+
},
1387+
},
1388+
],
1389+
},
1390+
});
1391+
1392+
it('accepts a TEXT | null reference for a plain TEXT parameter', () => {
1393+
// Node 1 returns TEXT | null; node 2's `value` parameter wants TEXT.
1394+
// The nullish part of the reference must not fail the validation.
1395+
const flow = buildFlow("custom::text::nullable", {
1396+
__typename: "ReferenceValue",
1397+
nodeFunctionId: "gid://sagittarius/NodeFunction/1",
1398+
});
1399+
1400+
const result = getFlowValidation(flow, CUSTOM_FUNCTIONS, DATA_TYPES);
1401+
1402+
expect(result.diagnostics).toHaveLength(0);
1403+
expect(result.isValid).toBe(true);
1404+
});
1405+
1406+
it('accepts a nullable object property reference for a plain TEXT parameter', () => {
1407+
// Node 1 returns {text?: TEXT | null}; the reference path drills into `text`
1408+
// (TEXT | null | undefined). The nullish part must not fail the validation.
1409+
const flow = buildFlow("custom::text::nullable_object", {
1410+
__typename: "ReferenceValue",
1411+
nodeFunctionId: "gid://sagittarius/NodeFunction/1",
1412+
referencePath: [{path: "text"}],
1413+
});
1414+
1415+
const result = getFlowValidation(flow, CUSTOM_FUNCTIONS, DATA_TYPES);
1416+
1417+
expect(result.diagnostics).toHaveLength(0);
1418+
expect(result.isValid).toBe(true);
1419+
});
1420+
1421+
it('still rejects a NUMBER | null reference for a plain TEXT parameter', () => {
1422+
// Ignoring the nullish part must not make the base types interchangeable:
1423+
// NUMBER | null stripped to NUMBER is still not a TEXT.
1424+
const flow = buildFlow("custom::number::nullable", {
1425+
__typename: "ReferenceValue",
1426+
nodeFunctionId: "gid://sagittarius/NodeFunction/1",
1427+
});
1428+
1429+
const result = getFlowValidation(flow, CUSTOM_FUNCTIONS, DATA_TYPES);
1430+
1431+
expect(result.isValid).toBe(false);
1432+
expect(result.diagnostics).toEqual(expect.arrayContaining([
1433+
expect.objectContaining({
1434+
nodeId: "gid://sagittarius/NodeFunction/2",
1435+
parameterIndex: 0,
1436+
severity: "error",
1437+
}),
1438+
]));
1439+
});
1440+
1441+
});
1442+
13291443
});

test/schema/schema.test.ts

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {describe, expect, it} from "vitest";
2-
import {Flow} from "@code0-tech/sagittarius-graphql-types";
2+
import {Flow, FunctionDefinition} from "@code0-tech/sagittarius-graphql-types";
33
import {getSignatureSchema, getTypeSchema} from "../../src";
44
import {DATA_TYPES, FUNCTION_SIGNATURES} from "../data";
55

@@ -688,4 +688,141 @@ describe("Schema", () => {
688688
).toBe(true);
689689
});
690690

691+
it('offers a nullable TEXT return as a ReferenceValue suggestion for a plain TEXT parameter', () => {
692+
// Node 1: custom::text::nullable(): TEXT | null — no parameters, returns a
693+
// nullable string.
694+
// Node 2: std::text::split(value: TEXT, delimiter: TEXT): LIST<TEXT>
695+
//
696+
// The `value` parameter is declared as plain TEXT (string). Node 1's return
697+
// type is string | null. Strict assignability would reject string | null → string,
698+
// but the editor must still offer node 1 as a ReferenceValue suggestion here:
699+
// the nullish part of a reference's type should be ignored for suggestion scoping.
700+
const NULLABLE_TEXT_FN: FunctionDefinition = {
701+
id: "gid://sagittarius/FunctionDefinition/9001",
702+
identifier: "custom::text::nullable",
703+
signature: "(): TEXT | null",
704+
};
705+
706+
const flow: Flow = {
707+
id: "gid://sagittarius/Flow/1",
708+
startingNodeId: "gid://sagittarius/NodeFunction/1",
709+
signature: "(): void",
710+
nodes: {
711+
nodes: [
712+
{
713+
id: "gid://sagittarius/NodeFunction/1",
714+
functionDefinition: {identifier: "custom::text::nullable"},
715+
nextNodeId: "gid://sagittarius/NodeFunction/2",
716+
parameters: {
717+
nodes: [],
718+
},
719+
},
720+
{
721+
id: "gid://sagittarius/NodeFunction/2",
722+
functionDefinition: {identifier: "std::text::split"},
723+
parameters: {
724+
nodes: [
725+
{value: null},
726+
{value: {__typename: "LiteralValue", value: ","}},
727+
],
728+
},
729+
},
730+
],
731+
},
732+
};
733+
734+
const [valueSchema] = getSignatureSchema(
735+
flow,
736+
DATA_TYPES,
737+
[...FUNCTION_SIGNATURES, NULLABLE_TEXT_FN],
738+
"gid://sagittarius/NodeFunction/2",
739+
);
740+
741+
// value: TEXT → free-form text input.
742+
expect(valueSchema.schema.input).toBe("text");
743+
744+
// Node 1 returns TEXT | null; stripping the nullish part leaves TEXT, which is
745+
// assignable to the TEXT parameter. Its return value is a direct reference
746+
// (no property path).
747+
const suggestions = (valueSchema.schema.suggestions ?? []) as any[];
748+
expect(
749+
suggestions.some(
750+
(s) =>
751+
s.__typename === "ReferenceValue" &&
752+
s.nodeFunctionId === "gid://sagittarius/NodeFunction/1" &&
753+
!s.referencePath,
754+
),
755+
).toBe(true);
756+
});
757+
758+
it('offers a nullable object property as a ReferenceValue path suggestion for a plain TEXT parameter', () => {
759+
// Node 1: custom::text::nullable_object(): {text?: TEXT | null} — no parameters,
760+
// returns an object whose `text` property is optional and nullable.
761+
// Node 2: std::text::split(value: TEXT, delimiter: TEXT): LIST<TEXT>
762+
//
763+
// The `value` parameter is declared as plain TEXT (string). Node 1's `text`
764+
// property has type string | null | undefined. Strict assignability would
765+
// reject it, but the editor must still offer node 1's `text` property as a
766+
// ReferenceValue suggestion with referencePath [{path: "text"}]: the nullish
767+
// part of a reference's type should be ignored for suggestion scoping.
768+
const NULLABLE_OBJECT_FN: FunctionDefinition = {
769+
id: "gid://sagittarius/FunctionDefinition/9002",
770+
identifier: "custom::text::nullable_object",
771+
signature: "(): {text?: TEXT | null}",
772+
};
773+
774+
const flow: Flow = {
775+
id: "gid://sagittarius/Flow/1",
776+
startingNodeId: "gid://sagittarius/NodeFunction/1",
777+
signature: "(): void",
778+
nodes: {
779+
nodes: [
780+
{
781+
id: "gid://sagittarius/NodeFunction/1",
782+
functionDefinition: {identifier: "custom::text::nullable_object"},
783+
nextNodeId: "gid://sagittarius/NodeFunction/2",
784+
parameters: {
785+
nodes: [],
786+
},
787+
},
788+
{
789+
id: "gid://sagittarius/NodeFunction/2",
790+
functionDefinition: {identifier: "std::text::split"},
791+
parameters: {
792+
nodes: [
793+
{value: null},
794+
{value: {__typename: "LiteralValue", value: ","}},
795+
],
796+
},
797+
},
798+
],
799+
},
800+
};
801+
802+
const [valueSchema] = getSignatureSchema(
803+
flow,
804+
DATA_TYPES,
805+
[...FUNCTION_SIGNATURES, NULLABLE_OBJECT_FN],
806+
"gid://sagittarius/NodeFunction/2",
807+
);
808+
809+
// value: TEXT → free-form text input.
810+
expect(valueSchema.schema.input).toBe("text");
811+
812+
// Node 1's `text` property is TEXT | null | undefined; stripping the nullish
813+
// part leaves TEXT, which is assignable to the TEXT parameter → node 1 must
814+
// be offered with referencePath [{path: "text"}].
815+
const suggestions = (valueSchema.schema.suggestions ?? []) as any[];
816+
expect(
817+
suggestions.some(
818+
(s) =>
819+
s.__typename === "ReferenceValue" &&
820+
s.nodeFunctionId === "gid://sagittarius/NodeFunction/1" &&
821+
Array.isArray(s.referencePath) &&
822+
s.referencePath.length === 1 &&
823+
s.referencePath[0].path === "text",
824+
),
825+
).toBe(true);
826+
});
827+
691828
})

0 commit comments

Comments
 (0)