Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/docs/configure/keybinds.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Override it in your config:
| Leader + `k` | Keybind list |
| Leader + `e` | Open editor |
| Leader + `q` | Quit |
| `Ctrl+Y` | Toggle YOLO mode for this session (confirms when enabling; instant when disabling) |

### Input Editing

Expand Down Expand Up @@ -114,7 +115,7 @@ All configurable keybind identifiers:

### Session

`session_export`, `session_new`, `session_list`, `session_timeline`, `session_fork`, `session_rename`, `session_delete`, `session_child_cycle`, `session_parent`, `session_share`, `session_unshare`, `session_interrupt`, `session_compact`
`session_export`, `session_new`, `session_list`, `session_timeline`, `session_fork`, `session_rename`, `session_delete`, `session_child_cycle`, `session_parent`, `session_share`, `session_unshare`, `session_interrupt`, `session_compact`, `session_yolo_toggle`

### Messages

Expand Down
2 changes: 2 additions & 0 deletions docs/docs/configure/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ The fallback `OPENCODE_YOLO` env var is also supported. When both are set, `ALTI

When yolo mode is active in the TUI, a `△ YOLO` indicator appears in the footer status bar.

**Mid-session toggle (TUI):** Press `Ctrl+Y` inside the TUI to toggle yolo mode for the current session without restarting. Enabling requires a one-tap confirmation; disabling is instant. The toggle is **session and subagent scoped and lives in memory only** — restart the CLI and yolo defaults back to whatever `--yolo`, `ALTIMATE_CLI_YOLO`, or `OPENCODE_YOLO` was at launch. Deny rules stay enforced.

## Recommended Configurations

### Data Engineering (Default, Balanced)
Expand Down
7 changes: 5 additions & 2 deletions packages/opencode/src/altimate/plugin/altimate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as OnboardingTelemetry from "../telemetry/onboarding"
// altimate_change — shared machine-id helper (race-safe, UUID-validated, size-capped)
import { getOrCreateMachineId } from "../util/machine-id"
import { Config } from "@/config/config"
import { Flag } from "@/flag/flag"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { Log } from "@/altimate/util/log"

Expand Down Expand Up @@ -76,9 +77,11 @@ const log = Log.create({ service: "altimate-plugin" })
export async function buildCliContext(machineIdPath?: string): Promise<string> {
// altimate_change start — honour both telemetry opt-out gates, mirroring
// telemetry/index.ts::doInit:
// 1. ALTIMATE_TELEMETRY_DISABLED=true env var (always-works hard opt-out)
// 1. ALTIMATE_TELEMETRY_DISABLED / OPENCODE_DISABLE_TELEMETRY env vars
// ("true"/"TRUE"/"1", case-insensitive — see Flag.truthyEnv)
// 2. config.telemetry.disabled (resolved via the async Config.get())
let disabled = process.env.ALTIMATE_TELEMETRY_DISABLED === "true"
let disabled =
Flag.truthyEnv("ALTIMATE_TELEMETRY_DISABLED") || Flag.truthyEnv("OPENCODE_DISABLE_TELEMETRY")
if (!disabled) {
try {
const userConfig = (await Config.get()) as any
Expand Down
5 changes: 4 additions & 1 deletion packages/opencode/src/altimate/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1683,7 +1683,10 @@ export namespace Telemetry {

async function doInit() {
try {
if (process.env.ALTIMATE_TELEMETRY_DISABLED === "true") {
// altimate_change — accept "true"/"TRUE"/"1" (case-insensitive) via truthyEnv,
// and honor the OPENCODE_DISABLE_TELEMETRY fallback promised by v0.9.4's CHANGELOG
// (previously only wired in test fixtures, silent no-op in product).
if (Flag.truthyEnv("ALTIMATE_TELEMETRY_DISABLED") || Flag.truthyEnv("OPENCODE_DISABLE_TELEMETRY")) {
Comment on lines +1686 to +1689

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
fd -a 'index.ts|telemetry.ts|instance-state.ts|welcome.ts' packages/opencode/src 2>/dev/null | sed 's#^\./##' | head -100

echo
echo "Telemetry outline:"
ast-grep outline packages/opencode/src/altimate/telemetry/index.ts 2>/dev/null | sed -n '1,220p' || true

echo
echo "Relevant telemetry lines:"
nl -ba packages/opencode/src/altimate/telemetry/index.ts | sed -n '1640,1780p'

echo
echo "Welcome references:"
nl -ba packages/opencode/src/cli/welcome.ts | sed -n '1,180p' 2>/dev/null | rg -n "FIXME|telemetry|Instance|opencode" -C 3 || true

Repository: AltimateAI/altimate-code

Length of output: 3149


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Search telemetry.disabled and opt-out usages:"
rg -n "telemetry\.disabled|DISABLE_TELEMETRY|ALTIMATE_TELEMETRY_DISABLED|open(d|c)ode_disable_telemetry|Instance\.provide|doInit|ensureTelemetryEnabled" packages/opencode/src packages/opencode -g '*.ts' -g '*.tsx' | sed -n '1,240p'

Repository: AltimateAI/altimate-code

Length of output: 27109


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("packages/opencode/src/altimate/telemetry/index.ts")
text = p.read_text()
for needle in [
    "async function doInit()",
    "Flag.truthyEnv(\"ALTIMATE_TELEMETRY_DISABLED\")",
    "Flag.truthyEnv(\"OPENCODE_DISABLE_TELEMETRY\")",
    "Config.get()",
    "process.env.ALTIMATE_TELEMETRY_FORCE",
    "process.env.ALTIMATE_TELEMETRY_DISABLED",
    "ENABLED_TELEMETRY",
    "machineId",
    "readMachineId",
    "writeMachineId",
    "doInit()"
]:
    print(f"\n== {needle} ==")
    idx = text.find(needle)
    if idx == -1:
        print("NOT FOUND")
        continue
    line = text[:idx].count("\n") + 1
    print(f"first at line {line}")
    start = max(0, idx - 800)
    end = min(len(text), idx + 1000)
    print(text[start:end])
PY

echo
echo "Welcome lines:"
python3 - <<'PY'
from pathlib import Path
for path in ["packages/opencode/src/cli/welcome.ts", "packages/opencode/src/altimate/telemetry/index.ts", "packages/opencode/src/altimate/plugin/altimate.ts"]:
    p=Path(path)
    print(f"\n== {path} ==")
    txt=p.read_text()
    for i,line in enumerate(txt.splitlines(),1):
        if "FIXME" in line or "telemetry.disabled" in line or "doInit" in line or "Instance.provide" in line or "readMachineId" in line or "writeMachineId" in line or "ENABLED_TELEMETRY" in line:
            print(f"{i}: {line}")
PY

Repository: AltimateAI/altimate-code

Length of output: 14958


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for path in ["packages/opencode/src/altimate/telemetry/index.ts", "packages/opencode/src/cli/welcome.ts", "packages/opencode/src/altimate/plugin/altimate.ts", "packages/opencode/src/config/config.ts":
    p=Path(path)
    print(f"\n== {path} ==")
    for i,line in enumerate(p.read_text().splitlines(),1):
        if "doInit" in line or "enable" in line.lower() or "disabled" in line or "Config.get" in line or "writeMachine" in line or "readMachine" in line or "FIXME" in line or "telemetry.disabled" in line:
            print(f"{i}: {line}")
PY

echo
echo "Read relevant telemetry sections by computed ranges:"
python3-script<<'PY'
from pathlib import Path
p=Path("packages/opencode/src/altimate/telemetry/index.ts")
lines=p.read_text().splitlines()
for start,end in [(1700,1880),(1880,1990)]:
    print(f"\n-- {start}-{end} --")
    for i in range(start,end+1):
        if i<=len(lines):
            print(f"{i}: {lines[i-1]}")
PY

Repository: AltimateAI/altimate-code

Length of output: 625


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
paths = [
    "packages/opencode/src/altimate/telemetry/index.ts",
    "packages/opencode/src/cli/welcome.ts",
    "packages/opencode/src/altimate/plugin/altimate.ts",
]
for path in paths:
    p = Path(path)
    print(f"\n== {path} keyword lines ==")
    for i, line in enumerate(p.read_text().splitlines(), 1):
        if any(k in line for k in ["doInit", "ENABLE_TELEMETRY", "enabled", "disabled", "writeMachineId", "readMachineId", "FIXME", "telemetry.disabled", "Config.get"]):
            print(f"{i}: {line}")
PY

echo
python3 - <<'PY'
from pathlib import Path
p = Path("packages/opencode/src/altimate/telemetry/index.ts")
lines = p.read_text().splitlines()
for start,end in [(1700,1880),(1880,1990)]:
    print(f"\n-- telemetry {start}-{end} --")
    for i in range(start, min(end, len(lines)) + 1):
        print(f"{i}: {lines[i-1]}")
PY

echo
echo "Config key definitions/usages:"
rg -n "telemetry|TELEMETRY|disabled|writeMachineId|readMachineId|ENABLE_TELEMETRY" packages/opencode/src packages/opencode/test -g '*.ts' | sed -n '1,260p'

Repository: AltimateAI/altimate-code

Length of output: 48007


Protect config-based telemetry opt-outs during early initialization.

doInit() catches unavailable Config.get() and proceeds as enabled, so telemetry.disabled is ignored on startup paths before Instance.provide(). packages/opencode/src/cli/welcome.ts documents this, but it still mints machineId and sends events. Delay initialization until configuration is resolvable and fail closed, then retry after instance setup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/altimate/telemetry/index.ts` around lines 1686 - 1689,
Update doInit() to resolve configuration before initializing telemetry, honoring
telemetry.disabled even when Instance.provide() is not yet available. If
Config.get() cannot be resolved during early startup, fail closed by deferring
initialization without minting machineId or sending events, then retry telemetry
initialization after instance setup; preserve the existing environment-variable
opt-outs.

