Skip to content
Draft
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
92 changes: 92 additions & 0 deletions packages/shared/src/cli/commands/generate-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,95 @@ describe("generate-types foreground spawn orchestration", () => {
expect(acquireSpawnLock).not.toHaveBeenCalled();
});
});

describe("generate-types --no-cache (F5)", () => {
let tmpRoot: string;
const prevWarehouse = process.env.DATABRICKS_WAREHOUSE_ID;

beforeEach(() => {
vi.clearAllMocks();
acquireSpawnLock.mockReturnValue(true);
// commander's `generateTypesCommand` is a module singleton whose parsed
// option values persist across parseAsync calls. Reset the flags these tests
// care about to their parse-time defaults so an absent flag in one test isn't
// polluted by an explicit flag in a previous one (e.g. a prior `--no-cache`
// leaving `cache:false`, or an earlier `--worker-lock` leaving workerLock set
// → no spawn).
generateTypesCommand.setOptionValue("cache", true);
generateTypesCommand.setOptionValue("wait", undefined);
generateTypesCommand.setOptionValue("workerLock", undefined);
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "gentypes-nc-"));
fs.mkdirSync(path.join(tmpRoot, "config", "queries"), { recursive: true });
process.env.DATABRICKS_WAREHOUSE_ID = "wh-123";
vi.spyOn(console, "log").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});
});

afterEach(() => {
vi.restoreAllMocks();
fs.rmSync(tmpRoot, { recursive: true, force: true });
if (prevWarehouse === undefined) {
delete process.env.DATABRICKS_WAREHOUSE_ID;
} else {
process.env.DATABRICKS_WAREHOUSE_ID = prevWarehouse;
}
});

test("foreground bypasses cache: noCache:true reaches generateFromEntryPoint AND generateServingTypes", async () => {
// The bug F5 fixes: the flag was read from `options.noCache` (which commander
// never sets), so `--no-cache` was a silent no-op. It must now flow as
// noCache:true into BOTH generators.
await runCli([tmpRoot, "--no-cache"]);

expect(generateFromEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({ noCache: true }),
);
expect(generateServingTypes).toHaveBeenCalledWith(
expect.objectContaining({ noCache: true }),
);
});

test("without --no-cache the foreground keeps caching (noCache:false)", async () => {
// `=== false` semantics: an absent flag (undefined cache) must NOT disable
// the cache.
await runCli([tmpRoot]);

expect(generateFromEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({ noCache: false }),
);
expect(generateServingTypes).toHaveBeenCalledWith(
expect.objectContaining({ noCache: false }),
);
});

test("spawned worker argv includes --no-cache when set, and still carries execArgv + --wait", async () => {
const outFile = path.join(tmpRoot, "shared/appkit-types/analytics.d.ts");

await runCli([tmpRoot, outFile, "wh-123", "--no-cache"]);

expect(spawn).toHaveBeenCalledTimes(1);
const [, argv] = spawn.mock.calls[0];

// De-stale guard: the parent's loader flags are still forwarded before the
// CLI entry (dropping them silently breaks a tsx-run worker).
const entryIdx = argv.indexOf(process.argv[1]);
expect(entryIdx).toBeGreaterThanOrEqual(0);
expect(argv.slice(0, entryIdx)).toEqual(process.execArgv);

// The worker invocation forwards --wait AND --no-cache (so the background
// refresh bypasses the cache exactly like the foreground).
const workerArgv = argv.slice(entryIdx);
expect(workerArgv).toContain("--wait");
expect(workerArgv).toContain("--no-cache");
});

test("spawned worker argv omits --no-cache when the flag is absent", async () => {
await runCli([tmpRoot]);

expect(spawn).toHaveBeenCalledTimes(1);
const [, argv] = spawn.mock.calls[0];
expect(argv).not.toContain("--no-cache");
// But --wait is always present on the worker.
expect(argv).toContain("--wait");
});
});
30 changes: 27 additions & 3 deletions packages/shared/src/cli/commands/generate-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ export function resolveTypegenMode(options?: {

/** Options parsed by commander for the generate-types command. */
interface GenerateTypesOptions {
noCache?: boolean;
/**
* Caching toggle. Commander stores the `--no-cache` boolean negation here as
* `cache` (NOT `noCache`): the option is `true` by default and `false` when
* `--no-cache` is passed. Reading `options.noCache` (which commander never
* populates) is why `--no-cache` used to be a silent no-op.
*/
cache?: boolean;
wait?: boolean;
/**
* Internal: present only on the detached worker invocation. Carries the path
Expand All @@ -51,7 +57,10 @@ async function runGenerateTypes(
) {
try {
const resolvedRootDir = rootDir || process.cwd();
const noCache = options?.noCache || false;
// Commander stores `--no-cache` as `cache: false` (the option defaults to
// true). `=== false` is deliberate: only an explicit `--no-cache` disables
// caching; an absent flag (undefined) leaves it on.
const noCache = options?.cache === false;
const mode = resolveTypegenMode(options);

const typeGen = await import("@databricks/appkit/type-generator");
Expand Down Expand Up @@ -138,11 +147,16 @@ async function runGenerateTypes(
* @param lockPath - the acquired single-flight lock; passed to the worker so it
* releases the SAME lock when it finishes.
* @param targets - the foreground's positional args, forwarded verbatim.
* @param noCache - whether the foreground ran with `--no-cache`; when true the
* worker is launched with `--no-cache` too, so the background refresh bypasses
* the cache exactly like the foreground did (otherwise the worker would re-read
* the very cache the user asked to skip).
* @returns true if the worker was spawned, false if spawning threw.
*/
export function spawnTypegenWorker(
lockPath: string,
targets: { rootDir?: string; outFile?: string; warehouseId?: string },
noCache = false,
): boolean {
// The script the runtime launched us with (the `appkit` bin shim). Re-running
// it under the same node binary reproduces this exact CLI in the worker.
Expand All @@ -169,6 +183,10 @@ export function spawnTypegenWorker(
cliEntry,
"generate-types",
"--wait",
// Mirror the foreground's cache choice: a `--no-cache` foreground must spawn
// a `--no-cache` worker, or the background refresh would re-read the cache
// the user explicitly opted out of.
...(noCache ? ["--no-cache"] : []),
"--worker-lock",
lockPath,
...positionals,
Expand Down Expand Up @@ -236,7 +254,13 @@ async function generateTypesAction(
const lockPath = getSpawnLockPath(resolvedRootDir);

if (acquireSpawnLock(lockPath)) {
spawnTypegenWorker(lockPath, { rootDir, outFile, warehouseId });
// Carry the foreground's cache choice into the worker (commander stored
// `--no-cache` as `cache: false`).
spawnTypegenWorker(
lockPath,
{ rootDir, outFile, warehouseId },
options.cache === false,
);
} else {
console.log("Type refresh already in progress, skipping.");
}
Expand Down
Loading