package exporter import ( "bytes" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zzet/gortex/internal/graph" ) func TestCypher_NodeShape(t *testing.T) { g := buildSampleGraph(t) var buf bytes.Buffer _, err := WriteCypher(&buf, g, Options{}) require.NoError(t, err) out := buf.String() // Each node should appear as a labeled CREATE. assert.Contains(t, out, "CREATE (:Function:GortexNode") assert.Contains(t, out, "CREATE (:Method:GortexNode") assert.Contains(t, out, "CREATE (:Field:GortexNode") // Required props on every node. assert.Contains(t, out, "id: 'main.go::F'") assert.Contains(t, out, "name: 'F'") assert.Contains(t, out, "qual_name: 'example.com/m.F'") assert.Contains(t, out, "file_path: 'main.go'") assert.Contains(t, out, "start_line: 3") assert.Contains(t, out, "language: 'go'") assert.Contains(t, out, "repo_prefix: 'example'") // Meta keys flattened onto the node, JSON for nested values. assert.Contains(t, out, "visibility: 'public'") assert.Contains(t, out, "complexity: 4") assert.Contains(t, out, `tags: '["hot","core"]'`) // Pure-statement format: no comment lines, no DDL. The first line of // the output should be a CREATE statement, not a `//` header. assert.NotContains(t, out, "//", "output must not contain comments — Memgraph's .cypherl loader rejects them") assert.NotContains(t, out, "CREATE INDEX", "DDL syntax differs between Neo4j and Memgraph; keep the file portable") assert.True(t, strings.HasPrefix(out, "CREATE (:"), "first line should be a CREATE statement; got: %q", out[:min(60, len(out))]) } func TestCypher_EdgeShape(t *testing.T) { g := buildSampleGraph(t) var buf bytes.Buffer _, err := WriteCypher(&buf, g, Options{}) require.NoError(t, err) out := buf.String() assert.Contains(t, out, "MATCH (a:GortexNode {id: 'main.go::F'}), (b:GortexNode {id: 'lib.go::Logger.Print'}) CREATE (a)-[:CALLS") assert.Contains(t, out, "confidence: 1") assert.Contains(t, out, "confidence_label: 'EXTRACTED'") assert.Contains(t, out, "origin: 'ast_resolved'") assert.Contains(t, out, "line: 4") // Writes edge with meta. assert.Contains(t, out, ":WRITES") assert.Contains(t, out, "receiver_type: 'Logger'") assert.Contains(t, out, "origin: 'ast_inferred'") } func TestCypher_StringEscaping(t *testing.T) { g := graph.New() g.AddNode(&graph.Node{ ID: "x.go::Quote", Kind: graph.KindFunction, Name: "Quote", FilePath: "x.go", StartLine: 1, EndLine: 1, Language: "go", Meta: map[string]any{ "doc": "It's a \"thing\"\n\twith escapes.", }, }) var buf bytes.Buffer _, err := WriteCypher(&buf, g, Options{}) require.NoError(t, err) out := buf.String() // Single-quote should be backslash-escaped. assert.Contains(t, out, `\'`) // Newline should be \n literal in the source, not an actual newline inside the string. assert.Contains(t, out, `\n`) assert.Contains(t, out, `\t`) } func TestCypher_StatsCount(t *testing.T) { g := buildSampleGraph(t) var buf bytes.Buffer stats, err := WriteCypher(&buf, g, Options{}) require.NoError(t, err) assert.Equal(t, 3, stats.NodesWritten) assert.Equal(t, 2, stats.EdgesWritten) assert.Equal(t, int64(buf.Len()), stats.BytesWritten) } func TestCypher_EmptyGraph(t *testing.T) { g := graph.New() var buf bytes.Buffer stats, err := WriteCypher(&buf, g, Options{}) require.NoError(t, err) assert.Equal(t, 0, stats.NodesWritten) assert.Equal(t, 0, stats.EdgesWritten) // Empty graph produces an empty file. .cypherl loaders accept this; // any header / DDL is the CLI's responsibility (printed to stderr). assert.Empty(t, buf.String()) } func TestCypher_FilterRemovesDanglingEdges(t *testing.T) { g := buildSampleGraph(t) var buf bytes.Buffer _, err := WriteCypher(&buf, g, Options{Kinds: []graph.NodeKind{graph.KindFunction}}) require.NoError(t, err) out := buf.String() assert.Contains(t, out, "CREATE (:Function:GortexNode") assert.NotContains(t, out, "CREATE (:Method:GortexNode") assert.NotContains(t, out, ":CALLS", "edges with excluded endpoints must be dropped, not produce dangling MATCH") } func TestCypher_PropertyNameSanitization(t *testing.T) { g := graph.New() g.AddNode(&graph.Node{ ID: "x.go::F", Kind: graph.KindFunction, Name: "F", FilePath: "x.go", StartLine: 1, EndLine: 1, Language: "go", Meta: map[string]any{ "with-dash": 1, "with space": 2, "normal": 3, "@@@invalid": 4, // will sanitize to "___invalid" "meta.dotted": 5, }, }) var buf bytes.Buffer _, err := WriteCypher(&buf, g, Options{}) require.NoError(t, err) out := buf.String() assert.Contains(t, out, "with_dash: 1") assert.Contains(t, out, "with_space: 2") assert.Contains(t, out, "normal: 3") assert.Contains(t, out, "meta_dotted: 5") // Hyphen / space / dot should never appear inside property keys. for _, line := range strings.Split(out, "\n") { // We only care about the property-map portion of CREATE statements. if !strings.Contains(line, "CREATE (:") { continue } // A key always looks like ` :`. None of our sanitized keys // should contain a non-identifier character. assert.NotContains(t, line, " with-dash:") assert.NotContains(t, line, " with space:") assert.NotContains(t, line, " meta.dotted:") } }