Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f59bad9
Revamping the lifecycle hooks, starting with init
ericallam Mar 21, 2025
0215a8c
vibes
ericallam Mar 21, 2025
2c44e59
init.ts at the root of the trigger dir is now automatically loaded
ericallam Mar 21, 2025
17e1623
Improve init lifecycle hook types and fix tabler icons on spans
ericallam Mar 21, 2025
1777ff4
move onStart to the new lifecycle hook system
ericallam Mar 21, 2025
be676fd
onFailure
ericallam Mar 22, 2025
06f6a04
onStart
ericallam Mar 22, 2025
e816ba4
onComplete
ericallam Mar 22, 2025
aaf2ed8
onWait and onResume
ericallam Mar 22, 2025
e77e8d4
handleError
ericallam Mar 22, 2025
597b7ba
adding imports
ericallam Mar 22, 2025
e34e520
more hooks
ericallam Mar 23, 2025
dc6e659
new locals API
ericallam Mar 24, 2025
6001dfa
Add middleware types
ericallam Mar 24, 2025
2d16d66
Add middleware hooks
ericallam Mar 24, 2025
2e6d0d8
share the hook registration code
ericallam Mar 24, 2025
1167889
use new onStart
ericallam Mar 24, 2025
b974d82
use new onFailure
ericallam Mar 24, 2025
307309f
implement onComplete
ericallam Mar 24, 2025
4a8d693
a couple tweaks
ericallam Mar 24, 2025
ef871b0
starting test executor
ericallam Mar 24, 2025
2262a42
more tests and fixes
ericallam Mar 24, 2025
91225da
test on failure
ericallam Mar 24, 2025
87824a8
dry up some stuff
ericallam Mar 24, 2025
f84ddcd
test oncomplete
ericallam Mar 24, 2025
6ab74c4
implement and test handleError (now catchError)
ericallam Mar 24, 2025
7f804f3
middleware working and tests passing
ericallam Mar 25, 2025
5edbb77
use tryCatch in TaskExecutor
ericallam Mar 25, 2025
6ba2b04
more tests
ericallam Mar 25, 2025
d0a5c16
Add cleanup hook
ericallam Mar 25, 2025
30705fd
implement cleanup
ericallam Mar 25, 2025
23ae77a
handle max duration timeout errors better
ericallam Mar 25, 2025
ba520fc
Make sure and register all the config hooks
ericallam Mar 25, 2025
df9cad6
Get it all working
ericallam Mar 25, 2025
3ca5985
Hooks now all use the new types, and adding some spans
ericallam Mar 25, 2025
268c9f6
implement onWait/onResume
ericallam Mar 25, 2025
887c5ca
Add changeset
ericallam Mar 25, 2025
e745fcb
Remove console.log
ericallam Mar 25, 2025
1def8d0
Use allSettled so onWait/onResume errors don't break anything
ericallam Mar 25, 2025
6d5d10a
Support other init file names
ericallam Mar 25, 2025
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
Prev Previous commit
Next Next commit
more tests
  • Loading branch information
ericallam committed Mar 25, 2025
commit 6ba2b0476b2014bc14adc16a80743d4dbabe23f5
4 changes: 2 additions & 2 deletions .cursor/rules/executing-commands.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ But often, when running tests, it's better to `cd` into the directory and then r

```
cd apps/webapp
pnpm run test
pnpm run test --run
```

This way you can run for a single file easily:

```
cd internal-packages/run-engine
pnpm run test ./src/engine/tests/ttl.test.ts
pnpm run test ./src/engine/tests/ttl.test.ts --run
```
10 changes: 2 additions & 8 deletions packages/cli-v3/src/entryPoints/dev-run-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ const zodIpc = new ZodIpcConnection({
tracer,
tracingSDK,
consoleInterceptor,
config,
retries: config.retries,
handleErrorFn,
});

Expand Down Expand Up @@ -406,13 +406,7 @@ const zodIpc = new ZodIpcConnection({
}
});

const { result } = await executor.execute(
execution,
metadata,
traceContext,
measurement,
signal
);
const { result } = await executor.execute(execution, metadata, traceContext, signal);

const usageSample = usage.stop(measurement);

Expand Down
25 changes: 18 additions & 7 deletions packages/core/src/v3/workers/taskExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,11 @@ export class TaskExecutor {
)
);

if (!hookError && result && typeof result === "object" && !Array.isArray(result)) {
if (hookError) {
throw hookError;
}

if (result && typeof result === "object" && !Array.isArray(result)) {
globalResults.push(result);
}
}
Expand Down Expand Up @@ -407,13 +411,12 @@ export class TaskExecutor {
)
);

if (hookError) {
throw hookError;
}

// Only merge if taskResult is an object
if (
!hookError &&
taskResult &&
typeof taskResult === "object" &&
!Array.isArray(taskResult)
) {
if (taskResult && typeof taskResult === "object" && !Array.isArray(taskResult)) {
return { ...mergedGlobalResults, ...taskResult };
}

Expand Down Expand Up @@ -645,6 +648,10 @@ export class TaskExecutor {
}
)
);

if (hookError) {
throw hookError;
}
}

if (taskStartHook) {
Expand All @@ -667,6 +674,10 @@ export class TaskExecutor {
}
)
);

if (hookError) {
throw hookError;
}
}
}
);
Expand Down
250 changes: 250 additions & 0 deletions packages/core/test/taskExecutor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,256 @@ describe("TaskExecutor", () => {
},
});
});

