151 lines
5.6 KiB
TypeScript
151 lines
5.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
buildGenericGraphQueryTemplates,
|
|
buildSchemaGuidance,
|
|
GENERIC_GRAPH_DEFAULT_CYPHER,
|
|
GENERIC_GRAPH_RELATIONS_CYPHER,
|
|
genericGraphQueryPlaceholder,
|
|
schemaHasDeclaredTypes,
|
|
schemaSupportsLegacyGraphProfile,
|
|
} from "../src/schemaCapabilities.ts";
|
|
|
|
const genericSchema = {
|
|
namespace: "human_flow_e2e_20260804_224229",
|
|
version: "1.0.0",
|
|
entity_types: {
|
|
Place: { label: "地点", fields: { name: { type: "string" } } },
|
|
Hotel: { label: "酒店", fields: { name: { type: "string" } } },
|
|
},
|
|
relation_types: {
|
|
LOCATED_IN: { from: "Place", to: "Place", properties: {} },
|
|
NEAR: { from: "Hotel", to: "Place", properties: {} },
|
|
},
|
|
};
|
|
|
|
test("generic schemas do not show fixed-route guidance", () => {
|
|
assert.deepEqual(buildSchemaGuidance(genericSchema), []);
|
|
assert.equal(schemaHasDeclaredTypes(genericSchema), true);
|
|
assert.equal(schemaSupportsLegacyGraphProfile(genericSchema, "travel"), false);
|
|
assert.equal(schemaSupportsLegacyGraphProfile(genericSchema, "travel_item"), false);
|
|
assert.equal(schemaSupportsLegacyGraphProfile(genericSchema, "libo"), false);
|
|
});
|
|
|
|
test("generic default query preserves isolated nodes when the schema has no relations", () => {
|
|
const zeroRelationSchema = {
|
|
namespace: "isolated_nodes",
|
|
entity_types: {
|
|
Place: { label: "地点", fields: { name: { type: "string" } } },
|
|
},
|
|
relation_types: {},
|
|
};
|
|
|
|
assert.equal(schemaHasDeclaredTypes(zeroRelationSchema), true);
|
|
assert.equal(
|
|
GENERIC_GRAPH_DEFAULT_CYPHER,
|
|
"MATCH (n) OPTIONAL MATCH (n)-[r]->(m) RETURN n,r,m LIMIT 2500",
|
|
);
|
|
assert.deepEqual(buildGenericGraphQueryTemplates(zeroRelationSchema).slice(0, 2), [
|
|
{ label: "全部节点", cypher: "MATCH (n) RETURN n LIMIT 200" },
|
|
{ label: "全部节点关系", cypher: GENERIC_GRAPH_RELATIONS_CYPHER },
|
|
]);
|
|
});
|
|
|
|
test("Libo shortcuts require the specialized POI model shape", () => {
|
|
assert.equal(schemaSupportsLegacyGraphProfile({
|
|
entity_types: {
|
|
Hotel: {},
|
|
FoodPlace: {},
|
|
TransitFacility: {},
|
|
ScenicSpot: {},
|
|
},
|
|
relation_types: {},
|
|
}, "libo"), true);
|
|
});
|
|
|
|
test("travel guidance only includes capabilities declared by that schema", () => {
|
|
const travelSchema = {
|
|
design_principles: ["主干路线固定且有序。"],
|
|
entity_types: {
|
|
TourProduct: {},
|
|
ProductDay: {},
|
|
RouteStop: {},
|
|
ScenicAttraction: {},
|
|
TravelItem: {},
|
|
ResourcePriceRule: {},
|
|
},
|
|
relation_types: {
|
|
DAY_HAS_STOP: { from: "ProductDay", to: "RouteStop" },
|
|
ROUTE_STOP_NEXT: { from: "RouteStop", to: "RouteStop" },
|
|
STOP_VISITS_ATTRACTION: { from: "RouteStop", to: "ScenicAttraction" },
|
|
STOP_USES_DEFAULT_RESOURCE: { from: "RouteStop", to: "TravelItem" },
|
|
ATTRACTION_NEARBY_RESOURCE: { from: "ScenicAttraction", to: "Hotel" },
|
|
},
|
|
};
|
|
|
|
assert.deepEqual(buildSchemaGuidance(travelSchema), [
|
|
"固定路线不可改",
|
|
"RouteStop 串联景区",
|
|
"默认资源挂停靠点",
|
|
"可替换资源走 nearby",
|
|
"价格统一用 ResourcePriceRule",
|
|
]);
|
|
assert.equal(schemaSupportsLegacyGraphProfile(travelSchema, "travel"), true);
|
|
assert.equal(schemaSupportsLegacyGraphProfile(travelSchema, "travel_item"), true);
|
|
});
|
|
|
|
test("generic graph shortcuts come from the active schema", () => {
|
|
const templates = buildGenericGraphQueryTemplates({ schema_jsonb: genericSchema });
|
|
|
|
assert.deepEqual(templates, [
|
|
{ label: "全部节点", cypher: "MATCH (n) RETURN n LIMIT 200" },
|
|
{ label: "全部节点关系", cypher: "MATCH (n)-[r]->(m) RETURN n,r,m LIMIT 2500" },
|
|
{ label: "地点(Place)节点", cypher: "MATCH (n:`Place`) RETURN n LIMIT 100" },
|
|
{ label: "酒店(Hotel)节点", cypher: "MATCH (n:`Hotel`) RETURN n LIMIT 100" },
|
|
{
|
|
label: "LOCATED_IN 关系",
|
|
cypher: "MATCH (a:`Place`)-[r:`LOCATED_IN`]->(b:`Place`) RETURN a,r,b LIMIT 100",
|
|
},
|
|
{
|
|
label: "NEAR 关系",
|
|
cypher: "MATCH (a:`Hotel`)-[r:`NEAR`]->(b:`Place`) RETURN a,r,b LIMIT 100",
|
|
},
|
|
]);
|
|
assert.equal(JSON.stringify(templates).includes("花溪"), false);
|
|
assert.equal(JSON.stringify(templates).includes("Area"), false);
|
|
assert.equal(genericGraphQueryPlaceholder(genericSchema), "输入自然语言或 Cypher,例如:查询 Place、Hotel 节点");
|
|
});
|
|
|
|
test("unsafe schema identifiers are never interpolated into shortcuts", () => {
|
|
const templates = buildGenericGraphQueryTemplates({
|
|
entity_types: {
|
|
"Place`) MATCH (secret": {},
|
|
SafeType: {},
|
|
},
|
|
relation_types: {
|
|
"REL`]->(secret": { from: "SafeType", to: "SafeType" },
|
|
SAFE_REL: { from: "SafeType", to: "Bad|Union" },
|
|
},
|
|
});
|
|
const allQueries = templates.map(({ cypher }) => cypher).join("\n");
|
|
|
|
assert.equal(allQueries.includes("secret"), false);
|
|
assert.equal(allQueries.includes("Bad|Union"), false);
|
|
assert.match(allQueries, /`SafeType`/);
|
|
assert.match(allQueries, /`SAFE_REL`/);
|
|
});
|
|
|
|
test("graph counts provide safe shortcuts when the schema endpoint is unavailable", () => {
|
|
assert.deepEqual(buildGenericGraphQueryTemplates({}, {
|
|
label_counts: [{ label: "Place", count: 2 }, { label: "Hotel", count: 1 }],
|
|
relationship_type_counts: [{ type: "NEAR", count: 1 }],
|
|
}), [
|
|
{ label: "全部节点", cypher: "MATCH (n) RETURN n LIMIT 200" },
|
|
{ label: "全部节点关系", cypher: "MATCH (n)-[r]->(m) RETURN n,r,m LIMIT 2500" },
|
|
{ label: "Place 节点", cypher: "MATCH (n:`Place`) RETURN n LIMIT 100" },
|
|
{ label: "Hotel 节点", cypher: "MATCH (n:`Hotel`) RETURN n LIMIT 100" },
|
|
{ label: "NEAR 关系", cypher: "MATCH (a)-[r:`NEAR`]->(b) RETURN a,r,b LIMIT 100" },
|
|
]);
|
|
});
|