Skip to content

Commit 5f51d65

Browse files
authored
chore(release): v0.9.5 pre-release review fixes (#1086)
* fix(telemetry): accept `1`/case-insensitive `true` on `ALTIMATE_TELEMETRY_DISABLED` + honor `OPENCODE_DISABLE_TELEMETRY` fallback Two of the v0.9.5 review reviewers flagged the same shape: - The two telemetry-disable gates (`altimate/plugin/altimate.ts::buildCliContext` and `altimate/telemetry/index.ts::doInit`) each did `=== "true"`. Users setting `ALTIMATE_TELEMETRY_DISABLED=1` (or `=TRUE`) were silently ignored — telemetry stayed on. - The v0.9.4 CHANGELOG advertised `OPENCODE_DISABLE_TELEMETRY=1` as an opt-out env var, but that name was wired into test fixtures only. Users who set it based on the CHANGELOG were not opted out. Both call sites now route through a shared `Flag.truthyEnv` helper that accepts `"true"` / `"TRUE"` / `"1"` and checks both env var names. Runtime evaluation is preserved (Flag.* constants freeze at import — wrong shape for gates the caller re-reads). Test coverage: `telemetry-opt-out-flag.test.ts` (14 tests) locks the accepted-value matrix down so a future edit to `truthy()` can't silently narrow it again. * docs(configure): document `ctrl+y` YOLO mid-session toggle in permissions + keybinds v0.9.5 review — PM P1. The mid-session YOLO toggle (introduced in #1078, `bfb5a7cebe`) is a security-relevant control — it auto-approves prompts for `rm -rf`, `git push --force`, `.env` reads, etc. — but zero external docs mentioned it existed. Discoverability was in-app only (a persistent hint next to the prompt + entry in the command palette), so a user who never noticed the hint could not find it via docs/search. - `docs/docs/configure/permissions.md`: added a "Mid-session toggle (TUI)" paragraph under the existing "Yolo Mode" section, documenting `Ctrl+Y`, confirmation-on-enable / instant-off, session/subagent scope + in-memory lifetime, and the deny-rules-still-apply guarantee. - `docs/docs/configure/keybinds.md`: added a `Ctrl+Y` row to the "UI Toggles" table and appended `session_yolo_toggle` to the Session identifier reference list. * test(telemetry): cover `classifyProvider` allowlist + prototype-pollution defense v0.9.5 review — Tech Lead P1. `Telemetry.classifyProvider` (`telemetry/index.ts:1028`) shipped this release with zero test coverage despite sitting on the `provider_selected` privacy path. Its guarantees are: - `CURATED_PROVIDER_ENUM` uses `Object.create(null)` — a plain literal would resolve `["constructor"]`, `["toString"]`, `["valueOf"]` to inherited functions (all truthy), and the branch `if (curated) return { provider: curated, ... }` would ship a JS built-in as a curated provider slug. This test asserts the null-prototype defense holds. - Only `KNOWN_PROVIDER_IDS` carry a raw `provider_id` on the wire; customer-named custom providers fall through to `{ provider: "other" }` with no id (no PII leak). - The `opencode` + `big-pickle` pair returns `"big_pickle"` only when BOTH args match — proves a regression that ignored `modelID` would be caught. 29 tests, all pure — no fixtures, no side effects. * test(sample-setup): cover `redactPaths` + `countSampleContents` docstring claims v0.9.5 review — Tech Lead P1. ~90 new lines in `sample-setup.ts` shipped this release without direct unit coverage. `redactPaths` in particular lists three specific bugs in its docstring that the implementation was written to fix — sentence-swallowing regex, `/root/…` prefix leak, and partial redaction of surnames containing an apostrophe or accented character (José, O'Connor). None were asserted. - `redactPaths` + `countSampleContents` promoted from module-local to exports for direct testing. Pure functions, no additional risk. - New `sample-setup-helpers.test.ts` (16 tests) asserts each docstring claim: sentence terminates on whitespace/quote; O'Connor and José redact cleanly; adjacent `<path>` markers collapse; short/empty known values don't shatter the input; user-supplied `extra` list works. - `countSampleContents` gets a real dir-tree fixture (models/staging, models/marts/core, seeds/) and asserts the recursive `.sql` count + top-level `.csv` count that feeds the `sample_setup_completed` telemetry event. * test(onboarding): assert `claimEnvironmentScan` idempotency + session isolation v0.9.5 review — Tech Lead P1. The `environment_scan_completed` emission at `project-scan.ts:952-964` fires only when `isOnboardingSession(sessionID) && claimEnvironmentScan(sessionID)`. The claim call is the one thing preventing a second `project_scan` run inside the same onboarding session from pushing `scan_gate_shown → environment_scan_completed` above 100% in the funnel dashboard. That claim wasn't directly tested. Full end-to-end coverage through `project-scan.ts` needs git/dbt/docker detection stubs and is disproportionately expensive for the guarantee at stake, so this file tests the load-bearing behavior at its actual home (`onboarding.ts`): 1. First claim → true, subsequent claims for same session → false 2. Claims are session-scoped — new sessionID claims independently 3. `isOnboardingSession` is false for untracked sessions 4. The composed guard `isOnboardingSession && claim` fires exactly once across N `project_scan` runs in an onboarding session 5. A non-onboarding session running the same guard chain never fires, and the claim stays unspent (proving the short-circuit works) 6 tests, `resetForTest()` between each. * chore(welcome): replace vague "tracked separately" claim with explicit `FIXME` anchor v0.9.5 review — PM P2. The comment at `welcome.ts:46` referenced a "pre-existing telemetry-init gap (tracked separately)" — a claim that implied a tracking issue existed. It didn't (verified by searching open issues on `AltimateAI/altimate-code`). Rewritten to: - Anchor the gap with `FIXME(telemetry-init-config-opt-out)` so it shows up in code searches. - Describe the exact failure mode: `doInit()` may run before `Instance.provide()` has made `Config.get()` resolvable — the catch branch in `telemetry/index.ts::doInit` proceeds with telemetry enabled, so a user who opted out via `telemetry.disabled` config (env var not set) can still have a machine-id minted on cold-start. - Explicitly note the env-var opt-out (`ALTIMATE_TELEMETRY_DISABLED` / `OPENCODE_DISABLE_TELEMETRY`) is unaffected — that check doesn't need Instance context. - Honestly state no tracking issue currently exists rather than repeat the earlier false claim. Pre-existing (not introduced by this release). Filed as part of the release/v0.9.5 deferred-items tracker. * docs(permissions): include `OPENCODE_YOLO` in YOLO mid-session toggle restart-state sentence Coderabbit review on PR #1086: the "Mid-session toggle (TUI)" paragraph added in commit `078876be89` listed only `--yolo` and `ALTIMATE_CLI_YOLO` as the launch-time sources whose value the toggle reverts to. Line 129 immediately above already documents `OPENCODE_YOLO` as a supported fallback, so the omission was inconsistent — a user who set only `OPENCODE_YOLO=true` had no docs describing what happens after a `Ctrl+Y` toggle + restart. * test(telemetry): drop redundant `not.toContain` before `toBe("other")` in prototype-defense test Cubic P3 on PR #1086: `expect([...curated...]).not.toContain(result.provider)` is a strict weakening of the very next line's `expect(result.provider).toBe("other")`. `toBe("other")` already excludes every curated enum value; the array-not-contain check adds no signal and implies the test enforces something broader than the exact-match assertion actually does. * test(sample-setup): switch to per-test `tmpdir()` fixture + assert whitespace-in-path documented gap Two bot-review follow-ups on PR #1086: - Coderabbit + cubic (P3): the `countSampleContents` describe used one shared `mkdtempSync` at module scope with cleanup only in `afterAll`. Two problems: the fixture leaks when the suite is filtered (only redactPaths tests run) or when a `beforeAll` throws before `afterAll` registers, and it disagrees with the repo's `await using tmp = await tmpdir()` convention. Reworked each test to own its own tmp dir via the shared `tmpdir` fixture from `test/fixture`. - Codex: the redactPaths tests exercised each listed path shape but not paths containing whitespace. The greedy pattern terminates at the first `\s`, so a real CWD like `/Users/alice/My Documents/dbt` leaks the middle segment `Documents` between two `<path>` markers. Added a test that: 1. asserts the raw pass DOES leak `Documents` (documents the limitation) 2. asserts the guarded pass (`redactPaths(msg, [cwd])`) collapses cleanly Production callers already pass the CWD as a known-value extra, so the guarded path is the one the wire sees. The test locks that in. * test(telemetry): cover `OPENCODE_DISABLE_TELEMETRY` fallback + gate-site source anchor All three PR #1086 reviewers (codex, coderabbit, cubic) named the same gap: the initial telemetry-opt-out test file only exercised the shared `Flag.truthyEnv` helper against `ALTIMATE_TELEMETRY_DISABLED`. A regression that dropped the `OPENCODE_DISABLE_TELEMETRY` fallback OR removed the `truthyEnv(A) || truthyEnv(B)` OR-composition at either call site (`altimate/plugin/altimate.ts::buildCliContext`, `altimate/telemetry/index.ts::doInit`) would still pass every existing test — the exact regression this suite exists to prevent. Three coverage layers now, one per describe block: 1. **Parser semantics** (existing, unchanged): `Flag.truthyEnv` accepts "true"/"TRUE"/"1", rejects the everything-else surface. 2. **Consumer-boundary composition**: reproduces the `truthyEnv(A) || truthyEnv(B)` shape via a local helper and exercises each branch — only-primary, only-fallback, both-set, neither-set. Proves the OPENCODE fallback works. 3. **Gate-site source anchor**: reads `altimate.ts` and `telemetry/index.ts`, strips line comments, and asserts both env-var names appear inside a two-line window (allowing prettier line-wraps of the OR expression). Fails loudly if a future edit deletes the fallback branch. Brittle by design — but the alternative (booting `doInit()` / `buildCliContext` in-process to observe the effect) requires Config, machine-id, and a sink, which is disproportionate for what this test proves. Env-mutation isolation: bot-review follow-up (coderabbit). Each test now snapshots both env vars in `beforeEach` and restores what it found in `afterEach`, so this suite is safe against other tests in the same process reading either variable. --------- Co-authored-by: Haider <haider@altimate.ai>
1 parent deb93e9 commit 5f51d65