buffer = []
return
}
Expand Down
7 changes: 5 additions & 2 deletions packages/opencode/src/altimate/tools/sample-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,9 @@ export const SampleSetupTool = Tool.define("sample_setup", {
* apply a conservative pattern that stops at whitespace and quotes rather than trying to guess
* where a path ends. Full detail is kept in `metadata.error`, which the model never sees.
*/
function redactPaths(message: string, extra: (string | undefined)[] = []): string {
// altimate_change — exported for direct unit testing (v0.9.5 review, Tech Lead P1).
// Pure function; no additional risk from exposing it.
export function redactPaths(message: string, extra: (string | undefined)[] = []): string {
let out = message
for (const known of [os.homedir(), process.cwd(), os.tmpdir(), ...extra]) {
if (!known || known.length < 2) continue
Expand Down Expand Up @@ -306,8 +308,9 @@ function countFilesWithExtension(dir: string, extension: string): number {
return total
}

// altimate_change — exported for direct unit testing (v0.9.5 review, Tech Lead P1).
/** dbt models (`models/**\/*.sql`) and seed tables (`seeds/*.csv`) in the shipped sample. */
function countSampleContents(sampleSourcePath: string): { models: number; tables: number } {
export function countSampleContents(sampleSourcePath: string): { models: number; tables: number } {
return {
models: countFilesWithExtension(path.join(sampleSourcePath, "models"), ".sql"),
tables: countFilesWithExtension(path.join(sampleSourcePath, "seeds"), ".csv"),
Expand Down
14 changes: 10 additions & 4 deletions packages/opencode/src/cli/welcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,16 @@ export function showWelcomeBannerIfNeeded(): void {
// launch. Probe existence with existsSync only — do NOT mint here. Minting is left to
// Telemetry.doInit() (its job, not the welcome banner's); the first_launch machine_id is
// attached at flush time from telemetry module state, so it does not depend on minting here.
// NOTE: doInit's early call runs before an Instance is available, so its CONFIG opt-out gate
// fails open and it can still mint for a config-only opt-out user. That is a pre-existing
// telemetry-init gap (tracked separately), not something this banner can fix — this code just
// stops adding a SECOND minting site under an even weaker (env-only) gate.
//
// FIXME(telemetry-init-config-opt-out): doInit() may run before Instance.provide() has made
// Config.get() resolvable (see the try/catch around Config.get in telemetry/index.ts::doInit
// — the catch branch proceeds with telemetry enabled). A user who opted out via the
// `telemetry.disabled` config key — with no env var set — can therefore still get a
// machine-id minted on first launch. The env-var opt-out (ALTIMATE_TELEMETRY_DISABLED /
// OPENCODE_DISABLE_TELEMETRY) is unaffected — that check does not need Instance context.
// Pre-existing (not introduced by this release); calling it out explicitly here rather than
// leaving the earlier "(tracked separately)" wording, which claimed a tracking issue that
// does not currently exist.
const machineIdPath = path.join(os.homedir(), ".altimate", "machine-id")
const isUpgrade = fs.existsSync(machineIdPath)
// altimate_change end
Expand Down
9 changes: 9 additions & 0 deletions packages/opencode/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ function altEnv(altKey: string, openKey: string) {
// altimate_change end

export namespace Flag {
// altimate_change start — runtime-evaluated env-truthy helper for callers that need
// current process.env state. Module-level Flag.* constants freeze their value at
// import time; that's the wrong semantics for gates the caller re-reads on each
// invocation (e.g. telemetry.doInit()). Accepts "true" / "TRUE" / "1" — case-insensitive
// — so one convention covers every telemetry/onboarding opt-out env var.
export function truthyEnv(key: string): boolean {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new Flag.truthyEnv is presented as the shared env-truthy convention, but an identical private truthyEnv (same === "true" || === "1" lowercase logic) and the same dual-env-var fallback pattern already exist in src/cli/upgrade.ts (isAutoupdateDisabledByEnv). That's a second copy of the same convention that won't stay in sync with this one if the accepted values ever change. Consider having upgrade.ts route through Flag.truthyEnv (or the inverse) so the telemetry and autoupdate opt-outs share one implementation rather than two independent ones.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/flag/flag.ts, line 27:

<comment>The new `Flag.truthyEnv` is presented as the shared env-truthy convention, but an identical private `truthyEnv` (same `=== "true" || === "1"` lowercase logic) and the same dual-env-var fallback pattern already exist in `src/cli/upgrade.ts` (`isAutoupdateDisabledByEnv`). That's a second copy of the same convention that won't stay in sync with this one if the accepted values ever change. Consider having `upgrade.ts` route through `Flag.truthyEnv` (or the inverse) so the telemetry and autoupdate opt-outs share one implementation rather than two independent ones.</comment>

<file context>
@@ -19,6 +19,15 @@ function altEnv(altKey: string, openKey: string) {
+  // import time; that's the wrong semantics for gates the caller re-reads on each
+  // invocation (e.g. telemetry.doInit()). Accepts "true" / "TRUE" / "1" — case-insensitive
+  // — so one convention covers every telemetry/onboarding opt-out env var.
+  export function truthyEnv(key: string): boolean {
+    return truthy(key)
+  }
</file context>

return truthy(key)
}
// altimate_change end
// altimate_change start - ALTIMATE_CLI_CLIENT flag with OPENCODE_CLIENT fallback
export declare const ALTIMATE_CLI_CLIENT: string
// altimate_change end
Expand Down
195 changes: 195 additions & 0 deletions packages/opencode/test/altimate/sample-setup-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// v0.9.5 review — Tech Lead P1 (initial pass) + coderabbit / cubic follow-ups.
//
// sample-setup.ts::redactPaths has a docstring listing three specific bugs the
// implementation was written to fix (regex swallowing surrounding sentence,
// `/root/…` prefix leaking, José/O'Connor surnames leaking on the first pass).
// None of them had test coverage this release. This file asserts the fixes so a
// future edit to the regex or the known-value substitution loop can't silently
// re-introduce them.
//
// countSampleContents is a tiny counting helper — but it's called on every
// sample_setup invocation and its output feeds a telemetry event, so a
// silent-zero bug (typo in the extension filter, wrong subdirectory name)
// would misreport onboarding activity. The fixture below builds a real dir tree
// per test and asserts the count.
//
// Fixture ownership note (bot-review follow-up, coderabbit + cubic):
// countSampleContents originally shared one `mkdtempSync` at module scope with
// afterAll cleanup. That leaks if the suite is filtered (only redactPaths tests
// selected) or if a `beforeAll` throws before `afterAll` registers. Switched to
// the repo's `await using tmp = await tmpdir()` pattern so each test owns its
// fixture and cleanup is bound to the test scope.

import { describe, expect, test } from "bun:test"
import fs from "fs"
import path from "path"

import { tmpdir } from "../fixture/fixture"
import { redactPaths, countSampleContents } from "../../src/altimate/tools/sample-setup"

describe("redactPaths", () => {
test("redacts an absolute POSIX path", () => {
const out = redactPaths("failed at /usr/local/bin/dbt")
expect(out).toBe("failed at <path>")
})

test("redacts a Windows drive path", () => {
const out = redactPaths("failed at C:\\Users\\alice\\dbt.exe")
// Windows paths pattern matches `C:\` opener and consumes until whitespace/quote.
expect(out).toBe("failed at <path>")
expect(out).not.toContain("Users")
expect(out).not.toContain("alice")
})

test("redacts a home-relative path (~/)", () => {
// Note: `~/.altimate/…` is redacted by the POSIX-`/` pattern first, which
// starts at the leading slash and leaves the `~` as a harmless prefix.
// What the assertion cares about is that no path segment leaks — the exact
// shape of the redaction marker is secondary.
const out = redactPaths("cannot read ~/.altimate/machine-id")
expect(out).not.toContain(".altimate")
expect(out).not.toContain("machine-id")
expect(out).toContain("<path>")
})

test("redacts a bare tilde-only path (~/x with no preceding slash)", () => {
// The dedicated `~\/…` pattern is what catches this shape — the POSIX-`/`
// one starts inside the path and can leave a `~` behind.
const input = "opening ~/opt/dbt for read"
const out = redactPaths(input)
expect(out).not.toContain("opt/dbt")
expect(out).toContain("<path>")
})

test("terminates at whitespace, does NOT swallow the surrounding sentence", () => {
// The docstring calls out this exact class of bug: an early implementation
// used a character class that included `.`, so the regex would consume the
// rest of the sentence past the path. Reader ends up with just "<path>".
const input = "Underlying error: /Users/alice/projects/dbt-demo failed to compile"
const out = redactPaths(input)
expect(out).toContain("failed to compile")
expect(out).toContain("Underlying error:")
expect(out).not.toContain("/Users")
})

test("terminates at a double-quote", () => {
const out = redactPaths('opening "/Users/alice/dbt_project.yml" for read')
expect(out).toContain("for read")
expect(out).not.toContain("alice")
})

test("handles a path containing an apostrophe (O'Connor)", () => {
// Docstring bug: an early character class excluded `'`, so the regex would
// stop at the apostrophe and leak the substring after it. Now apostrophes
// are permitted inside the redacted run.
const out = redactPaths("failed at /Users/O'Connor/projects/x")
expect(out).not.toContain("O'Connor")
expect(out).not.toContain("Connor")
expect(out).toBe("failed at <path>")
})

test("handles a path containing an accented character (José)", () => {
const out = redactPaths("failed at /Users/José/dbt")
expect(out).not.toContain("José")
expect(out).toBe("failed at <path>")
})

test("path segment terminates at the first whitespace, so a path containing a space leaks the tail", () => {
// codex-review gap: the greedy pattern stops at the first `\s`, so a real
// CWD like `/Users/alice/My Documents/dbt` gets split — only `/Users/alice/My`
// is redacted; `Documents/dbt` is left in the output.
//
// The `extra` list is what production callers use to close this gap (they
// pass the exact CWD to `redactPaths(msg, [cwd])`), so this test also
// asserts the compensating behavior — with the CWD known-value, the whole
// path collapses cleanly.
const cwd = "/Users/alice/My Documents/dbt"
const raw = redactPaths(`failed at ${cwd}/models/foo.sql`)
// Documented limitation of the pattern-only pass: the greedy path pattern
// terminates at the first whitespace, so `/Users/alice/My` and
// `/dbt/models/foo.sql` each redact cleanly but the middle segment
// `Documents` sits between two `<path>` markers.
expect(raw).toContain("Documents")
expect(raw).not.toBe("failed at <path>")
// With the CWD passed as a known value the whole path collapses cleanly:
const guarded = redactPaths(`failed at ${cwd}/models/foo.sql`, [cwd])
expect(guarded).toBe("failed at <path>")
expect(guarded).not.toContain("Documents")
expect(guarded).not.toContain("alice")
})
Comment on lines +97 to +119

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not accept partial path redaction.

redactPaths leaves the segment after whitespace visible. A directory name can contain project or user data. Passing [cwd] only protects callers that provide the exact path.

Update redactPaths to redact the complete path, then expect raw to equal "failed at &lt;path&gt;".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/test/altimate/sample-setup-helpers.test.ts` around lines 97
- 119, Update redactPaths so filesystem paths containing whitespace are redacted
as a complete path during the pattern-only pass, without requiring the path in
the extra list. Then change the raw assertion in the test to expect exactly
"failed at <path>" and remove expectations documenting the partial-redaction
limitation, while preserving the guarded known-path coverage.


test("collapses adjacent <path> segments so double-redaction reads clean", () => {
// The known-value pass replaces os.homedir() etc first; the greedy pattern
// then may match the "<path>" tail and re-redact. The collapse rule keeps
// the output from becoming "<path><path><path>".
const home = require("os").homedir()
const out = redactPaths(`failed at ${home}/dbt/models/foo.sql`)
expect(out).toBe("failed at <path>")
expect(out).not.toMatch(/<path><path>/)
})

test("passes short/empty known values without exploding", () => {
// Guard for `known.length < 2` — empty string or single-char known values
// used to `split("")` and shatter every character. The guard keeps them out
// of the substitution loop.
const out = redactPaths("hello world", ["", "a", undefined])
expect(out).toBe("hello world")
})

test("substitutes user-supplied extras", () => {
const out = redactPaths("clone failed at /tmp/checkout-xyz", ["/tmp/checkout-xyz"])
expect(out).toBe("clone failed at <path>")
})

test("returns the message unchanged when nothing path-shaped is present", () => {
expect(redactPaths("dbt run completed in 3s")).toBe("dbt run completed in 3s")
})
})

// Shared helper — each test uses its own tmp dir via `await using`, so cleanup
// is scoped to the test itself (bot-review follow-up).
async function seedSampleTree(dir: string) {
fs.mkdirSync(path.join(dir, "models", "staging"), { recursive: true })
fs.mkdirSync(path.join(dir, "models", "marts", "core"), { recursive: true })
fs.mkdirSync(path.join(dir, "seeds"), { recursive: true })

fs.writeFileSync(path.join(dir, "models", "top.sql"), "select 1")
fs.writeFileSync(path.join(dir, "models", "staging", "stg_orders.sql"), "select 1")
fs.writeFileSync(path.join(dir, "models", "staging", "stg_users.sql"), "select 1")
fs.writeFileSync(path.join(dir, "models", "marts", "core", "dim_customers.sql"), "select 1")

// Non-.sql alongside .sql, must NOT be counted.
fs.writeFileSync(path.join(dir, "models", "readme.md"), "hi")
fs.writeFileSync(path.join(dir, "models", "schema.yml"), "version: 2")

fs.writeFileSync(path.join(dir, "seeds", "country_codes.csv"), "code,name\n")
fs.writeFileSync(path.join(dir, "seeds", "regions.csv"), "id,name\n")
// Non-.csv seed — not counted.
fs.writeFileSync(path.join(dir, "seeds", "notes.md"), "hi")
}

describe("countSampleContents", () => {
test("counts .sql files recursively under models/", async () => {
await using tmp = await tmpdir()
await seedSampleTree(tmp.path)
expect(countSampleContents(tmp.path).models).toBe(4)
})

test("counts .csv files under seeds/ (top level; matches production sample layout)", async () => {
await using tmp = await tmpdir()
await seedSampleTree(tmp.path)
expect(countSampleContents(tmp.path).tables).toBe(2)
})

test("returns zeros when the sample dir is missing the expected subdirs", async () => {
await using tmp = await tmpdir()
expect(countSampleContents(tmp.path)).toEqual({ models: 0, tables: 0 })
})

test("returns zeros when the sample dir does not exist at all", async () => {
// countFilesWithExtension swallows the readdirSync error and returns 0 —
// this is the graceful-degradation shape the telemetry event depends on.
await using tmp = await tmpdir()
expect(countSampleContents(path.join(tmp.path, "does-not-exist"))).toEqual({ models: 0, tables: 0 })
})
})
Loading
Loading