test("should propagate errors from init hooks", async () => {
const executionOrder: string[] = [];
const expectedError = new Error("Init hook error");

// Register global init hook that throws an error
lifecycleHooks.registerGlobalInitHook({
id: "failing-init",
fn: async () => {
executionOrder.push("global-init");
throw expectedError;
},
});

// Register task init hook that should never be called
lifecycleHooks.registerTaskInitHook("test-task", {
id: "task-init",
fn: async () => {
executionOrder.push("task-init");
return {
foo: "bar",
};
},
});

// Register failure hook to verify it's called
lifecycleHooks.registerGlobalFailureHook({
id: "global-failure",
fn: async ({ error }) => {
executionOrder.push("failure");
expect(error).toBe(expectedError);
},
});

// Register complete hook to verify it's called with error
lifecycleHooks.registerGlobalCompleteHook({
id: "global-complete",
fn: async ({ result }) => {
executionOrder.push("complete");
expect(result).toEqual({
ok: false,
error: expectedError,
});
},
});

const task = {
id: "test-task",
fns: {
run: async (payload: any, params: RunFnParams<any>) => {
executionOrder.push("run");
return {
output: "test-output",
};
},
},
};

const result = await executeTask(task, { test: "data" });

// Verify only the global init hook ran, and failure/complete hooks were called
expect(executionOrder).toEqual(["global-init", "failure", "complete"]);

// Verify the error result
expect(result).toEqual({
result: {
ok: false,
id: "test-run-id",
error: {
type: "BUILT_IN_ERROR",
message: "Init hook error",
name: "Error",
stackTrace: expect.any(String),
},
skippedRetrying: false,
},
});
});

test("should propagate errors from task init hooks", async () => {
const executionOrder: string[] = [];
const expectedError = new Error("Task init hook error");

// Register global init hook that succeeds
lifecycleHooks.registerGlobalInitHook({
id: "global-init",
fn: async () => {
executionOrder.push("global-init");
return {
foo: "bar",
};
},
});

// Register task init hook that throws an error
lifecycleHooks.registerTaskInitHook("test-task", {
id: "task-init",
fn: async () => {
executionOrder.push("task-init");
throw expectedError;
},
});

// Register failure hook to verify it's called
lifecycleHooks.registerGlobalFailureHook({
id: "global-failure",
fn: async ({ error, init }) => {
executionOrder.push("failure");
expect(error).toBe(expectedError);
// Verify we got the global init data
expect(init).toEqual({ foo: "bar" });
},
});

// Register complete hook to verify it's called with error
lifecycleHooks.registerGlobalCompleteHook({
id: "global-complete",
fn: async ({ result, init }) => {
executionOrder.push("complete");
expect(result).toEqual({
ok: false,
error: expectedError,
});
// Verify we got the global init data
expect(init).toEqual({ foo: "bar" });
},
});

const task = {
id: "test-task",
fns: {
run: async (payload: any, params: RunFnParams<any>) => {
executionOrder.push("run");
return {
output: "test-output",
};
},
},
};

const result = await executeTask(task, { test: "data" });

// Verify both init hooks ran, but run wasn't called, and failure/complete hooks were called
expect(executionOrder).toEqual(["global-init", "task-init", "failure", "complete"]);

// Verify the error result
expect(result).toEqual({
result: {
ok: false,
id: "test-run-id",
error: {
type: "BUILT_IN_ERROR",
message: "Task init hook error",
name: "Error",
stackTrace: expect.any(String),
},
skippedRetrying: false,
},
});
});

test("should propagate errors from start hooks", async () => {
const executionOrder: string[] = [];
const expectedError = new Error("Start hook error");

// Register global init hook that succeeds
lifecycleHooks.registerGlobalInitHook({
id: "global-init",
fn: async () => {
executionOrder.push("global-init");
return {
foo: "bar",
};
},
});

// Register global start hook that throws an error
lifecycleHooks.registerGlobalStartHook({
id: "global-start",
fn: async () => {
executionOrder.push("global-start");
throw expectedError;
},
});

// Register task start hook that should never be called
lifecycleHooks.registerTaskStartHook("test-task", {
id: "task-start",
fn: async () => {
executionOrder.push("task-start");
},
});

// Register failure hook to verify it's called
lifecycleHooks.registerGlobalFailureHook({
id: "global-failure",
fn: async ({ error, init }) => {
executionOrder.push("failure");
expect(error).toBe(expectedError);
// Verify we got the init data
expect(init).toEqual({ foo: "bar" });
},
});

// Register complete hook to verify it's called with error
lifecycleHooks.registerGlobalCompleteHook({
id: "global-complete",
fn: async ({ result, init }) => {
executionOrder.push("complete");
expect(result).toEqual({
ok: false,
error: expectedError,
});
// Verify we got the init data
expect(init).toEqual({ foo: "bar" });
},
});

const task = {
id: "test-task",
fns: {
run: async (payload: any, params: RunFnParams<any>) => {
executionOrder.push("run");
return {
output: "test-output",
};
},
},
};

const result = await executeTask(task, { test: "data" });

// Verify init succeeded, start hook failed, and run wasn't called
expect(executionOrder).toEqual(["global-init", "global-start", "failure", "complete"]);

// Verify the error result
expect(result).toEqual({
result: {
ok: false,
id: "test-run-id",
error: {
type: "BUILT_IN_ERROR",
message: "Start hook error",
name: "Error",
stackTrace: expect.any(String),
},
skippedRetrying: false,
},
});
});
});

function executeTask(task: TaskMetadataWithFunctions, payload: any) {
Expand Down