Files
Cloud-Tour-to-Libo/admin-web/tests/schemaCapabilities.test.ts
T

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" },
]);
});