Final tests fix

This commit is contained in:
Konstantin Wohlwend 2025-09-03 15:56:20 -07:00
parent 656e738af9
commit 599f418da0
3 changed files with 57 additions and 49 deletions

View File

@ -139,17 +139,23 @@ async function compileWorkflow(tenancy: Tenancy, workflowId: string): Promise<Re
}
const workflow = tenancy.config.workflows.availableWorkflows[workflowId];
const res = await timeout(async () => {
console.log(`Compiling workflow ${workflowId}...`);
const compiledCodeResult = await compileWorkflowSource(workflow.tsSource);
if (compiledCodeResult.status === "error") {
return Result.error({ compileError: `Failed to compile workflow: ${compiledCodeResult.error}` });
}
console.log(`Compiled workflow source for ${workflowId}, running compilation trigger...`, { compiledCodeResult });
const compileTriggerResult = await triggerWorkflowRaw(tenancy, compiledCodeResult.data, {
type: "compile",
});
if (compileTriggerResult.status === "error") {
return Result.error({ compileError: `Failed to initialize workflow: ${compileTriggerResult.error}` });
}
console.log(`Compilation trigger result:`, { compileTriggerResult });
const compileTriggerOutputResult = compileTriggerResult.data;
if (typeof compileTriggerOutputResult !== "object" || !compileTriggerOutputResult || !("triggerOutput" in compileTriggerOutputResult)) {
captureError("workflows-compile-trigger-output", new StackAssertionError(`Failed to parse compile trigger output`, { compileTriggerOutputResult }));
@ -161,6 +167,8 @@ async function compileWorkflow(tenancy: Tenancy, workflowId: string): Promise<Re
return Result.error({ compileError: `Failed to parse compile trigger output, should be array of strings` });
}
console.log(`Workflow ${workflowId} compiled successfully, returning result...`, { registeredTriggers, compiledCodeResult });
return Result.ok({
compiledCode: compiledCodeResult.data,
registeredTriggers: registeredTriggers,
@ -369,46 +377,48 @@ async function compileAndGetEnabledWorkflows(tenancy: Tenancy): Promise<Map<stri
}
async function triggerWorkflowRaw(tenancy: Tenancy, compiledWorkflowCode: string, trigger: WorkflowTrigger): Promise<Result<unknown, string>> {
const workflowToken = generateSecureRandomString();
const workflowTriggerToken = await globalPrismaClient.workflowTriggerToken.create({
data: {
expiresAt: new Date(Date.now() + 1000 * 35),
tenancyId: tenancy.id,
tokenHash: await hashWorkflowTriggerToken(workflowToken),
},
});
const tokenRefreshInterval = setInterval(() => {
runAsynchronously(async () => {
await globalPrismaClient.workflowTriggerToken.update({
where: {
tenancyId_id: {
tenancyId: tenancy.id,
id: workflowTriggerToken.id,
},
},
data: { expiresAt: new Date(Date.now() + 1000 * 35) },
});
});
}, 10_000);
try {
const freestyle = new Freestyle();
const freestyleRes = await freestyle.executeScript(compiledWorkflowCode, {
envVars: {
STACK_WORKFLOW_TRIGGER_DATA: JSON.stringify(trigger),
NEXT_PUBLIC_STACK_PROJECT_ID: tenancy.project.id,
NEXT_PUBLIC_STACK_API_URL: getEnvVariable("NEXT_PUBLIC_STACK_API_URL").replace("http://localhost", "http://host.docker.internal"), // the replace is a hardcoded hack for the Freestyle mock server
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: "<placeholder publishable client key; the actual auth happens with the workflow token>",
STACK_SECRET_SERVER_KEY: "<placeholder secret server key; the actual auth happens with the workflow token>",
STACK_WORKFLOW_TOKEN_SECRET: workflowToken,
return await traceSpan({ description: `triggerWorkflowRaw ${trigger.type}` }, async () => {
const workflowToken = generateSecureRandomString();
const workflowTriggerToken = await globalPrismaClient.workflowTriggerToken.create({
data: {
expiresAt: new Date(Date.now() + 1000 * 35),
tenancyId: tenancy.id,
tokenHash: await hashWorkflowTriggerToken(workflowToken),
},
nodeModules: Object.fromEntries(Object.entries(externalPackages).map(([packageName, version]) => [packageName, version])),
});
return Result.map(freestyleRes, (data) => data.result);
} finally {
clearInterval(tokenRefreshInterval);
}
const tokenRefreshInterval = setInterval(() => {
runAsynchronously(async () => {
await globalPrismaClient.workflowTriggerToken.update({
where: {
tenancyId_id: {
tenancyId: tenancy.id,
id: workflowTriggerToken.id,
},
},
data: { expiresAt: new Date(Date.now() + 1000 * 35) },
});
});
}, 10_000);
try {
const freestyle = new Freestyle();
const freestyleRes = await freestyle.executeScript(compiledWorkflowCode, {
envVars: {
STACK_WORKFLOW_TRIGGER_DATA: JSON.stringify(trigger),
NEXT_PUBLIC_STACK_PROJECT_ID: tenancy.project.id,
NEXT_PUBLIC_STACK_API_URL: getEnvVariable("NEXT_PUBLIC_STACK_API_URL").replace("http://localhost", "http://host.docker.internal"), // the replace is a hardcoded hack for the Freestyle mock server
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: "<placeholder publishable client key; the actual auth happens with the workflow token>",
STACK_SECRET_SERVER_KEY: "<placeholder secret server key; the actual auth happens with the workflow token>",
STACK_WORKFLOW_TOKEN_SECRET: workflowToken,
},
nodeModules: Object.fromEntries(Object.entries(externalPackages).map(([packageName, version]) => [packageName, version])),
});
return Result.map(freestyleRes, (data) => data.result);
} finally {
clearInterval(tokenRefreshInterval);
}
});
}
async function createScheduledTrigger(tenancy: Tenancy, workflowId: string, trigger: WorkflowTrigger, scheduledAt: Date) {

View File

@ -218,20 +218,12 @@ export namespace Auth {
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 401,
"body": {
"code": "ADMIN_ACCESS_TOKEN_EXPIRED",
"details": { "expired_at_millis": 1756938402000 },
"error": "Admin access token has expired. Please refresh it and try again. (The access token expired at 2025-09-03T22:26:42.000Z.)",
},
"headers": Headers {
"x-stack-known-error": "ADMIN_ACCESS_TOKEN_EXPIRED",
<some fields may have been hidden>,
},
"status": 200,
"body": { "access_token": <stripped field 'access_token'> },
"headers": Headers { <some fields may have been hidden> },
}
`);
backendContext.set({ userAuth: { accessToken: response.body.access_token, refreshToken: response.body.refresh_token } });
await ensureParsableAccessToken();
return {
refreshAccessTokenResponse: response,
};

View File

@ -50,6 +50,7 @@ async function waitForServerMetadataNotNull(userId: string, key: string) {
test("onSignUp workflow sends email for client sign-up", async ({ expect }) => {
await Project.createAndSwitch();
await InternalApiKey.createAndSetProjectKeys();
const mailbox = await bumpEmailAddress({ unindexed: true });
const subject = `WF client signup ${crypto.randomUUID()}`;
@ -106,6 +107,7 @@ test("onSignUp workflow sends email for client sign-up", async ({ expect }) => {
test("onSignUp workflow can schedule callbacks", async ({ expect }) => {
await Project.createAndSwitch();
await InternalApiKey.createAndSetProjectKeys();
const mailbox = await bumpEmailAddress({ unindexed: true });
const subject = `WF client signup ${crypto.randomUUID()}`;
@ -204,6 +206,7 @@ test("onSignUp workflow sends email for server-created user", async ({ expect })
test("disabled workflows do not trigger", async ({ expect }) => {
await Project.createAndSwitch();
await InternalApiKey.createAndSetProjectKeys();
const mailbox = await bumpEmailAddress({ unindexed: true });
const subject = `WF disabled ${crypto.randomUUID()}`;
@ -237,6 +240,7 @@ test("disabled workflows do not trigger", async ({ expect }) => {
test("compile/runtime errors in one workflow don't block others", async ({ expect }) => {
await Project.createAndSwitch();
await InternalApiKey.createAndSetProjectKeys();
const mailbox = await bumpEmailAddress({ unindexed: true });
const subject = `WF ok ${crypto.randomUUID()}`;
@ -286,6 +290,7 @@ test("compile/runtime errors in one workflow don't block others", async ({ expec
test("anonymous sign-up does not trigger; upgrade triggers workflow", async ({ expect }) => {
await Project.createAndSwitch();
await InternalApiKey.createAndSetProjectKeys();
const markerKey = `wfMarker-${crypto.randomUUID()}`;
await Project.updateConfig({
@ -323,6 +328,7 @@ test("anonymous sign-up does not trigger; upgrade triggers workflow", async ({ e
test("workflow source changes take effect for subsequent sign-ups", async ({ expect }) => {
await Project.createAndSwitch();
await InternalApiKey.createAndSetProjectKeys();
const markerKey = `versionMarker-${crypto.randomUUID()}`;
// v1