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