fix(ci): repair two pre-existing test failures on dev

1. apps/backend/src/lib/redirect-urls.test.tsx
   The withHostedHandlerEnv helper only set/cleared the STACK_*-prefixed
   env vars. CI's e2e-custom-base-port-api-tests workflow sets only the
   HEXCLAVE_*-prefixed sibling (NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX=67), and
   the dual-read shim in stack-shared/utils/env.tsx prefers HEXCLAVE_*
   over STACK_*. Result: getEnvVariable('NEXT_PUBLIC_STACK_PORT_PREFIX')
   returned '67' instead of the test's '92', breaking the assertion at
   line 75. Mirror every STACK_* key to its HEXCLAVE_* sibling so the
   dual-read resolves to the same value regardless of lookup order.

2. apps/backend/prisma/migrations/20260526060000_nullable_oauth_access_token_expires_at/tests/nullable-expires-at.ts
   Raw INSERT into OAuthAccessToken omitted the 'updatedAt' column.
   The Prisma model declares 'updatedAt DateTime @updatedAt' with no
   DB-level default; Prisma populates it at the ORM layer, but raw SQL
   bypasses that and hits the NOT NULL constraint. Set updatedAt = NOW()
   explicitly.
This commit is contained in:
Bilal Godil 2026-05-26 11:52:39 -07:00
parent c5a49d6721
commit 75c8e4343e
2 changed files with 34 additions and 22 deletions

View File

@ -55,6 +55,9 @@ export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof pre
expect(columnRows[0].is_nullable).toBe("YES");
expect(columnRows[0].data_type).toBe("timestamp without time zone");
// `OAuthAccessToken."updatedAt"` is `DateTime @updatedAt` in Prisma with no
// DB-level default — Prisma populates it at the ORM layer on insert. This
// raw SQL bypasses Prisma, so the column has to be set explicitly.
await sql`
INSERT INTO "OAuthAccessToken" (
"id",
@ -62,7 +65,8 @@ export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof pre
"oauthAccountId",
"accessToken",
"scopes",
"expiresAt"
"expiresAt",
"updatedAt"
)
VALUES (
${randomUUID()}::uuid,
@ -70,7 +74,8 @@ export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof pre
${ctx.oauthAccountId}::uuid,
'github-access-token-without-expiry',
ARRAY['user:email']::text[],
NULL
NULL,
NOW()
)
`;

View File

@ -12,34 +12,41 @@ describe('validateRedirectUrl', () => {
callback: () => T,
): T => {
const processEnv = Reflect.get(process, "env");
const oldHostedHandlerUrlTemplate = Reflect.get(processEnv, "NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE");
const oldHostedHandlerDomainSuffix = Reflect.get(processEnv, "NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX");
const oldStackPortPrefix = Reflect.get(processEnv, "NEXT_PUBLIC_STACK_PORT_PREFIX");
try {
for (const [key, value] of Object.entries({
NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE: values.hostedHandlerUrlTemplate,
NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX: values.hostedHandlerDomainSuffix,
NEXT_PUBLIC_STACK_PORT_PREFIX: values.stackPortPrefix,
})) {
// Hexclave rebrand: getEnvVariable() in stack-shared/utils/env.tsx prefers the
// HEXCLAVE_*-prefixed sibling of each STACK_* var. CI sets only the HEXCLAVE_*
// variant (e.g. NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX), so writing only the STACK_*
// key here would be silently overridden. Mirror every STACK_* key to its
// HEXCLAVE_* sibling so both representations resolve to the same value.
const stackKeys = [
"NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE",
"NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX",
"NEXT_PUBLIC_STACK_PORT_PREFIX",
] as const;
const hexclaveOf = (name: string) => name.replace("STACK_", "HEXCLAVE_");
const allKeys = [...stackKeys, ...stackKeys.map(hexclaveOf)];
const oldValues = Object.fromEntries(allKeys.map((k) => [k, Reflect.get(processEnv, k)]));
const newValues: Record<string, string | undefined> = {
NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE: values.hostedHandlerUrlTemplate,
NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX: values.hostedHandlerDomainSuffix,
NEXT_PUBLIC_STACK_PORT_PREFIX: values.stackPortPrefix,
};
for (const stackKey of stackKeys) {
newValues[hexclaveOf(stackKey)] = newValues[stackKey];
}
const applyValues = (entries: Record<string, string | undefined>) => {
for (const [key, value] of Object.entries(entries)) {
if (value == null) {
Reflect.deleteProperty(processEnv, key);
} else {
Reflect.set(processEnv, key, value);
}
}
};
try {
applyValues(newValues);
return callback();
} finally {
for (const [key, value] of Object.entries({
NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE: oldHostedHandlerUrlTemplate,
NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX: oldHostedHandlerDomainSuffix,
NEXT_PUBLIC_STACK_PORT_PREFIX: oldStackPortPrefix,
})) {
if (value == null) {
Reflect.deleteProperty(processEnv, key);
} else {
Reflect.set(processEnv, key, value);
}
}
applyValues(oldValues);
}
};