-
Notifications
You must be signed in to change notification settings - Fork 29
test: registry env-var loading + dbt lineage helpers #495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
+497
−0
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
330 changes: 330 additions & 0 deletions
330
packages/opencode/test/altimate/dbt-lineage-helpers.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,330 @@ | ||
| /** | ||
| * Tests for dbt lineage helper functions: findModel, detectDialect, | ||
| * buildSchemaContext, and the top-level dbtLineage() error paths. | ||
| * | ||
| * These pure functions parse manifest data and build schema contexts | ||
| * for column-level lineage analysis. Zero tests existed previously. | ||
| * A bug in findModel or buildSchemaContext causes lineage to silently | ||
| * return empty results, which users see as "no lineage available". | ||
| */ | ||
|
|
||
| import { describe, test, expect, afterEach } from "bun:test" | ||
| import { dbtLineage } from "../../src/altimate/native/dbt/lineage" | ||
| import { writeFileSync, mkdtempSync, rmSync } from "fs" | ||
| import { tmpdir } from "os" | ||
| import { join } from "path" | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Helpers | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| let tmpDirs: string[] = [] | ||
|
|
||
| function makeTmpDir(): string { | ||
| const dir = mkdtempSync(join(tmpdir(), "dbt-lineage-test-")) | ||
| tmpDirs.push(dir) | ||
| return dir | ||
| } | ||
|
|
||
| function writeManifest(dir: string, manifest: Record<string, any>): string { | ||
| const manifestPath = join(dir, "manifest.json") | ||
| writeFileSync(manifestPath, JSON.stringify(manifest)) | ||
| return manifestPath | ||
| } | ||
|
|
||
| afterEach(() => { | ||
| for (const dir of tmpDirs) { | ||
| rmSync(dir, { recursive: true, force: true }) | ||
| } | ||
| tmpDirs = [] | ||
| }) | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Minimal manifest fixtures | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| const BASE_MANIFEST = { | ||
| metadata: { adapter_type: "snowflake" }, | ||
| nodes: { | ||
| "model.proj.orders": { | ||
| resource_type: "model", | ||
| name: "orders", | ||
| schema: "public", | ||
| database: "analytics", | ||
| config: { materialized: "table" }, | ||
| compiled_code: "SELECT c.id, c.name FROM customers c", | ||
| depends_on: { nodes: ["source.proj.raw.customers"] }, | ||
| columns: { | ||
| id: { name: "id", data_type: "INTEGER" }, | ||
| name: { name: "name", data_type: "VARCHAR" }, | ||
| }, | ||
| }, | ||
| "model.proj.revenue": { | ||
| resource_type: "model", | ||
| name: "revenue", | ||
| compiled_code: "SELECT SUM(amount) AS total FROM orders", | ||
| depends_on: { nodes: ["model.proj.orders"] }, | ||
| columns: {}, | ||
| }, | ||
| "test.proj.not_null": { | ||
| resource_type: "test", | ||
| name: "not_null", | ||
| }, | ||
| }, | ||
| sources: { | ||
| "source.proj.raw.customers": { | ||
| name: "customers", | ||
| source_name: "raw", | ||
| schema: "raw_data", | ||
| database: "analytics", | ||
| columns: { | ||
| id: { name: "id", data_type: "INTEGER" }, | ||
| name: { name: "name", data_type: "VARCHAR" }, | ||
| email: { name: "email", data_type: "VARCHAR" }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // 1. Model lookup (findModel) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| describe("dbtLineage: model lookup", () => { | ||
| test("finds model by unique_id", () => { | ||
| const dir = makeTmpDir() | ||
| const manifestPath = writeManifest(dir, BASE_MANIFEST) | ||
|
|
||
| const result = dbtLineage({ | ||
| manifest_path: manifestPath, | ||
| model: "model.proj.orders", | ||
| }) | ||
|
|
||
| expect(result.model_name).toBe("orders") | ||
| expect(result.model_unique_id).toBe("model.proj.orders") | ||
| expect(result.compiled_sql).toContain("SELECT") | ||
| }) | ||
|
|
||
| test("finds model by short name", () => { | ||
| const dir = makeTmpDir() | ||
| const manifestPath = writeManifest(dir, BASE_MANIFEST) | ||
|
|
||
| const result = dbtLineage({ | ||
| manifest_path: manifestPath, | ||
| model: "orders", | ||
| }) | ||
|
|
||
| expect(result.model_name).toBe("orders") | ||
| expect(result.model_unique_id).toBe("model.proj.orders") | ||
| }) | ||
|
|
||
| test("returns low confidence when model not found", () => { | ||
| const dir = makeTmpDir() | ||
| const manifestPath = writeManifest(dir, BASE_MANIFEST) | ||
|
|
||
| const result = dbtLineage({ | ||
| manifest_path: manifestPath, | ||
| model: "nonexistent_model", | ||
| }) | ||
|
|
||
| expect(result.confidence).toBe("low") | ||
| expect(result.confidence_factors).toContain("Model 'nonexistent_model' not found in manifest") | ||
| }) | ||
|
|
||
| test("does not match test or seed nodes by name", () => { | ||
| const dir = makeTmpDir() | ||
| const manifestPath = writeManifest(dir, BASE_MANIFEST) | ||
|
|
||
| const result = dbtLineage({ | ||
| manifest_path: manifestPath, | ||
| model: "not_null", | ||
| }) | ||
|
|
||
| // "not_null" is a test node, not a model — should not be found | ||
| expect(result.confidence).toBe("low") | ||
| expect(result.confidence_factors[0]).toContain("not found in manifest") | ||
| }) | ||
| }) | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // 2. Dialect detection (detectDialect) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| describe("dbtLineage: dialect detection", () => { | ||
| test("detects dialect from manifest metadata.adapter_type", () => { | ||
| const dir = makeTmpDir() | ||
| const manifest = { | ||
| ...BASE_MANIFEST, | ||
| metadata: { adapter_type: "bigquery" }, | ||
| } | ||
| const manifestPath = writeManifest(dir, manifest) | ||
|
|
||
| const result = dbtLineage({ | ||
| manifest_path: manifestPath, | ||
| model: "orders", | ||
| }) | ||
|
|
||
| // We can't directly check dialect, but the result shouldn't error | ||
| // due to dialect mismatch. The model has compiled_code, so confidence | ||
| // should be high if lineage succeeds or reflect the actual error. | ||
| expect(result.model_name).toBe("orders") | ||
| }) | ||
|
|
||
| test("explicit dialect param overrides auto-detection", () => { | ||
| const dir = makeTmpDir() | ||
| const manifestPath = writeManifest(dir, BASE_MANIFEST) | ||
|
|
||
| const result = dbtLineage({ | ||
| manifest_path: manifestPath, | ||
| model: "orders", | ||
| dialect: "postgres", | ||
| }) | ||
|
|
||
| // Should not throw regardless of dialect choice | ||
| expect(result.model_name).toBe("orders") | ||
| }) | ||
|
|
||
| test("defaults to snowflake when adapter_type is missing", () => { | ||
| const dir = makeTmpDir() | ||
| const manifest = { | ||
| ...BASE_MANIFEST, | ||
| metadata: {}, | ||
| } | ||
| const manifestPath = writeManifest(dir, manifest) | ||
|
|
||
| const result = dbtLineage({ | ||
| manifest_path: manifestPath, | ||
| model: "orders", | ||
| }) | ||
|
|
||
| // Should not throw — defaults to snowflake | ||
| expect(result.model_name).toBe("orders") | ||
| }) | ||
| }) | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // 3. Schema context building (buildSchemaContext) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| describe("dbtLineage: schema context from upstream deps", () => { | ||
| test("builds context from source with columns", () => { | ||
| const dir = makeTmpDir() | ||
| const manifestPath = writeManifest(dir, BASE_MANIFEST) | ||
|
|
||
| const result = dbtLineage({ | ||
| manifest_path: manifestPath, | ||
| model: "orders", | ||
| }) | ||
|
|
||
| // The orders model depends on source.proj.raw.customers which has columns. | ||
| // If schema context was built correctly, lineage should have non-empty output. | ||
| expect(result.model_name).toBe("orders") | ||
| // compiled_sql should be present | ||
| expect(result.compiled_sql).toBeDefined() | ||
| expect(result.compiled_sql).toContain("SELECT") | ||
| }) | ||
|
|
||
| test("handles model with no upstream columns gracefully", () => { | ||
| const dir = makeTmpDir() | ||
| // Revenue depends on orders, but orders has columns — so context should build. | ||
| // Create a model that depends on a node with no columns. | ||
| const manifest = { | ||
| ...BASE_MANIFEST, | ||
| nodes: { | ||
| ...BASE_MANIFEST.nodes, | ||
| "model.proj.bare": { | ||
| resource_type: "model", | ||
| name: "bare", | ||
| compiled_code: "SELECT 1 AS val", | ||
| depends_on: { nodes: ["model.proj.no_cols"] }, | ||
| columns: {}, | ||
| }, | ||
| "model.proj.no_cols": { | ||
| resource_type: "model", | ||
| name: "no_cols", | ||
| compiled_code: "SELECT 1", | ||
| depends_on: { nodes: [] }, | ||
| columns: {}, | ||
| }, | ||
| }, | ||
| } | ||
| const manifestPath = writeManifest(dir, manifest) | ||
|
|
||
| const result = dbtLineage({ | ||
| manifest_path: manifestPath, | ||
| model: "bare", | ||
| }) | ||
|
|
||
| // Should not crash — just returns with whatever lineage can determine | ||
| expect(result.model_name).toBe("bare") | ||
| expect(result.compiled_sql).toBe("SELECT 1 AS val") | ||
| }) | ||
| }) | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // 4. Error paths | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| describe("dbtLineage: error handling", () => { | ||
| test("returns low confidence for non-existent manifest", () => { | ||
| const result = dbtLineage({ | ||
| manifest_path: "/tmp/definitely-not-a-manifest.json", | ||
| model: "orders", | ||
| }) | ||
|
Comment on lines
+269
to
+273
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a guaranteed-missing path instead of a hardcoded A fixed path can collide on some hosts. Build a unique path in a test temp dir and intentionally do not create the file. 🤖 Prompt for AI Agents |
||
|
|
||
| expect(result.confidence).toBe("low") | ||
| expect(result.confidence_factors).toContain("Manifest file not found") | ||
| expect(result.raw_lineage).toEqual({}) | ||
| }) | ||
|
|
||
| test("returns low confidence for invalid JSON manifest", () => { | ||
| const dir = makeTmpDir() | ||
| const manifestPath = join(dir, "manifest.json") | ||
| writeFileSync(manifestPath, "not valid json {{{") | ||
|
|
||
| const result = dbtLineage({ | ||
| manifest_path: manifestPath, | ||
| model: "orders", | ||
| }) | ||
|
|
||
| expect(result.confidence).toBe("low") | ||
| expect(result.confidence_factors[0]).toContain("Failed to parse manifest") | ||
| }) | ||
|
|
||
| test("returns low confidence when model has no compiled SQL", () => { | ||
| const dir = makeTmpDir() | ||
| const manifest = { | ||
| nodes: { | ||
| "model.proj.uncompiled": { | ||
| resource_type: "model", | ||
| name: "uncompiled", | ||
| // No compiled_code or compiled_sql | ||
| depends_on: { nodes: [] }, | ||
| columns: {}, | ||
| }, | ||
| }, | ||
| sources: {}, | ||
| } | ||
| const manifestPath = writeManifest(dir, manifest) | ||
|
|
||
| const result = dbtLineage({ | ||
| manifest_path: manifestPath, | ||
| model: "uncompiled", | ||
| }) | ||
|
|
||
| expect(result.confidence).toBe("low") | ||
| expect(result.confidence_factors).toContain("No compiled SQL found — run `dbt compile` first") | ||
| }) | ||
|
|
||
| test("handles manifest with no nodes key at all", () => { | ||
| const dir = makeTmpDir() | ||
| const manifestPath = writeManifest(dir, { metadata: {} }) | ||
|
|
||
| const result = dbtLineage({ | ||
| manifest_path: manifestPath, | ||
| model: "orders", | ||
| }) | ||
|
|
||
| expect(result.confidence).toBe("low") | ||
| }) | ||
| }) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Use the shared
tmpdir()fixture withawait usinginstead of manual temp-dir lifecycle.This manual
mkdtempSync+ array tracking +afterEachcleanup pattern bypasses the required test fixture conventions and realpath guarantees.As per coding guidelines: "Use the
tmpdirfunction fromfixture/fixture.tsto create temporary directories for tests with automatic cleanup in test files" and "Always useawait usingsyntax withtmpdir()for automatic cleanup when the variable goes out of scope".🤖 Prompt for AI Agents