Skip to content

Commit 497b9d2

Browse files
committed
Fix local-emulator e2e test and address review feedback
- The positive happy-path test minted a project owned by the test user's team, but the CLI signs in as the local-emulator admin whose listOwnedProjects() only returns LOCAL_EMULATOR_OWNER_TEAM_ID-owned projects. Mint the project via /internal/local-emulator/project so it shows up under the admin's team. - Surface stderr when the positive test exits non-zero so future regressions report the real CLI error instead of a bare exit-code mismatch. - Add expect(createdProjectId).toBeDefined() guards to the two negative emulator tests for parity with the positive test. - Use performance.now() instead of Date.now() for the local-emulator sign-in retry deadline so wall-clock skew can't break the loop.
1 parent 6acd561 commit 497b9d2

2 files changed

Lines changed: 34 additions & 7 deletions

File tree

apps/e2e/tests/general/cli.test.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ describe("Stack CLI", () => {
323323
});
324324

325325
it("local-default exec errors when emulator PCK file is missing", async ({ expect }) => {
326+
expect(createdProjectId).toBeDefined();
326327
// Without --cloud, exec defaults to the local emulator. With
327328
// STACK_EMULATOR_HOME pointed at an empty dir, the PCK file lookup fires
328329
// before any network call and we get a clear error. Setting
@@ -346,6 +347,7 @@ describe("Stack CLI", () => {
346347
});
347348

348349
it("local-default exec errors when emulator API is unreachable", async ({ expect }) => {
350+
expect(createdProjectId).toBeDefined();
349351
// PCK file present (so we get past the file check) but STACK_EMULATOR_API_URL
350352
// points at a port nothing is listening on — fetch fails with a clear error.
351353
// STACK_EMULATOR_READY_TIMEOUT_MS=0 keeps the retry loop from waiting.
@@ -375,21 +377,46 @@ describe("Stack CLI", () => {
375377
// there). Stages a STACK_EMULATOR_HOME with the real internal PCK and
376378
// points STACK_EMULATOR_API_URL at the running backend, so the CLI takes
377379
// the local-default path and signs in as the emulator admin.
380+
//
381+
// The CLI signs in as the emulator admin, whose listOwnedProjects() only
382+
// returns projects owned by LOCAL_EMULATOR_OWNER_TEAM_ID. createdProjectId
383+
// is owned by the test user's team and would be invisible, so we mint a
384+
// fresh project via the local-emulator endpoint instead.
378385
it.runIf(isLocalEmulator)("local-default exec runs against the local emulator backend", async ({ expect }) => {
379-
expect(createdProjectId).toBeDefined();
386+
const emulatorConfigPath = path.join(tmpDir, `stack-emulator-${crypto.randomUUID()}.config.ts`);
387+
fs.writeFileSync(emulatorConfigPath, "");
388+
const projectRes = await niceFetch(`${STACK_BACKEND_BASE_URL}/api/v1/internal/local-emulator/project`, {
389+
method: "POST",
390+
headers: {
391+
"content-type": "application/json",
392+
"x-stack-access-type": "server",
393+
"x-stack-project-id": "internal",
394+
"x-stack-publishable-client-key": STACK_INTERNAL_PROJECT_CLIENT_KEY,
395+
"x-stack-secret-server-key": STACK_INTERNAL_PROJECT_SERVER_KEY,
396+
},
397+
body: JSON.stringify({ absolute_file_path: emulatorConfigPath }),
398+
});
399+
if (projectRes.status !== 200) {
400+
throw new Error(`Failed to mint local emulator project: ${projectRes.status} ${JSON.stringify(projectRes.body)}`);
401+
}
402+
const emulatorProjectId = (projectRes.body as { project_id: string }).project_id;
403+
380404
const fakeEmulatorHome = fs.mkdtempSync(path.join(os.tmpdir(), "stack-cli-emu-positive-"));
381405
try {
382406
const pckDir = path.join(fakeEmulatorHome, "run", "vm");
383407
fs.mkdirSync(pckDir, { recursive: true });
384408
fs.writeFileSync(path.join(pckDir, "internal-pck"), STACK_INTERNAL_PROJECT_CLIENT_KEY);
385-
const { stdout, exitCode } = await runCli(
409+
const { stdout, stderr, exitCode } = await runCli(
386410
["exec", "return 1+1"],
387411
{
388-
STACK_PROJECT_ID: createdProjectId,
412+
STACK_PROJECT_ID: emulatorProjectId,
389413
STACK_EMULATOR_HOME: fakeEmulatorHome,
390414
STACK_EMULATOR_API_URL: STACK_BACKEND_BASE_URL,
391415
},
392416
);
417+
if (exitCode !== 0) {
418+
throw new Error(`CLI exited ${exitCode}. stderr: ${stderr}`);
419+
}
393420
expect(exitCode).toBe(0);
394421
expect(stdout.trim()).toBe("2");
395422
} finally {

packages/stack-cli/src/lib/auth.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,25 +177,25 @@ async function attemptLocalEmulatorSignIn(apiUrl: string, internalPck: string, b
177177
}
178178

179179
async function localEmulatorSignInWithRetry(apiUrl: string, internalPck: string, body: SignInBody, totalTimeoutMs: number): Promise<Response> {
180-
const deadline = Date.now() + totalTimeoutMs;
180+
const deadline = performance.now() + totalTimeoutMs;
181181
let delay = 100;
182182
let lastError: unknown = null;
183183
while (true) {
184184
// Cap each request so the user-set total budget is actually honored — a
185185
// 5s default per-request would otherwise overshoot a small total.
186-
const remainingForRequest = Math.max(1, deadline - Date.now());
186+
const remainingForRequest = Math.max(1, deadline - performance.now());
187187
const perRequestTimeoutMs = Math.min(LOCAL_EMULATOR_PER_REQUEST_TIMEOUT_MS, remainingForRequest);
188188
try {
189189
return await attemptLocalEmulatorSignIn(apiUrl, internalPck, body, perRequestTimeoutMs);
190190
} catch (err) {
191191
if (!isRetryableFetchError(err)) throw err;
192192
lastError = err;
193193
}
194-
if (Date.now() >= deadline) {
194+
if (performance.now() >= deadline) {
195195
const message = lastError instanceof Error ? lastError.message : String(lastError);
196196
throw new AuthError(`Cannot reach local emulator at ${apiUrl} (after ${totalTimeoutMs}ms): ${message}. Start it with \`stack emulator start\`, or pass --cloud to use the cloud API.`);
197197
}
198-
const remaining = deadline - Date.now();
198+
const remaining = deadline - performance.now();
199199
await new Promise((r) => setTimeout(r, Math.min(delay, remaining)));
200200
delay = Math.min(delay * 2, 1_000);
201201
}

0 commit comments

Comments
 (0)