Skip to content

Commit 30deef1

Browse files
committed
test(cli): cover embedded suppression, v3Alternative pointer, and null-clientPath re-run
Close the three test gaps auxesis flagged on #705 — all callee-side or negative-case coverage the diff's new behaviour landed without: - eql/migration.ts: assert the six `if (!embedded)` suppression sites and the `?? false` default at the callee (banners emitted by default; suppressed + SQL still written when embedded; abort outro suppressed but CliExit still propagates). - db/install.ts: assert the `v3Alternative` pointer via its distinctive `stash eql migration --drizzle` substring (the pre-diff `/--drizzle/` match was unguarded), plus the lopsided-negative that it stays absent for --latest / --migration / --migrations-dir. - init/steps/install-eql.ts: cover the null-clientPath re-run path (config already on disk) and assert a plain Drizzle project passes supabase: undefined. Tests-only; no production change.
1 parent c8726cd commit 30deef1

3 files changed

Lines changed: 120 additions & 0 deletions

File tree

packages/cli/src/__tests__/supabase-migration.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,36 @@ describe('validateInstallFlags', () => {
338338
}),
339339
).toBeNull()
340340
})
341+
342+
it('points --drizzle at the v3 migration command instead of only at --eql-version 2', () => {
343+
// Steering users to `--eql-version 2` is how new projects ended up on the
344+
// legacy generation (#691). Both the default-branch wording and the explicit
345+
// `--eql-version 3` wording must carry the pointer. Assert the distinctive
346+
// substring, not a bare `/--drizzle/` — that already appears in the base
347+
// message body and would pass even if the pointer were dropped.
348+
expect(validateInstallFlags({ drizzle: true })).toMatch(
349+
/stash eql migration --drizzle/,
350+
)
351+
expect(validateInstallFlags({ eqlVersion: '3', drizzle: true })).toMatch(
352+
/stash eql migration --drizzle/,
353+
)
354+
})
355+
356+
it('does NOT offer the Drizzle alternative for the other v2-only flags', () => {
357+
// There is no `stash eql migration` route for these — suggesting one would
358+
// send the user to a command that cannot help them. Guards the pointer being
359+
// gated on `v2OnlyFlag === '--drizzle'`.
360+
for (const opts of [
361+
{ latest: true },
362+
{ supabase: true, migration: true },
363+
{ supabase: true, migrationsDir: 'db/migrations' },
364+
]) {
365+
expect(validateInstallFlags(opts)).not.toMatch(/stash eql migration/)
366+
expect(validateInstallFlags({ ...opts, eqlVersion: '3' })).not.toMatch(
367+
/stash eql migration/,
368+
)
369+
}
370+
})
341371
})
342372

