Enhance setBranchConfigOverride for local emulator file handling

- Updated the `setBranchConfigOverride` function to write branch configuration directly to a file when the local emulator is enabled, ensuring the file serves as the single source of truth.
- Added tests to verify that configuration writes to the local emulator file are handled correctly and that database operations are skipped when writing to the file.
- Improved error handling to surface file write failures before attempting database updates.

These changes enhance the local emulator's configuration management and improve reliability for developers working with branch configurations.
This commit is contained in:
mantrakp04 2026-03-19 15:31:17 -07:00
parent 80ee1ab9fb
commit 42bc12587c
2 changed files with 91 additions and 13 deletions

View File

@ -279,6 +279,17 @@ export async function setBranchConfigOverride(options: {
if (overrideErrors.status === "error") {
captureError("setBranchConfigOverride", new StackAssertionError(`Config override is invalid — at a place where it should have already been validated! ${overrideErrors.error}`, { projectId: options.projectId, branchId: options.branchId }));
}
if (isLocalEmulatorEnabled()) {
const filePath = await getLocalEmulatorFilePath(options.projectId);
if (filePath != null) {
// Local emulator projects read branch config directly from the host config file,
// so the file stays the single source of truth to avoid split DB/file state.
await writeConfigToFile(filePath, newConfig);
return;
}
}
await globalPrismaClient.branchConfigOverride.upsert({
where: {
projectId_branchId: {
@ -295,14 +306,6 @@ export async function setBranchConfigOverride(options: {
config: newConfig,
},
});
// In the local emulator, write config changes back to the config file
if (isLocalEmulatorEnabled()) {
const filePath = await getLocalEmulatorFilePath(options.projectId);
if (filePath != null) {
await writeConfigToFile(filePath, newConfig);
}
}
}
/**
@ -1104,6 +1107,73 @@ import.meta.vitest?.test('setEnvironmentConfigOverride blocks writes in local em
}
});
import.meta.vitest?.test('setBranchConfigOverride writes local emulator config to the file instead of the DB', async ({ expect }) => {
const vi = import.meta.vitest?.vi;
if (!vi) {
throw new StackAssertionError("Vitest context is required for in-source tests.");
}
const localEmulator = await import("./local-emulator");
const isLocalEmulatorEnabledSpy = vi.spyOn(localEmulator, "isLocalEmulatorEnabled").mockReturnValue(true);
const getLocalEmulatorFilePathSpy = vi.spyOn(localEmulator, "getLocalEmulatorFilePath").mockResolvedValue("/Users/foo/project/stack.config.ts");
const writeConfigToFileSpy = vi.spyOn(localEmulator, "writeConfigToFile").mockResolvedValue(undefined);
const upsertSpy = vi.spyOn(globalPrismaClient.branchConfigOverride, "upsert").mockImplementation(() => {
throw new StackAssertionError("DB upsert should not run for local emulator branch config writes.");
});
try {
await setBranchConfigOverride({
projectId: "project-id",
branchId: "branch-id",
branchConfigOverride: {
"teams.allowClientTeamCreation": true,
},
});
expect(writeConfigToFileSpy).toHaveBeenCalledWith("/Users/foo/project/stack.config.ts", {
"teams.allowClientTeamCreation": true,
});
expect(upsertSpy).not.toHaveBeenCalled();
} finally {
upsertSpy.mockRestore();
writeConfigToFileSpy.mockRestore();
getLocalEmulatorFilePathSpy.mockRestore();
isLocalEmulatorEnabledSpy.mockRestore();
}
});
import.meta.vitest?.test('setBranchConfigOverride surfaces local emulator file write failures before touching the DB', async ({ expect }) => {
const vi = import.meta.vitest?.vi;
if (!vi) {
throw new StackAssertionError("Vitest context is required for in-source tests.");
}
const localEmulator = await import("./local-emulator");
const isLocalEmulatorEnabledSpy = vi.spyOn(localEmulator, "isLocalEmulatorEnabled").mockReturnValue(true);
const getLocalEmulatorFilePathSpy = vi.spyOn(localEmulator, "getLocalEmulatorFilePath").mockResolvedValue("/Users/foo/project/stack.config.ts");
const writeConfigToFileSpy = vi.spyOn(localEmulator, "writeConfigToFile").mockRejectedValue(new Error("virtio-9p timeout"));
const upsertSpy = vi.spyOn(globalPrismaClient.branchConfigOverride, "upsert").mockImplementation(() => {
throw new StackAssertionError("DB upsert should not run when the local emulator file write fails.");
});
try {
await expect(setBranchConfigOverride({
projectId: "project-id",
branchId: "branch-id",
branchConfigOverride: {
"teams.allowClientTeamCreation": true,
},
})).rejects.toThrow("virtio-9p timeout");
expect(upsertSpy).not.toHaveBeenCalled();
} finally {
upsertSpy.mockRestore();
writeConfigToFileSpy.mockRestore();
getLocalEmulatorFilePathSpy.mockRestore();
isLocalEmulatorEnabledSpy.mockRestore();
}
});
// ---------------------------------------------------------------------------------------------------------------------
// Conversions
// ---------------------------------------------------------------------------------------------------------------------

View File

@ -1,5 +1,5 @@
import { Command } from "commander";
import { execFileSync, execSync, spawn } from "child_process";
import { execFileSync, spawn } from "child_process";
import { existsSync, mkdirSync, renameSync, unlinkSync } from "fs";
import { join, resolve } from "path";
import { CliError } from "../lib/errors.js";
@ -103,10 +103,18 @@ export function registerEmulatorCommand(program: Command) {
console.log(`Pulling image for ${arch} from release ${tag}...`);
try {
execSync(
`gh release download ${JSON.stringify(tag)} --repo ${JSON.stringify(repo)} --pattern ${JSON.stringify(asset)} --output ${JSON.stringify(tmpDest)} --clobber`,
{ stdio: "inherit" }
);
execFileSync("gh", [
"release",
"download",
tag,
"--repo",
repo,
"--pattern",
asset,
"--output",
tmpDest,
"--clobber",
], { stdio: "inherit" });
} catch (err) {
if (existsSync(tmpDest)) unlinkSync(tmpDest);
const reason = err instanceof Error