11 files changed

Lines changed: 624 additions & 10 deletions

File tree

‎docs/docs/configure/keybinds.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Override it in your config:
6161
| Leader + `k` | Keybind list |
6262
| Leader + `e` | Open editor |
6363
| Leader + `q` | Quit |
64+
| `Ctrl+Y` | Toggle YOLO mode for this session (confirms when enabling; instant when disabling) |
6465

6566
### Input Editing
6667

@@ -114,7 +115,7 @@ All configurable keybind identifiers:
114115

115116
### Session
116117

117-
`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`
118+
`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`
118119

119120
### Messages
120121

‎docs/docs/configure/permissions.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ The fallback `OPENCODE_YOLO` env var is also supported. When both are set, `ALTI
132132

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

135+
**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.
136+
135137
## Recommended Configurations
136138

137139
### Data Engineering (Default, Balanced)

‎packages/opencode/src/altimate/plugin/altimate.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as OnboardingTelemetry from "../telemetry/onboarding"
88
// altimate_change — shared machine-id helper (race-safe, UUID-validated, size-capped)
99
import { getOrCreateMachineId } from "../util/machine-id"
1010
import { Config } from "@/config/config"
11+
import { Flag } from "@/flag/flag"
1112
import { InstallationVersion } from "@opencode-ai/core/installation/version"
1213
import { Log } from "@/altimate/util/log"
1314

@@ -76,9 +77,11 @@ const log = Log.create({ service: "altimate-plugin" })
7677
export async function buildCliContext(machineIdPath?: string): Promise<string> {
7778
// altimate_change start — honour both telemetry opt-out gates, mirroring
7879
// telemetry/index.ts::doInit:
79-
// 1. ALTIMATE_TELEMETRY_DISABLED=true env var (always-works hard opt-out)
80+
// 1. ALTIMATE_TELEMETRY_DISABLED / OPENCODE_DISABLE_TELEMETRY env vars
81+
// ("true"/"TRUE"/"1", case-insensitive — see Flag.truthyEnv)
8082
// 2. config.telemetry.disabled (resolved via the async Config.get())
81-
let disabled = process.env.ALTIMATE_TELEMETRY_DISABLED === "true"
83+
let disabled =
84+
Flag.truthyEnv("ALTIMATE_TELEMETRY_DISABLED") || Flag.truthyEnv("OPENCODE_DISABLE_TELEMETRY")
8285
if (!disabled) {
8386
try {
8487
const userConfig = (await Config.get()) as any

‎packages/opencode/src/altimate/telemetry/index.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1683,7 +1683,10 @@ export namespace Telemetry {
16831683

16841684
async function doInit() {
16851685
try {
1686-
if (process.env.ALTIMATE_TELEMETRY_DISABLED === "true") {
1686+
// altimate_change — accept "true"/"TRUE"/"1" (case-insensitive) via truthyEnv,
1687+
// and honor the OPENCODE_DISABLE_TELEMETRY fallback promised by v0.9.4's CHANGELOG
1688+
// (previously only wired in test fixtures, silent no-op in product).
1689+
if (Flag.truthyEnv("ALTIMATE_TELEMETRY_DISABLED") || Flag.truthyEnv("OPENCODE_DISABLE_TELEMETRY")) {
16871690
buffer = []
16881691
return
16891692
}

‎packages/opencode/src/altimate/tools/sample-setup.ts‎

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

311+
// altimate_change — exported for direct unit testing (v0.9.5 review, Tech Lead P1).
309312
/** dbt models (`models/**\/*.sql`) and seed tables (`seeds/*.csv`) in the shipped sample. */
310-
function countSampleContents(sampleSourcePath: string): { models: number; tables: number } {
313+
export function countSampleContents(sampleSourcePath: string): { models: number; tables: number } {
311314
return {
312315
models: countFilesWithExtension(path.join(sampleSourcePath, "models"), ".sql"),
313316
tables: countFilesWithExtension(path.join(sampleSourcePath, "seeds"), ".csv"),

‎packages/opencode/src/cli/welcome.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,16 @@ export function showWelcomeBannerIfNeeded(): void {
4343
// launch. Probe existence with existsSync only — do NOT mint here. Minting is left to
4444
// Telemetry.doInit() (its job, not the welcome banner's); the first_launch machine_id is
4545
// attached at flush time from telemetry module state, so it does not depend on minting here.
46-
// NOTE: doInit's early call runs before an Instance is available, so its CONFIG opt-out gate
47-
// fails open and it can still mint for a config-only opt-out user. That is a pre-existing
48-
// telemetry-init gap (tracked separately), not something this banner can fix — this code just
49-
// stops adding a SECOND minting site under an even weaker (env-only) gate.
46+
//
47+
// FIXME(telemetry-init-config-opt-out): doInit() may run before Instance.provide() has made
48+
// Config.get() resolvable (see the try/catch around Config.get in telemetry/index.ts::doInit
49+
// — the catch branch proceeds with telemetry enabled). A user who opted out via the
50+
// `telemetry.disabled` config key — with no env var set — can therefore still get a
51+
// machine-id minted on first launch. The env-var opt-out (ALTIMATE_TELEMETRY_DISABLED /
52+
// OPENCODE_DISABLE_TELEMETRY) is unaffected — that check does not need Instance context.
53+
// Pre-existing (not introduced by this release); calling it out explicitly here rather than
54+
// leaving the earlier "(tracked separately)" wording, which claimed a tracking issue that
55+
// does not currently exist.
5056
const machineIdPath = path.join(os.homedir(), ".altimate", "machine-id")
5157
const isUpgrade = fs.existsSync(machineIdPath)
5258
// altimate_change end

‎packages/opencode/src/flag/flag.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ function altEnv(altKey: string, openKey: string) {
1919
// altimate_change end
2020

2121
export namespace Flag {
22+
// altimate_change start — runtime-evaluated env-truthy helper for callers that need
23+
// current process.env state. Module-level Flag.* constants freeze their value at
24+
// import time; that's the wrong semantics for gates the caller re-reads on each
25+
// invocation (e.g. telemetry.doInit()). Accepts "true" / "TRUE" / "1" — case-insensitive
26+
// — so one convention covers every telemetry/onboarding opt-out env var.
27+
export function truthyEnv(key: string): boolean {
28+
return truthy(key)
29+
}
30+
// altimate_change end
2231
// altimate_change start - ALTIMATE_CLI_CLIENT flag with OPENCODE_CLIENT fallback
2332
export declare const ALTIMATE_CLI_CLIENT: string
2433
// altimate_change end
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// v0.9.5 review — Tech Lead P1 (initial pass) + coderabbit / cubic follow-ups.
2+
//
3+
// sample-setup.ts::redactPaths has a docstring listing three specific bugs the
4+
// implementation was written to fix (regex swallowing surrounding sentence,
5+
// `/root/…` prefix leaking, José/O'Connor surnames leaking on the first pass).
6+
// None of them had test coverage this release. This file asserts the fixes so a
7+
// future edit to the regex or the known-value substitution loop can't silently
8+
// re-introduce them.
9+
//
10+
// countSampleContents is a tiny counting helper — but it's called on every
11+
// sample_setup invocation and its output feeds a telemetry event, so a
12+
// silent-zero bug (typo in the extension filter, wrong subdirectory name)
13+
// would misreport onboarding activity. The fixture below builds a real dir tree
14+
// per test and asserts the count.
15+
//
16+
// Fixture ownership note (bot-review follow-up, coderabbit + cubic):
17+
// countSampleContents originally shared one `mkdtempSync` at module scope with
18+
// afterAll cleanup. That leaks if the suite is filtered (only redactPaths tests
19+
// selected) or if a `beforeAll` throws before `afterAll` registers. Switched to
20+
// the repo's `await using tmp = await tmpdir()` pattern so each test owns its
21+
// fixture and cleanup is bound to the test scope.
22+
23+
import { describe, expect, test } from "bun:test"
24+
import fs from "fs"
25+
import path from "path"
26+
27+
import { tmpdir } from "../fixture/fixture"
28+
import { redactPaths, countSampleContents } from "../../src/altimate/tools/sample-setup"
29+
30+
describe("redactPaths", () => {
31+
test("redacts an absolute POSIX path", () => {
32+
const out = redactPaths("failed at /usr/local/bin/dbt")
33+
expect(out).toBe("failed at <path>")
34+
})
35+
36+
test("redacts a Windows drive path", () => {
37+
const out = redactPaths("failed at C:\\Users\\alice\\dbt.exe")
38+
// Windows paths pattern matches `C:\` opener and consumes until whitespace/quote.
39+
expect(out).toBe("failed at <path>")
40+
expect(out).not.toContain("Users")
41+
expect(out).not.toContain("alice")
42+
})
43+
44+
test("redacts a home-relative path (~/)", () => {
45+
// Note: `~/.altimate/…` is redacted by the POSIX-`/` pattern first, which
46+
// starts at the leading slash and leaves the `~` as a harmless prefix.
47+
// What the assertion cares about is that no path segment leaks — the exact
48+
// shape of the redaction marker is secondary.
49+
const out = redactPaths("cannot read ~/.altimate/machine-id")
50+
expect(out).not.toContain(".altimate")
51+
expect(out).not.toContain("machine-id")
52+
expect(out).toContain("<path>")
53+
})
54+
55+
test("redacts a bare tilde-only path (~/x with no preceding slash)", () => {
56+
// The dedicated `~\/…` pattern is what catches this shape — the POSIX-`/`
57+
// one starts inside the path and can leave a `~` behind.
58+
const input = "opening ~/opt/dbt for read"
59+
const out = redactPaths(input)
60+
expect(out).not.toContain("opt/dbt")
61+
expect(out).toContain("<path>")
62+
})
63+
64+
test("terminates at whitespace, does NOT swallow the surrounding sentence", () => {
65+
// The docstring calls out this exact class of bug: an early implementation
66+
// used a character class that included `.`, so the regex would consume the
67+
// rest of the sentence past the path. Reader ends up with just "<path>".
68+
const input = "Underlying error: /Users/alice/projects/dbt-demo failed to compile"
69+
const out = redactPaths(input)
70+
expect(out).toContain("failed to compile")
71+
expect(out).toContain("Underlying error:")
72+
expect(out).not.toContain("/Users")
73+
})
74+
75+
test("terminates at a double-quote", () => {
76+
const out = redactPaths('opening "/Users/alice/dbt_project.yml" for read')
77+
expect(out).toContain("for read")
78+
expect(out).not.toContain("alice")
79+
})
80+
81+
test("handles a path containing an apostrophe (O'Connor)", () => {
82+
// Docstring bug: an early character class excluded `'`, so the regex would
83+
// stop at the apostrophe and leak the substring after it. Now apostrophes
84+
// are permitted inside the redacted run.
85+
const out = redactPaths("failed at /Users/O'Connor/projects/x")
86+
expect(out).not.toContain("O'Connor")
87+
expect(out).not.toContain("Connor")
88+
expect(out).toBe("failed at <path>")
89+
})
90+
91+
test("handles a path containing an accented character (José)", () => {
92+
const out = redactPaths("failed at /Users/José/dbt")
93+
expect(out).not.toContain("José")
94+
expect(out).toBe("failed at <path>")
95+
})
96+
97+
test("path segment terminates at the first whitespace, so a path containing a space leaks the tail", () => {
98+
// codex-review gap: the greedy pattern stops at the first `\s`, so a real
99+
// CWD like `/Users/alice/My Documents/dbt` gets split — only `/Users/alice/My`
100+
// is redacted; `Documents/dbt` is left in the output.
101+
//
102+
// The `extra` list is what production callers use to close this gap (they
103+
// pass the exact CWD to `redactPaths(msg, [cwd])`), so this test also
104+
// asserts the compensating behavior — with the CWD known-value, the whole
105+
// path collapses cleanly.
106+
const cwd = "/Users/alice/My Documents/dbt"
107+
const raw = redactPaths(`failed at ${cwd}/models/foo.sql`)
108+
// Documented limitation of the pattern-only pass: the greedy path pattern
109+
// terminates at the first whitespace, so `/Users/alice/My` and
110+
// `/dbt/models/foo.sql` each redact cleanly but the middle segment
111+
// `Documents` sits between two `<path>` markers.
112+
expect(raw).toContain("Documents")
113+
expect(raw).not.toBe("failed at <path>")
114+
// With the CWD passed as a known value the whole path collapses cleanly:
115+
const guarded = redactPaths(`failed at ${cwd}/models/foo.sql`, [cwd])
116+
expect(guarded).toBe("failed at <path>")
117+
expect(guarded).not.toContain("Documents")
118+
expect(guarded).not.toContain("alice")
119+
})
120+
121+
test("collapses adjacent <path> segments so double-redaction reads clean", () => {
122+
// The known-value pass replaces os.homedir() etc first; the greedy pattern
123+
// then may match the "<path>" tail and re-redact. The collapse rule keeps
124+
// the output from becoming "<path><path><path>".
125+
const home = require("os").homedir()
126+
const out = redactPaths(`failed at ${home}/dbt/models/foo.sql`)
127+
expect(out).toBe("failed at <path>")
128+
expect(out).not.toMatch(/<path><path>/)
129+
})
130+
131+
test("passes short/empty known values without exploding", () => {
132+
// Guard for `known.length < 2` — empty string or single-char known values
133+
// used to `split("")` and shatter every character. The guard keeps them out
134+
// of the substitution loop.
135+
const out = redactPaths("hello world", ["", "a", undefined])
136+
expect(out).toBe("hello world")
137+
})
138+
139+
test("substitutes user-supplied extras", () => {
140+
const out = redactPaths("clone failed at /tmp/checkout-xyz", ["/tmp/checkout-xyz"])
141+
expect(out).toBe("clone failed at <path>")
142+
})
143+
144+
test("returns the message unchanged when nothing path-shaped is present", () => {
145+
expect(redactPaths("dbt run completed in 3s")).toBe("dbt run completed in 3s")
146+
})
147+
})
148+
149+
// Shared helper — each test uses its own tmp dir via `await using`, so cleanup
150+
// is scoped to the test itself (bot-review follow-up).
151+
async function seedSampleTree(dir: string) {
152+
fs.mkdirSync(path.join(dir, "models", "staging"), { recursive: true })
153+
fs.mkdirSync(path.join(dir, "models", "marts", "core"), { recursive: true })
154+
fs.mkdirSync(path.join(dir, "seeds"), { recursive: true })
155+
156+
fs.writeFileSync(path.join(dir, "models", "top.sql"), "select 1")
157+
fs.writeFileSync(path.join(dir, "models", "staging", "stg_orders.sql"), "select 1")
158+
fs.writeFileSync(path.join(dir, "models", "staging", "stg_users.sql"), "select 1")
159+
fs.writeFileSync(path.join(dir, "models", "marts", "core", "dim_customers.sql"), "select 1")
160+
161+
// Non-.sql alongside .sql, must NOT be counted.
162+
fs.writeFileSync(path.join(dir, "models", "readme.md"), "hi")
163+
fs.writeFileSync(path.join(dir, "models", "schema.yml"), "version: 2")
164+
165+
fs.writeFileSync(path.join(dir, "seeds", "country_codes.csv"), "code,name\n")
166+
fs.writeFileSync(path.join(dir, "seeds", "regions.csv"), "id,name\n")
167+
// Non-.csv seed — not counted.
168+
fs.writeFileSync(path.join(dir, "seeds", "notes.md"), "hi")
169+
}
170+
171+
describe("countSampleContents", () => {
172+
test("counts .sql files recursively under models/", async () => {
173+
await using tmp = await tmpdir()
174+
await seedSampleTree(tmp.path)
175+
expect(countSampleContents(tmp.path).models).toBe(4)
176+
})
177+
178+
test("counts .csv files under seeds/ (top level; matches production sample layout)", async () => {
179+
await using tmp = await tmpdir()
180+
await seedSampleTree(tmp.path)
181+
expect(countSampleContents(tmp.path).tables).toBe(2)
182+
})
183+
184+
test("returns zeros when the sample dir is missing the expected subdirs", async () => {
185+
await using tmp = await tmpdir()
186+
expect(countSampleContents(tmp.path)).toEqual({ models: 0, tables: 0 })
187+
})
188+
189+
test("returns zeros when the sample dir does not exist at all", async () => {
190+
// countFilesWithExtension swallows the readdirSync error and returns 0 —
191+
// this is the graceful-degradation shape the telemetry event depends on.
192+
await using tmp = await tmpdir()
193+
expect(countSampleContents(path.join(tmp.path, "does-not-exist"))).toEqual({ models: 0, tables: 0 })
194+
})
195+
})

0 commit comments

Comments
 (0)