Skip to content

Commit d8a863b

Browse files
committed
fix(cli): reset requires --allow-destructive; atomic .env writer; seed prod guard
1 parent a59c303 commit d8a863b

7 files changed

Lines changed: 293 additions & 271 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ build
33
tmp
44
node_modules
55
.env
6+
.env.bak
7+
.env.bak.*
8+
.env.tmp.*
69

710
coverage
811

packages/shared/src/cli/commands/db/__tests__/init.test.ts

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,8 @@ const DEFAULT_CREATE_BRANCH = {
108108
};
109109

110110
/**
111-
* Build a fake Databricks CLI runner. Each call inspects the args and returns
112-
* the canned response for the matching subcommand. Strongly typed so a typo
113-
* in a fixture (e.g. `endpoint_typee`) fails to compile instead of silently
114-
* misbehaving at runtime.
111+
* Fake Databricks CLI runner that returns a canned response per subcommand.
112+
* Typed so a fixture typo (e.g. `endpoint_typee`) is a compile error.
115113
*/
116114
function fakeCli(responses: FakeCliResponses = {}) {
117115
const tracked = vi.fn(async (args: string[]): Promise<unknown> => {
@@ -156,7 +154,7 @@ function mkTempProject(
156154
opts: { schema?: boolean; seed?: boolean } = {},
157155
): TestEnv {
158156
const cwd = mkdtempSync(path.join(tmpdir(), "appkit-init-"));
159-
// databasePaths() walks up from cwd until it finds package.json.
157+
// databasePaths() walks up from cwd looking for package.json.
160158
writeFileSync(path.join(cwd, "package.json"), '{"name":"fixture"}');
161159
if (opts.schema) {
162160
const configDir = path.join(cwd, "config", "database");
@@ -180,9 +178,7 @@ let testEnv: TestEnv | undefined;
180178
let envSnapshot: Record<string, string | undefined>;
181179

182180
beforeEach(() => {
183-
// Snapshot the OWNED env keys so tests that go through the default writer
184-
// don't bleed state into one another. Anything outside the owned set is
185-
// left alone.
181+
// Snapshot OWNED keys so the default writer can't bleed state between tests.
186182
envSnapshot = Object.fromEntries(
187183
OWNED_ENV_KEYS.map((key) => [key, process.env[key]]),
188184
);
@@ -199,13 +195,9 @@ afterEach(() => {
199195
});
200196

201197
/**
202-
* Build deps with sane defaults plus an env-update collector and an
203-
* always-interactive `isInteractive`. Tests that need real `.env` writes can
204-
* override `applyEnvUpdates`.
205-
*
206-
* `loadSchemaFile` defaults to a fake that returns a non-empty `$tables` map
207-
* so the migrate/reset preflight passes without requiring tests to write a
208-
* real Drizzle schema. Tests that exercise the preflight can override this.
198+
* Build deps with sane defaults: env-update collector, interactive=true, and
199+
* a `loadSchemaFile` fake with non-empty `$tables` so the migrate preflight
200+
* passes. Tests that exercise either path override the relevant field.
209201
*/
210202
function makeDeps(
211203
overrides: Partial<RunInitDeps> & {
@@ -256,9 +248,8 @@ function makeDeps(
256248
}
257249

258250
function scriptedOptions(extra: Partial<RunInitOptions> = {}): RunInitOptions {
259-
// Always pass --profile, --project, --from, --cwd to skip every interactive
260-
// prompt so tests stay deterministic and don't try to render @clack/prompts
261-
// on a non-TTY stdin.
251+
// Pass every flag that would otherwise prompt, so tests never render @clack
252+
// on non-TTY stdin and stay deterministic.
262253
return {
263254
profile: "DEFAULT",
264255
project: "projects/foo",
@@ -341,9 +332,8 @@ describe("initCommand", () => {
341332
});
342333

343334
test("commander parses --no-seed as seed=false; --seed as true; absent as undefined", async () => {
344-
// Guards against a future Commander upgrade quietly changing this. We
345-
// assert against `opts()` directly rather than re-running the action so
346-
// we don't have to mock everything `runInit` touches.
335+
// Guards against a future Commander upgrade silently changing this.
336+
// Assert on opts() directly so we don't have to mock all of runInit.
347337
const cases: Array<[string[], boolean | undefined]> = [
348338
[[], undefined],
349339
[["--seed"], true],
@@ -391,8 +381,7 @@ describe("runInit — migrate flow", () => {
391381
});
392382

393383
test("soft-fails gracefully when config/database/schema.ts is missing", async () => {
394-
// Previously this threw; now the preflight prints a starter snippet and
395-
// returns so the user can treat `db init` as a safe first-run command.
384+
// Used to throw; now prints a starter snippet so `db init` is safe to re-run.
396385
testEnv = mkTempProject({ schema: false });
397386

398387
const deps = makeDeps({ tableCount: 0 });
@@ -430,20 +419,27 @@ describe("runInit — reset flow", () => {
430419
testEnv = mkTempProject({ schema: true });
431420

432421
const deps = makeDeps({});
433-
// `--yes` skips the typed-branch confirmation; prod CI sets it the same way.
422+
// `--yes` skips the typed-branch confirm (same flag CI uses).
434423
await runInit(
435-
scriptedOptions({ from: "reset", seed: false, yes: true }),
424+
scriptedOptions({
425+
from: "reset",
426+
seed: false,
427+
yes: true,
428+
allowDestructive: true,
429+
}),
436430
deps,
437431
);
438432

439-
expect(deps.dropAllAppTables).toHaveBeenCalledWith({ schema: "public" });
433+
expect(deps.dropAllAppTables).toHaveBeenCalledWith({
434+
schema: "public",
435+
allowDestructive: true,
436+
});
440437
expect(deps.setupDev).toHaveBeenCalledWith({
441438
name: "init",
442439
seed: false,
443440
force: false,
444441
});
445-
// dropAllAppTables must run BEFORE setupDev — ordering matters so we
446-
// don't try to apply migrations on top of stale tables.
442+
// dropAllAppTables MUST precede setupDev: we don't migrate over stale tables.
447443
const dropOrder = deps.dropAllAppTables.mock.invocationCallOrder[0];
448444
const setupOrder = deps.setupDev.mock.invocationCallOrder[0];
449445
expect(dropOrder).toBeLessThan(setupOrder);
@@ -459,18 +455,25 @@ describe("runInit — reset flow", () => {
459455
schema: "app",
460456
seed: false,
461457
yes: true,
458+
allowDestructive: true,
462459
}),
463460
deps,
464461
);
465462

466-
expect(deps.dropAllAppTables).toHaveBeenCalledWith({ schema: "app" });
463+
expect(deps.dropAllAppTables).toHaveBeenCalledWith({
464+
schema: "app",
465+
allowDestructive: true,
466+
});
467467
});
468468

469469
test("reset short-circuits on missing schema.ts (does NOT drop tables)", async () => {
470470
testEnv = mkTempProject({ schema: false });
471471

472472
const deps = makeDeps({});
473-
await runInit(scriptedOptions({ from: "reset", yes: true }), deps);
473+
await runInit(
474+
scriptedOptions({ from: "reset", yes: true, allowDestructive: true }),
475+
deps,
476+
);
474477

475478
expect(deps.dropAllAppTables).not.toHaveBeenCalled();
476479
expect(deps.setupDev).not.toHaveBeenCalled();
@@ -483,7 +486,12 @@ describe("runInit — reset flow", () => {
483486
loadSchemaFile: vi.fn(async () => ({ $drizzle: {}, $tables: {} })),
484487
});
485488
await runInit(
486-
scriptedOptions({ from: "reset", seed: false, yes: true }),
489+
scriptedOptions({
490+
from: "reset",
491+
seed: false,
492+
yes: true,
493+
allowDestructive: true,
494+
}),
487495
deps,
488496
);
489497

@@ -510,7 +518,7 @@ describe("runInit — dry-run", () => {
510518

511519
describe("runInit — seed gate", () => {
512520
test("--seed with missing seed.sql warns and skips instead of crashing", async () => {
513-
// schema: true, seed: false on disk but caller passes --seed=true.
521+
// No seed.sql on disk, but caller passes --seed=true.
514522
testEnv = mkTempProject({ schema: true, seed: false });
515523

516524
const deps = makeDeps({});
@@ -540,7 +548,7 @@ describe("runInit — seed gate", () => {
540548
testEnv = mkTempProject({ schema: true, seed: false });
541549

542550
const deps = makeDeps({});
543-
// Omit seed entirely (scriptedOptions defaults seed=false; strip it).
551+
// scriptedOptions defaults seed=false; strip it to test "absent" path.
544552
const opts = scriptedOptions({ from: "migrate" });
545553
delete (opts as { seed?: unknown }).seed;
546554
await runInit(opts, deps);
@@ -665,7 +673,7 @@ describe("runInit — env writer", () => {
665673
"utf8",
666674
);
667675

668-
// Use the default writer (no override) so we exercise the real file path.
676+
// No overrideexercise the real file-path writer.
669677
const deps = makeDeps({ tableCount: 0 });
670678
deps.applyEnvUpdates = undefined as unknown as ReturnType<typeof vi.fn>;
671679
await runInit(scriptedOptions({ from: "migrate", seed: false }), {

0 commit comments

Comments
 (0)