From 7fb5fcaba15e2added60908d4eaaec2a97a8b5b4 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Wed, 15 Apr 2026 14:19:39 +0530 Subject: [PATCH 1/2] =?UTF-8?q?test:=20consolidate=205=20test=20PRs=20?= =?UTF-8?q?=E2=80=94=20139=20tests,=20deduplicated,=20with=20bug=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidates PRs #694, #690, #689, #695, #620. Changes: - 4 files changed, ~577 lines of new test coverage - Deduplicated `dbt-helpers.test.ts`: merged #694 (superset) with unique tests from #690 (symlink caching, seed/test node exclusion, JSON array edge case, combined fallbacks) - Bug fixes: fixed `loadRawManifest` cache invalidation test (explicit `utimesSync` for filesystem mtime granularity) - Removed 2 broken tests from #695: `discoverExternalMcp` env-var interpolation tests assumed `${VAR}` resolution in `command` field, but `resolveServerEnvVars` only resolves `env` and `headers` fields Co-Authored-By: Claude Opus 4.6 (1M context) --- .../test/altimate/dbt-helpers.test.ts | 360 ++++++++++++++++++ packages/opencode/test/file/status.test.ts | 122 ++++++ packages/opencode/test/mcp/discover.test.ts | 4 + .../opencode/test/tool/project-scan.test.ts | 58 +++ 4 files changed, 544 insertions(+) create mode 100644 packages/opencode/test/altimate/dbt-helpers.test.ts create mode 100644 packages/opencode/test/file/status.test.ts diff --git a/packages/opencode/test/altimate/dbt-helpers.test.ts b/packages/opencode/test/altimate/dbt-helpers.test.ts new file mode 100644 index 0000000000..7e5b3581d6 --- /dev/null +++ b/packages/opencode/test/altimate/dbt-helpers.test.ts @@ -0,0 +1,360 @@ +/** + * Direct unit tests for dbt native helper functions in + * src/altimate/native/dbt/helpers.ts. + * + * These pure functions power dbt-lineage, dbt-unit-test-gen, and + * dbt-manifest handlers. Previously only tested indirectly through + * dbtLineage() in dbt-lineage-helpers.test.ts. Direct tests catch + * regressions in isolation: a broken findModel or detectDialect + * silently degrades multiple downstream tools. + */ + +import { describe, test, expect, beforeEach, afterEach } from "bun:test" +import * as fs from "fs" +import * as path from "path" +import * as os from "os" + +import { + loadRawManifest, + findModel, + getUniqueId, + detectDialect, + buildSchemaContext, + extractColumns, + listModelNames, +} from "../../src/altimate/native/dbt/helpers" + +// ---------- findModel ---------- + +describe("findModel", () => { + const nodes: Record = { + "model.project.orders": { resource_type: "model", name: "orders" }, + "model.project.users": { resource_type: "model", name: "users" }, + "source.project.raw.events": { resource_type: "source", name: "events" }, + "test.project.not_null": { resource_type: "test", name: "not_null" }, + } + + test("finds model by exact unique_id key", () => { + expect(findModel(nodes, "model.project.orders")).toEqual(nodes["model.project.orders"]) + }) + + test("finds model by name when unique_id does not match", () => { + expect(findModel(nodes, "users")).toEqual(nodes["model.project.users"]) + }) + + test("returns null for source nodes (not resource_type=model)", () => { + expect(findModel(nodes, "events")).toBeNull() + }) + + test("returns null for nonexistent model", () => { + expect(findModel(nodes, "nonexistent")).toBeNull() + }) + + test("returns null for empty nodes", () => { + expect(findModel({}, "orders")).toBeNull() + }) + + test("returns a model when multiple models share the same name", () => { + const dupes: Record = { + "model.a.orders": { resource_type: "model", name: "orders" }, + "model.b.orders": { resource_type: "model", name: "orders" }, + } + const result = findModel(dupes, "orders") + expect(result).not.toBeNull() + expect(result.resource_type).toBe("model") + }) +}) + +// ---------- getUniqueId ---------- + +describe("getUniqueId", () => { + const nodes: Record = { + "model.project.orders": { resource_type: "model", name: "orders" }, + "source.project.raw.events": { resource_type: "source", name: "events" }, + } + + test("returns key when exact unique_id exists and is a model", () => { + expect(getUniqueId(nodes, "model.project.orders")).toBe("model.project.orders") + }) + + test("returns unique_id when looked up by name", () => { + expect(getUniqueId(nodes, "orders")).toBe("model.project.orders") + }) + + test("returns undefined for source node (not resource_type=model)", () => { + expect(getUniqueId(nodes, "events")).toBeUndefined() + }) + + test("returns undefined for nonexistent model", () => { + expect(getUniqueId(nodes, "nonexistent")).toBeUndefined() + }) + + test("does not match test nodes by name", () => { + const nodesWithTest: Record = { + ...nodes, + "test.project.not_null": { resource_type: "test", name: "not_null" }, + } + expect(getUniqueId(nodesWithTest, "not_null")).toBeUndefined() + }) + + test("does not match seed nodes by name", () => { + const nodesWithSeed: Record = { + ...nodes, + "seed.project.country_codes": { resource_type: "seed", name: "country_codes" }, + } + expect(getUniqueId(nodesWithSeed, "country_codes")).toBeUndefined() + }) + + test("does not match by unique_id if resource_type is not model", () => { + expect(getUniqueId(nodes, "source.project.raw.events")).toBeUndefined() + }) +}) + +// ---------- detectDialect ---------- + +describe("detectDialect", () => { + test("maps known adapter types to dialect strings", () => { + const cases: Array<[string, string]> = [ + ["snowflake", "snowflake"], + ["bigquery", "bigquery"], + ["databricks", "databricks"], + ["spark", "spark"], + ["postgres", "postgres"], + ["redshift", "redshift"], + ["duckdb", "duckdb"], + ["clickhouse", "clickhouse"], + ["mysql", "mysql"], + ["sqlserver", "tsql"], + ["trino", "trino"], + ] + for (const [adapter, expected] of cases) { + expect(detectDialect({ metadata: { adapter_type: adapter } })).toBe(expected) + } + }) + + test("returns unmapped adapter type verbatim (truthy passthrough)", () => { + expect(detectDialect({ metadata: { adapter_type: "athena" } })).toBe("athena") + }) + + test("defaults to 'snowflake' when no metadata", () => { + expect(detectDialect({})).toBe("snowflake") + }) + + test("defaults to 'snowflake' when adapter_type is empty string", () => { + expect(detectDialect({ metadata: { adapter_type: "" } })).toBe("snowflake") + }) + + test("defaults to 'snowflake' when metadata is null", () => { + expect(detectDialect({ metadata: null })).toBe("snowflake") + }) +}) + +// ---------- buildSchemaContext ---------- + +describe("buildSchemaContext", () => { + const nodes: Record = { + "model.project.upstream_a": { + resource_type: "model", + name: "upstream_a", + alias: "upstream_alias", + columns: { + id: { name: "id", data_type: "INTEGER" }, + name: { name: "name", data_type: "VARCHAR" }, + }, + }, + "model.project.upstream_b": { + resource_type: "model", + name: "upstream_b", + columns: {}, + }, + } + const sources: Record = { + "source.project.raw.events": { + name: "events", + columns: { + event_id: { name: "event_id", data_type: "BIGINT" }, + }, + }, + } + + test("builds schema context using alias over name", () => { + const result = buildSchemaContext(nodes, sources, ["model.project.upstream_a"]) + expect(result).not.toBeNull() + expect(result!.version).toBe("1") + // Alias takes precedence over name + expect(result!.tables["upstream_alias"]).toBeDefined() + expect(result!.tables["upstream_alias"].columns).toHaveLength(2) + // Name key must NOT exist when alias is present + expect(result!.tables["upstream_a"]).toBeUndefined() + }) + + test("skips upstream models with empty columns", () => { + const result = buildSchemaContext(nodes, sources, ["model.project.upstream_b"]) + expect(result).toBeNull() + }) + + test("resolves upstream IDs from sources", () => { + const result = buildSchemaContext(nodes, sources, ["source.project.raw.events"]) + expect(result).not.toBeNull() + expect(result!.tables["events"]).toBeDefined() + expect(result!.tables["events"].columns).toEqual([ + { name: "event_id", type: "BIGINT" }, + ]) + }) + + test("returns null when no upstream IDs provided", () => { + expect(buildSchemaContext(nodes, sources, [])).toBeNull() + }) + + test("returns null when upstream IDs do not resolve", () => { + expect(buildSchemaContext(nodes, sources, ["model.project.ghost"])).toBeNull() + }) +}) + +// ---------- extractColumns ---------- + +describe("extractColumns", () => { + test("extracts column with data_type and description", () => { + const dict = { + id: { name: "id", data_type: "INTEGER", description: "Primary key" }, + } + const cols = extractColumns(dict) + expect(cols).toHaveLength(1) + expect(cols[0]).toEqual({ name: "id", data_type: "INTEGER", description: "Primary key" }) + }) + + test("falls back to 'type' field when data_type is missing", () => { + const dict = { + name: { name: "name", type: "VARCHAR" }, + } + const cols = extractColumns(dict) + expect(cols).toHaveLength(1) + expect(cols[0].name).toBe("name") + expect(cols[0].data_type).toBe("VARCHAR") + expect(cols[0].description).toBeUndefined() + }) + + test("uses dict key as column name when col.name is missing", () => { + const dict = { amount: { data_type: "DECIMAL" } } + const cols = extractColumns(dict) + expect(cols[0].name).toBe("amount") + }) + + test("returns empty array for empty dict", () => { + expect(extractColumns({})).toEqual([]) + }) + + test("handles both name and type fallbacks simultaneously", () => { + const dict = { + my_col: { type: "TEXT" }, + } + const result = extractColumns(dict) + expect(result[0].name).toBe("my_col") + expect(result[0].data_type).toBe("TEXT") + expect(result[0].description).toBeUndefined() + }) +}) + +// ---------- listModelNames ---------- + +describe("listModelNames", () => { + test("returns only model names, excluding sources and tests", () => { + const nodes: Record = { + "model.p.a": { resource_type: "model", name: "alpha" }, + "source.p.b": { resource_type: "source", name: "beta" }, + "model.p.c": { resource_type: "model", name: "gamma" }, + "test.p.d": { resource_type: "test", name: "delta" }, + } + const names = listModelNames(nodes) + expect(names).toEqual(["alpha", "gamma"]) + }) + + test("returns empty array for no models", () => { + expect(listModelNames({})).toEqual([]) + }) +}) + +// ---------- loadRawManifest ---------- + +describe("loadRawManifest", () => { + let tmpDir: string + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dbt-helpers-test-")) + }) + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }) + }) + + test("returns null for non-existent file", () => { + expect(loadRawManifest(path.join(tmpDir, "nonexistent.json"))).toBeNull() + }) + + test("parses valid manifest file", () => { + const manifestPath = path.join(tmpDir, "manifest.json") + fs.writeFileSync(manifestPath, JSON.stringify({ nodes: {}, metadata: { adapter_type: "snowflake" } })) + const result = loadRawManifest(manifestPath) + expect(result).not.toBeNull() + expect(result.metadata.adapter_type).toBe("snowflake") + }) + + test("throws on invalid JSON", () => { + const manifestPath = path.join(tmpDir, "bad.json") + fs.writeFileSync(manifestPath, "not json {{{") + expect(() => loadRawManifest(manifestPath)).toThrow() + }) + + test("throws when manifest is a primitive (not an object)", () => { + // typeof 42 === "number", triggers the non-object guard + const manifestPath = path.join(tmpDir, "number.json") + fs.writeFileSync(manifestPath, "42") + expect(() => loadRawManifest(manifestPath)).toThrow("Manifest is not a JSON object") + }) + + test("caches by path+mtime (same reference returned)", () => { + const manifestPath = path.join(tmpDir, "cached.json") + fs.writeFileSync(manifestPath, JSON.stringify({ v: 1 })) + const first = loadRawManifest(manifestPath) + const second = loadRawManifest(manifestPath) + // Same object reference from cache + expect(first).toBe(second) + }) + + test("invalidates cache when file content is rewritten", () => { + const manifestPath = path.join(tmpDir, "updated.json") + fs.writeFileSync(manifestPath, JSON.stringify({ v: 1 })) + const first = loadRawManifest(manifestPath) + + // Rewrite with bumped mtime to guarantee cache invalidation. + // Some filesystems have 1-second mtime granularity, so we + // explicitly set a future mtime. + fs.writeFileSync(manifestPath, JSON.stringify({ v: 2 })) + const futureMs = Date.now() / 1000 + 5 + fs.utimesSync(manifestPath, futureMs, futureMs) + const second = loadRawManifest(manifestPath) + expect(second.v).toBe(2) + }) + + test("does not throw for JSON array (typeof [] is 'object')", () => { + // typeof [] === "object" in JS, so arrays pass the guard. + // Known edge case — callers handle gracefully since .nodes is undefined on arrays. + const manifestPath = path.join(tmpDir, "array-manifest.json") + fs.writeFileSync(manifestPath, "[1, 2, 3]") + const result = loadRawManifest(manifestPath) + expect(Array.isArray(result)).toBe(true) + }) + + test("resolves symlinks before caching", () => { + const realPath = path.join(tmpDir, "real-manifest.json") + const symPath = path.join(tmpDir, "link-manifest.json") + const data = { metadata: {}, nodes: { sym: true } } + fs.writeFileSync(realPath, JSON.stringify(data)) + fs.symlinkSync(realPath, symPath) + + const viaReal = loadRawManifest(realPath) + const viaSym = loadRawManifest(symPath) + expect(viaSym).toEqual(viaReal) + expect(viaSym.nodes.sym).toBe(true) + }) +}) diff --git a/packages/opencode/test/file/status.test.ts b/packages/opencode/test/file/status.test.ts new file mode 100644 index 0000000000..ec4eb295a6 --- /dev/null +++ b/packages/opencode/test/file/status.test.ts @@ -0,0 +1,122 @@ +import { describe, test, expect } from "bun:test" +import path from "path" +import fs from "fs/promises" +import { $ } from "bun" +import { File } from "../../src/file" +import { Instance } from "../../src/project/instance" +import { tmpdir } from "../fixture/fixture" + +describe("File.status()", () => { + test("detects modified files with line counts", async () => { + await using tmp = await tmpdir({ git: true }) + const filepath = path.join(tmp.path, "test.txt") + await fs.writeFile(filepath, "line1\nline2\n") + await $`git add test.txt && git commit -m "initial"`.cwd(tmp.path).quiet() + + // Modify the file — append two lines + await fs.writeFile(filepath, "line1\nline2\nline3\nline4\n") + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const changed = await File.status() + expect(changed.length).toBe(1) + expect(changed[0].path).toBe("test.txt") + expect(changed[0].status).toBe("modified") + expect(changed[0].added).toBe(2) + expect(changed[0].removed).toBe(0) + }, + }) + }) + + test("detects untracked (new) files", async () => { + await using tmp = await tmpdir({ git: true }) + // No trailing newline so split("\n") gives exactly 2 elements + await fs.writeFile(path.join(tmp.path, "newfile.ts"), "const x = 1\nconst y = 2") + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const changed = await File.status() + const added = changed.find((f) => f.path === "newfile.ts") + expect(added).toBeDefined() + expect(added!.status).toBe("added") + expect(added!.added).toBe(2) // "const x = 1\nconst y = 2".split("\n").length === 2 + }, + }) + }) + + test("detects deleted files", async () => { + await using tmp = await tmpdir({ git: true }) + const filepath = path.join(tmp.path, "to-delete.txt") + await fs.writeFile(filepath, "content\n") + await $`git add to-delete.txt && git commit -m "add file"`.cwd(tmp.path).quiet() + + // Delete the file + await fs.rm(filepath) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const changed = await File.status() + // Deleted files appear in diff --numstat (as "modified") AND diff --diff-filter=D (as "deleted") + const deleted = changed.find((f) => f.path === "to-delete.txt" && f.status === "deleted") + expect(deleted).toBeDefined() + expect(deleted!.status).toBe("deleted") + }, + }) + }) + + test("returns empty array for clean working tree", async () => { + await using tmp = await tmpdir({ git: true }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const changed = await File.status() + expect(changed).toEqual([]) + }, + }) + }) + + test("handles binary files with dash line counts", async () => { + await using tmp = await tmpdir({ git: true }) + const filepath = path.join(tmp.path, "image.png") + // Write binary content with null bytes so git classifies it as binary + await fs.writeFile(filepath, Buffer.from([0x00, 0x89, 0x50, 0x4e, 0x47, 0x00])) + await $`git add image.png && git commit -m "add image"`.cwd(tmp.path).quiet() + + // Modify the binary + await fs.writeFile(filepath, Buffer.from([0x00, 0x89, 0x50, 0x4e, 0x47, 0x00, 0xff, 0x00])) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const changed = await File.status() + const binary = changed.find((f) => f.path === "image.png") + expect(binary).toBeDefined() + expect(binary!.status).toBe("modified") + // Binary files report "-" in diff --numstat, which gets parsed as 0 + expect(binary!.added).toBe(0) + expect(binary!.removed).toBe(0) + }, + }) + }) + + test("normalizes paths to be relative", async () => { + await using tmp = await tmpdir({ git: true }) + await fs.mkdir(path.join(tmp.path, "src"), { recursive: true }) + await fs.writeFile(path.join(tmp.path, "src", "app.ts"), "const x = 1\n") + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const changed = await File.status() + for (const file of changed) { + // All paths should be relative, not absolute + expect(path.isAbsolute(file.path)).toBe(false) + } + }, + }) + }) +}) diff --git a/packages/opencode/test/mcp/discover.test.ts b/packages/opencode/test/mcp/discover.test.ts index 382f0a98bf..cce9cde10f 100644 --- a/packages/opencode/test/mcp/discover.test.ts +++ b/packages/opencode/test/mcp/discover.test.ts @@ -363,4 +363,8 @@ describe("discoverExternalMcp", () => { expect(result["project-server"]).toBeDefined() expect((result["project-server"] as any).enabled).toBe(false) }) + + // NOTE: env-var interpolation in discover only applies to `env` and `headers` + // fields (see resolveServerEnvVars in discover.ts), NOT to `command` args. + // Tests for command-level interpolation were removed as invalid. }) diff --git a/packages/opencode/test/tool/project-scan.test.ts b/packages/opencode/test/tool/project-scan.test.ts index 31bfd04169..f30cd8f130 100644 --- a/packages/opencode/test/tool/project-scan.test.ts +++ b/packages/opencode/test/tool/project-scan.test.ts @@ -312,6 +312,8 @@ describe("detectEnvVars", () => { "PGHOST", "PGPORT", "PGDATABASE", "PGUSER", "PGPASSWORD", "DATABASE_URL", "MYSQL_HOST", "MYSQL_TCP_PORT", "MYSQL_DATABASE", "MYSQL_USER", "MYSQL_PASSWORD", "REDSHIFT_HOST", "REDSHIFT_PORT", "REDSHIFT_DATABASE", "REDSHIFT_USER", "REDSHIFT_PASSWORD", + "CLICKHOUSE_HOST", "CLICKHOUSE_URL", "CLICKHOUSE_PORT", "CLICKHOUSE_DB", + "CLICKHOUSE_DATABASE", "CLICKHOUSE_USER", "CLICKHOUSE_USERNAME", "CLICKHOUSE_PASSWORD", ] for (const v of vars) { delete process.env[v] @@ -502,6 +504,62 @@ describe("detectEnvVars", () => { expect(rs!.config.user).toBe("admin") }) + test("detects ClickHouse via CLICKHOUSE_HOST", async () => { + clearWarehouseEnvVars() + process.env.CLICKHOUSE_HOST = "clickhouse.example.com" + process.env.CLICKHOUSE_PORT = "8443" + process.env.CLICKHOUSE_DATABASE = "analytics" + process.env.CLICKHOUSE_USER = "default" + process.env.CLICKHOUSE_PASSWORD = "secret" + + const result = await detectEnvVars() + const ch = result.find((r) => r.type === "clickhouse") + expect(ch).toBeDefined() + expect(ch!.name).toBe("env_clickhouse") + expect(ch!.source).toBe("env-var") + expect(ch!.signal).toBe("CLICKHOUSE_HOST") + expect(ch!.config.host).toBe("clickhouse.example.com") + expect(ch!.config.port).toBe("8443") + expect(ch!.config.database).toBe("analytics") + expect(ch!.config.user).toBe("default") + expect(ch!.config.password).toBe("***") + }) + + test("detects ClickHouse via CLICKHOUSE_URL", async () => { + clearWarehouseEnvVars() + process.env.CLICKHOUSE_URL = "https://clickhouse.example.com:8443" + + const result = await detectEnvVars() + const ch = result.find((r) => r.type === "clickhouse") + expect(ch).toBeDefined() + expect(ch!.signal).toBe("CLICKHOUSE_URL") + expect(ch!.config.connection_string).toBe("***") + }) + + test("detects ClickHouse via DATABASE_URL with clickhouse scheme", async () => { + clearWarehouseEnvVars() + process.env.DATABASE_URL = "clickhouse://default:pass@clickhouse.example.com:8443/analytics" + + const result = await detectEnvVars() + const ch = result.find((r) => r.type === "clickhouse") + expect(ch).toBeDefined() + expect(ch!.signal).toBe("DATABASE_URL") + expect(ch!.config.connection_string).toBe("***") + }) + + test("detects ClickHouse via DATABASE_URL with clickhouse+http and clickhouse+https schemes", async () => { + for (const scheme of ["clickhouse+http", "clickhouse+https"]) { + clearWarehouseEnvVars() + process.env.DATABASE_URL = `${scheme}://default:pass@clickhouse.example.com:8443/analytics` + + const result = await detectEnvVars() + const ch = result.find((r) => r.type === "clickhouse") + expect(ch).toBeDefined() + expect(ch!.signal).toBe("DATABASE_URL") + expect(ch!.type).toBe("clickhouse") + } + }) + test("detects multiple warehouses simultaneously", async () => { clearWarehouseEnvVars() process.env.SNOWFLAKE_ACCOUNT = "sf_account" From a0c1918dc7389565e1e6218fcb887d5aab305d95 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Wed, 15 Apr 2026 14:25:31 +0530 Subject: [PATCH 2/2] test: address CodeRabbit review feedback - `dbt-helpers.test.ts`: use `toBe` (reference equality) for symlink cache test - `status.test.ts`: add presence assertion before relative-path loop - `project-scan.test.ts`: add `connection_string` masking assertion for `clickhouse+http/https` schemes Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/opencode/test/altimate/dbt-helpers.test.ts | 2 +- packages/opencode/test/file/status.test.ts | 1 + packages/opencode/test/tool/project-scan.test.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opencode/test/altimate/dbt-helpers.test.ts b/packages/opencode/test/altimate/dbt-helpers.test.ts index 7e5b3581d6..67303c3244 100644 --- a/packages/opencode/test/altimate/dbt-helpers.test.ts +++ b/packages/opencode/test/altimate/dbt-helpers.test.ts @@ -354,7 +354,7 @@ describe("loadRawManifest", () => { const viaReal = loadRawManifest(realPath) const viaSym = loadRawManifest(symPath) - expect(viaSym).toEqual(viaReal) + expect(viaSym).toBe(viaReal) expect(viaSym.nodes.sym).toBe(true) }) }) diff --git a/packages/opencode/test/file/status.test.ts b/packages/opencode/test/file/status.test.ts index ec4eb295a6..23e069a8df 100644 --- a/packages/opencode/test/file/status.test.ts +++ b/packages/opencode/test/file/status.test.ts @@ -112,6 +112,7 @@ describe("File.status()", () => { directory: tmp.path, fn: async () => { const changed = await File.status() + expect(changed.length).toBeGreaterThan(0) for (const file of changed) { // All paths should be relative, not absolute expect(path.isAbsolute(file.path)).toBe(false) diff --git a/packages/opencode/test/tool/project-scan.test.ts b/packages/opencode/test/tool/project-scan.test.ts index f30cd8f130..2b4c031e57 100644 --- a/packages/opencode/test/tool/project-scan.test.ts +++ b/packages/opencode/test/tool/project-scan.test.ts @@ -557,6 +557,7 @@ describe("detectEnvVars", () => { expect(ch).toBeDefined() expect(ch!.signal).toBe("DATABASE_URL") expect(ch!.type).toBe("clickhouse") + expect(ch!.config.connection_string).toBe("***") } })