343373
describe('routeInstallPathForEqlVersion', () => {

packages/cli/src/commands/eql/__tests__/migration.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { join } from 'node:path'
1111
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
1212
import { CliExit } from '../../../cli/exit.js'
1313
import { messages } from '../../../messages.js'
14+
import { printNextSteps } from '../../db/install.js'
1415
import { buildEqlV3MigrationSql, eqlMigrationCommand } from '../migration.js'
1516

1617
// clack is chrome — silence it and spy on the error/note channels the command
@@ -242,4 +243,63 @@ describe('eqlMigrationCommand — Drizzle', () => {
242243
expect(existsSync(scaffolded)).toBe(false)
243244
expect(clack.log.error).toHaveBeenCalledWith('EACCES: permission denied')
244245
})
246+
247+
// The `embedded` flag is asserted at the caller (install-eql.test.ts) as
248+
// *passed*; these pin what it *does* at the callee. All six suppression sites
249+
// hang off `options.embedded ?? false`, and none were exercised — flipping the
250+
// default or dropping an `if (!embedded)` guard would pass CI silently.
251+
it('emits the standalone banners by default (embedded unset)', async () => {
252+
const out = join(tmp, 'drizzle')
253+
mkdirSync(out, { recursive: true })
254+
spawnMock.mockImplementation(() => {
255+
fsWrite.real(join(out, '0000_install-eql.sql'), '')
256+
return { status: 0, stdout: '', stderr: '' }
257+
})
258+
259+
await eqlMigrationCommand({ drizzle: true, out })
260+
261+
expect(clack.intro).toHaveBeenCalled()
262+
expect(clack.outro).toHaveBeenCalledWith('Done!')
263+
expect(printNextSteps).toHaveBeenCalled()
264+
})
265+
266+
it('suppresses intro/outro/next-steps when embedded, but still writes the SQL', async () => {
267+
// `stash init` renders its own summary + agent handoff; a second "what next"
268+
// block from here would compete with it. The migration itself is unchanged.
269+
const out = join(tmp, 'drizzle')
270+
mkdirSync(out, { recursive: true })
271+
spawnMock.mockImplementation(() => {
272+
fsWrite.real(join(out, '0000_install-eql.sql'), '')
273+
return { status: 0, stdout: '', stderr: '' }
274+
})
275+
276+
await eqlMigrationCommand({ drizzle: true, out, embedded: true })
277+
278+
expect(clack.intro).not.toHaveBeenCalled()
279+
expect(clack.outro).not.toHaveBeenCalled()
280+
expect(printNextSteps).not.toHaveBeenCalled()
281+
// Presentational suppression only — the migration note itself still renders,
282+
// and the SQL is written to the located file.
283+
expect(clack.note).toHaveBeenCalledWith(expect.any(String), 'Next Steps')
284+
expect(readFileSync(join(out, '0000_install-eql.sql'), 'utf-8')).toContain(
285+
'EQL v3 schema creation',
286+
)
287+
})
288+
289+
it('suppresses the abort outro when embedded but still exits 1', async () => {
290+
// `install-eql`'s catch depends on the CliExit propagating; embedded must
291+
// silence the outro banner without swallowing the failure.
292+
spawnMock.mockReturnValue({ status: 1, stdout: '', stderr: 'boom' })
293+
294+
await expect(
295+
eqlMigrationCommand({
296+
drizzle: true,
297+
out: join(tmp, 'drizzle'),
298+
embedded: true,
299+
}),
300+
).rejects.toBeInstanceOf(CliExit)
301+
302+
expect(clack.outro).not.toHaveBeenCalled()
303+
expect(clack.log.error).toHaveBeenCalledWith('boom')
304+
})
245305
})

packages/cli/src/commands/init/steps/__tests__/install-eql.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,36 @@ describe('installEqlStep', () => {
160160
)
161161
})
162162

163+
it('passes supabase: undefined for a plain (non-Supabase) Drizzle project', async () => {
164+
// Symmetric negative to the --supabase forward: a plain Drizzle project
165+
// must NOT leak supabase: true, or the migration would append role grants
166+
// no one asked for. `toMatchObject` above ignores the key, so it can't
167+
// catch a leak — this asserts it explicitly.
168+
await installEqlStep.run(drizzleState, drizzleProvider)
169+
170+
expect(
171+
vi.mocked(eqlMigrationCommand).mock.calls[0][0].supabase,
172+
).toBeUndefined()
173+
})
174+
175+
it('still generates the migration when stash.config.ts already exists', async () => {
176+
// offerStashConfig returns null when the config is already on disk — the
177+
// re-run case (config-scaffold.ts: `if (existsSync(configPath)) return
178+
// null`), i.e. every re-run of `stash init` on a Drizzle project. Nothing
179+
// to scaffold, but the migration must still be written and the state must
180+
// still say "pending". If the `if (clientPath)` guard were dropped,
181+
// ensureEncryptionClient(null, …) would throw outside the try/catch and
182+
// abort init after the user has already authenticated.
183+
vi.mocked(offerStashConfig).mockResolvedValueOnce(null)
184+
185+
const result = await installEqlStep.run(drizzleState, drizzleProvider)
186+
187+
expect(ensureEncryptionClient).not.toHaveBeenCalled()
188+
expect(eqlMigrationCommand).toHaveBeenCalledTimes(1)
189+
expect(result.eqlMigrationPending).toBe(true)
190+
expect(result.eqlInstalled).toBe(false)
191+
})
192+
163193
it('degrades to "not installed" (never crashes init) when drizzle-kit is missing', async () => {
164194
// eqlMigrationCommand throws CliExit when drizzle-kit isn't installed or
165195
// configured. init must absorb that and report honestly — a thrown

0 commit comments

Comments
 (0)