Skip to content

Commit 6b2b1d1

Browse files
committed
test: registry env-var loading + dbt lineage helpers
Cover two previously untested code paths: - ConnectionRegistry loadFromEnv() parsing for CI/CD environments - dbtLineage() model lookup, dialect detection, and error handling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> https://claude.ai/code/session_011ZpwXLNnjySge6t7CBbcrW
1 parent abcaa1d commit 6b2b1d1

2 files changed

Lines changed: 497 additions & 0 deletions

File tree

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
/**
2+
* Tests for dbt lineage helper functions: findModel, detectDialect,
3+
* buildSchemaContext, and the top-level dbtLineage() error paths.
4+
*
5+
* These pure functions parse manifest data and build schema contexts
6+
* for column-level lineage analysis. Zero tests existed previously.
7+
* A bug in findModel or buildSchemaContext causes lineage to silently
8+
* return empty results, which users see as "no lineage available".
9+
*/
10+
11+
import { describe, test, expect, afterEach } from "bun:test"
12+
import { dbtLineage } from "../../src/altimate/native/dbt/lineage"
13+
import { writeFileSync, mkdtempSync, rmSync } from "fs"
14+
import { tmpdir } from "os"
15+
import { join } from "path"
16+
17+
// ---------------------------------------------------------------------------
18+
// Helpers
19+
// ---------------------------------------------------------------------------
20+
21+
let tmpDirs: string[] = []
22+
23+
function makeTmpDir(): string {
24+
const dir = mkdtempSync(join(tmpdir(), "dbt-lineage-test-"))
25+
tmpDirs.push(dir)
26+
return dir
27+
}
28+
29+
function writeManifest(dir: string, manifest: Record<string, any>): string {
30+
const manifestPath = join(dir, "manifest.json")
31+
writeFileSync(manifestPath, JSON.stringify(manifest))
32+
return manifestPath
33+
}
34+
35+
afterEach(() => {
36+
for (const dir of tmpDirs) {
37+
rmSync(dir, { recursive: true, force: true })
38+
}
39+
tmpDirs = []
40+
})
41+
42+
// ---------------------------------------------------------------------------
43+
// Minimal manifest fixtures
44+
// ---------------------------------------------------------------------------
45+
46+
const BASE_MANIFEST = {
47+
metadata: { adapter_type: "snowflake" },
48+
nodes: {
49+
"model.proj.orders": {
50+
resource_type: "model",
51+
name: "orders",
52+
schema: "public",
53+
database: "analytics",
54+
config: { materialized: "table" },
55+
compiled_code: "SELECT c.id, c.name FROM customers c",
56+
depends_on: { nodes: ["source.proj.raw.customers"] },
57+
columns: {
58+
id: { name: "id", data_type: "INTEGER" },
59+
name: { name: "name", data_type: "VARCHAR" },
60+
},
61+
},
62+
"model.proj.revenue": {
63+
resource_type: "model",
64+
name: "revenue",
65+
compiled_code: "SELECT SUM(amount) AS total FROM orders",
66+
depends_on: { nodes: ["model.proj.orders"] },
67+
columns: {},
68+
},
69+
"test.proj.not_null": {
70+
resource_type: "test",
71+
name: "not_null",
72+
},
73+
},
74+
sources: {
75+
"source.proj.raw.customers": {
76+
name: "customers",
77+
source_name: "raw",
78+
schema: "raw_data",
79+
database: "analytics",
80+
columns: {
81+
id: { name: "id", data_type: "INTEGER" },
82+
name: { name: "name", data_type: "VARCHAR" },
83+
email: { name: "email", data_type: "VARCHAR" },
84+
},
85+
},
86+
},
87+
}
88+
89+
// ---------------------------------------------------------------------------
90+
// 1. Model lookup (findModel)
91+
// ---------------------------------------------------------------------------
92+
93+
describe("dbtLineage: model lookup", () => {
94+
test("finds model by unique_id", () => {
95+
const dir = makeTmpDir()
96+
const manifestPath = writeManifest(dir, BASE_MANIFEST)
97+
98+
const result = dbtLineage({
99+
manifest_path: manifestPath,
100+
model: "model.proj.orders",
101+
})
102+
103+
expect(result.model_name).toBe("orders")
104+
expect(result.model_unique_id).toBe("model.proj.orders")
105+
expect(result.compiled_sql).toContain("SELECT")
106+
})
107+
108+
test("finds model by short name", () => {
109+
const dir = makeTmpDir()
110+
const manifestPath = writeManifest(dir, BASE_MANIFEST)
111+
112+
const result = dbtLineage({
113+
manifest_path: manifestPath,
114+
model: "orders",
115+
})
116+
117+
expect(result.model_name).toBe("orders")
118+
expect(result.model_unique_id).toBe("model.proj.orders")
119+
})
120+
121+
test("returns low confidence when model not found", () => {
122+
const dir = makeTmpDir()
123+
const manifestPath = writeManifest(dir, BASE_MANIFEST)
124+
125+
const result = dbtLineage({
126+
manifest_path: manifestPath,
127+
model: "nonexistent_model",
128+
})
129+
130+
expect(result.confidence).toBe("low")
131+
expect(result.confidence_factors).toContain("Model 'nonexistent_model' not found in manifest")
132+
})
133+
134+
test("does not match test or seed nodes by name", () => {
135+
const dir = makeTmpDir()
136+
const manifestPath = writeManifest(dir, BASE_MANIFEST)
137+
138+
const result = dbtLineage({
139+
manifest_path: manifestPath,
140+
model: "not_null",
141+
})
142+
143+
// "not_null" is a test node, not a model — should not be found
144+
expect(result.confidence).toBe("low")
145+
expect(result.confidence_factors[0]).toContain("not found in manifest")
146+
})
147+
})
148+
149+
// ---------------------------------------------------------------------------
150+
// 2. Dialect detection (detectDialect)
151+
// ---------------------------------------------------------------------------
152+
153+
describe("dbtLineage: dialect detection", () => {
154+
test("detects dialect from manifest metadata.adapter_type", () => {
155+
const dir = makeTmpDir()
156+
const manifest = {
157+
...BASE_MANIFEST,
158+
metadata: { adapter_type: "bigquery" },
159+
}
160+
const manifestPath = writeManifest(dir, manifest)
161+
162+
const result = dbtLineage({
163+
manifest_path: manifestPath,
164+
model: "orders",
165+
})
166+
167+
// We can't directly check dialect, but the result shouldn't error
168+
// due to dialect mismatch. The model has compiled_code, so confidence
169+
// should be high if lineage succeeds or reflect the actual error.
170+
expect(result.model_name).toBe("orders")
171+
})
172+
173+
test("explicit dialect param overrides auto-detection", () => {
174+
const dir = makeTmpDir()
175+
const manifestPath = writeManifest(dir, BASE_MANIFEST)
176+
177+
const result = dbtLineage({
178+
manifest_path: manifestPath,
179+
model: "orders",
180+
dialect: "postgres",
181+
})
182+
183+
// Should not throw regardless of dialect choice
184+
expect(result.model_name).toBe("orders")
185+
})
186+
187+
test("defaults to snowflake when adapter_type is missing", () => {
188+
const dir = makeTmpDir()
189+
const manifest = {
190+
...BASE_MANIFEST,
191+
metadata: {},
192+
}
193+
const manifestPath = writeManifest(dir, manifest)
194+
195+
const result = dbtLineage({
196+
manifest_path: manifestPath,
197+
model: "orders",
198+
})
199+
200+
// Should not throw — defaults to snowflake
201+
expect(result.model_name).toBe("orders")
202+
})
203+
})
204+
205+
// ---------------------------------------------------------------------------
206+
// 3. Schema context building (buildSchemaContext)
207+
// ---------------------------------------------------------------------------
208+
209+
describe("dbtLineage: schema context from upstream deps", () => {
210+
test("builds context from source with columns", () => {
211+
const dir = makeTmpDir()
212+
const manifestPath = writeManifest(dir, BASE_MANIFEST)
213+
214+
const result = dbtLineage({
215+
manifest_path: manifestPath,
216+
model: "orders",
217+
})
218+
219+
// The orders model depends on source.proj.raw.customers which has columns.
220+
// If schema context was built correctly, lineage should have non-empty output.
221+
expect(result.model_name).toBe("orders")
222+
// compiled_sql should be present
223+
expect(result.compiled_sql).toBeDefined()
224+
expect(result.compiled_sql).toContain("SELECT")
225+
})
226+
227+
test("handles model with no upstream columns gracefully", () => {
228+
const dir = makeTmpDir()
229+
// Revenue depends on orders, but orders has columns — so context should build.
230+
// Create a model that depends on a node with no columns.
231+
const manifest = {
232+
...BASE_MANIFEST,
233+
nodes: {
234+
...BASE_MANIFEST.nodes,
235+
"model.proj.bare": {
236+
resource_type: "model",
237+
name: "bare",
238+
compiled_code: "SELECT 1 AS val",
239+
depends_on: { nodes: ["model.proj.no_cols"] },
240+
columns: {},
241+
},
242+
"model.proj.no_cols": {
243+
resource_type: "model",
244+
name: "no_cols",
245+
compiled_code: "SELECT 1",
246+
depends_on: { nodes: [] },
247+
columns: {},
248+
},
249+
},
250+
}
251+
const manifestPath = writeManifest(dir, manifest)
252+
253+
const result = dbtLineage({
254+
manifest_path: manifestPath,
255+
model: "bare",
256+
})
257+
258+
// Should not crash — just returns with whatever lineage can determine
259+
expect(result.model_name).toBe("bare")
260+
expect(result.compiled_sql).toBe("SELECT 1 AS val")
261+
})
262+
})
263+
264+
// ---------------------------------------------------------------------------
265+
// 4. Error paths
266+
// ---------------------------------------------------------------------------
267+
268+
describe("dbtLineage: error handling", () => {
269+
test("returns low confidence for non-existent manifest", () => {
270+
const result = dbtLineage({
271+
manifest_path: "/tmp/definitely-not-a-manifest.json",
272+
model: "orders",
273+
})
274+
275+
expect(result.confidence).toBe("low")
276+
expect(result.confidence_factors).toContain("Manifest file not found")
277+
expect(result.raw_lineage).toEqual({})
278+
})
279+
280+
test("returns low confidence for invalid JSON manifest", () => {
281+
const dir = makeTmpDir()
282+
const manifestPath = join(dir, "manifest.json")
283+
writeFileSync(manifestPath, "not valid json {{{")
284+
285+
const result = dbtLineage({
286+
manifest_path: manifestPath,
287+
model: "orders",
288+
})
289+
290+
expect(result.confidence).toBe("low")
291+
expect(result.confidence_factors[0]).toContain("Failed to parse manifest")
292+
})
293+
294+
test("returns low confidence when model has no compiled SQL", () => {
295+
const dir = makeTmpDir()
296+
const manifest = {
297+
nodes: {
298+
"model.proj.uncompiled": {
299+
resource_type: "model",
300+
name: "uncompiled",
301+
// No compiled_code or compiled_sql
302+
depends_on: { nodes: [] },
303+
columns: {},
304+
},
305+
},
306+
sources: {},
307+
}
308+
const manifestPath = writeManifest(dir, manifest)
309+
310+
const result = dbtLineage({
311+
manifest_path: manifestPath,
312+
model: "uncompiled",
313+
})
314+
315+
expect(result.confidence).toBe("low")
316+
expect(result.confidence_factors).toContain("No compiled SQL found — run `dbt compile` first")
317+
})
318+
319+
test("handles manifest with no nodes key at all", () => {
320+
const dir = makeTmpDir()
321+
const manifestPath = writeManifest(dir, { metadata: {} })
322+
323+
const result = dbtLineage({
324+
manifest_path: manifestPath,
325+
model: "orders",
326+
})
327+
328+
expect(result.confidence).toBe("low")
329+
})
330+
})

0 commit comments

Comments
 (0)