In-Browser Recursive Subagent Node Execution and Collapsible Nested Run Logs - #17
In-Browser Recursive Subagent Node Execution and Collapsible Nested Run Logs#17Jacobcdsmith wants to merge 1 commit into
Conversation
…ted logs - Update runFlow.ts to dynamically resolve subagent graph parameters from the Workflows and Templates Library and execute them recursively. - Inject nested execution logs (subLogs) and return final sub-workflow state output to the parent state.last_output. - Enhance Inspector.tsx to render a clean, reactive select dropdown populated with templates and custom workflows for subagent nodes, excluding the active workflow to prevent immediate self-recursion. - Enhance the Run Drawer UI in Index.tsx to display indented, collapsible step-by-step subagent execution paths with individual nested state snapshots. - Update codegen.ts for Python and JavaScript to generate informative, self-documenting comments mapping sub-workflow names, node counts, and edge counts. - Add comprehensive Vitest unit tests in subagentExecution.test.ts verifying complete correctness of execution, inputs/outputs, error fallback, and log nesting.
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
📝 WalkthroughWalkthroughSubagent nodes can select saved or template workflows, execute them recursively, and return nested logs. Generated code includes resolved workflow metadata. The Inspector excludes the active workflow, and the run-log UI displays nested execution details. ChangesSubagent workflow execution
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant ParentFlow
participant SubagentNode
participant ChildWorkflow
participant RunLog
ParentFlow->>SubagentNode: execute configured subagent
SubagentNode->>ChildWorkflow: resolve and run with parsed input state
ChildWorkflow-->>SubagentNode: return output and nested logs
SubagentNode-->>ParentFlow: return output and subLogs
ParentFlow->>RunLog: render parent and child execution details
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
frontend/src/test/subagentExecution.test.ts (1)
1-229: 🩺 Stability & Availability | 🔵 TrivialThe coverage here is solid for the fallback and recursive-execution happy paths, and matches
runNode'ssubagentcase infrontend/src/flow/runFlow.ts. Once a cycle/depth guard is added there (see the critical comment on that file), add a test with two saved workflows that delegate to each other to confirm the guard throws a controlled error instead of recursing indefinitely.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/test/subagentExecution.test.ts` around lines 1 - 229, Add a test in the subagent execution suite covering cyclic delegation: save two workflows whose subagent nodes reference each other, execute the first through runNode or runFlow, and assert the depth/cycle guard raises a controlled error rather than recursing indefinitely. Keep the existing fallback and successful nested-execution tests unchanged.frontend/src/flow/Inspector.tsx (1)
50-78: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winHoist the workflow list computation out of the field loop.
allWorkflowsandsubagentWorkflowOptionsare recomputed on every iteration ofmeta.configFields.map(...), for every node, on every render.loadWorkflows()reads and parseslocalStorageon each call. This work repeats even for non-subagent nodes and non-graphfields, and repeats on every keystroke in unrelated inputs (for example, thenamefield), since anyonChangetriggers a re-render ofInspector.Compute this once per render, guarded by
node.data.kind === "subagent", and memoize it.♻️ Proposed refactor
+import { useMemo, useState } from "react"; ... export function Inspector({ node, edges, nodes, gateways = [], activeWorkflowId, onChange, onDelete }: Props) { const [confirming, setConfirming] = useState(false); + const subagentWorkflowOptions = useMemo(() => { + if (!node || node.data.kind !== "subagent") return []; + return [...TEMPLATES, ...loadWorkflows()].filter((w) => w.id !== activeWorkflowId); + }, [node, activeWorkflowId]); if (!node) { return ( ... ); } ... {meta.configFields.map((f) => { const isSubagentGraph = node.data.kind === "subagent" && f.key === "graph"; - const allWorkflows = [...TEMPLATES, ...loadWorkflows()]; - const subagentWorkflowOptions = allWorkflows.filter((w) => w.id !== activeWorkflowId); return (Note:
useMemomust run unconditionally before theif (!node)early return, matching the existinguseStateplacement, to keep the hook order stable across renders.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/flow/Inspector.tsx` around lines 50 - 78, Hoist the workflow option computation out of the `meta.configFields.map` callback and memoize it with `useMemo`, guarded by `node?.data.kind === "subagent"` so `loadWorkflows()` runs only when needed and once per render inputs. Place this hook unconditionally before the `if (!node)` early return, matching the existing `useState` placement, then reuse its result for the `isSubagentGraph` select options.frontend/src/pages/Index.tsx (1)
2000-2071: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winNested log rendering handles only one level of subagent nesting.
This block only reads
(l.output as any).subLogs.runNode'ssubagentcase infrontend/src/flow/runFlow.tsreturnssubLogs, and each of those sub-log entries can itself carryoutput.subLogsif the child workflow contains its own subagent node. Those deeper levels have no expand affordance here, so a subagent-of-subagent chain silently loses visibility past the first level.Separately,
handleLogsExpandAll/handleLogsCollapseAllonly patch keys from the top-levelrunLogsarray. They don't set thesub-${uniqueKey}toggle or any nestedsubUniqueKeystate-snapshot keys added here, so "Expand Snapshots" won't reveal subagent state or force-open the nested log view.Consider extracting a recursive log-entry renderer (parameterized by depth/key-prefix) instead of duplicating the JSX for one hardcoded nesting level, and extend the expand/collapse handlers to walk into
subLogsrecursively.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/pages/Index.tsx` around lines 2000 - 2071, The nested log UI around the `subLogs` rendering only supports one level and the expand-all/collapse-all handlers omit nested keys. Extract or implement a recursive renderer for `RunLog` entries that discovers `output.subLogs` at every depth, generates unique depth-aware keys, and preserves state snapshot toggles; update `handleLogsExpandAll` and `handleLogsCollapseAll` to recursively traverse all nested `subLogs` and set the corresponding log, sub-log, and snapshot expansion keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/flow/runFlow.ts`:
- Around line 344-356: Forward the parent run’s stepDelay option in the
recursive runFlow call within the subworkflow execution path. Add opts.stepDelay
alongside the other propagated execution options so nested workflows preserve
the selected visualized stepping delay.
- Around line 315-366: Update RunOptions and the recursive delegation logic in
runFlow to track workflow IDs in the current delegation chain. Before recursing
for subWorkflow, detect whether its ID is already present and return the
existing simulated/fallback result instead of calling runFlow again; otherwise
pass a copied chain containing the subWorkflow ID into the recursive call.
Initialize the chain for top-level runs and preserve normal delegation for
workflows not yet visited.
---
Nitpick comments:
In `@frontend/src/flow/Inspector.tsx`:
- Around line 50-78: Hoist the workflow option computation out of the
`meta.configFields.map` callback and memoize it with `useMemo`, guarded by
`node?.data.kind === "subagent"` so `loadWorkflows()` runs only when needed and
once per render inputs. Place this hook unconditionally before the `if (!node)`
early return, matching the existing `useState` placement, then reuse its result
for the `isSubagentGraph` select options.
In `@frontend/src/pages/Index.tsx`:
- Around line 2000-2071: The nested log UI around the `subLogs` rendering only
supports one level and the expand-all/collapse-all handlers omit nested keys.
Extract or implement a recursive renderer for `RunLog` entries that discovers
`output.subLogs` at every depth, generates unique depth-aware keys, and
preserves state snapshot toggles; update `handleLogsExpandAll` and
`handleLogsCollapseAll` to recursively traverse all nested `subLogs` and set the
corresponding log, sub-log, and snapshot expansion keys.
In `@frontend/src/test/subagentExecution.test.ts`:
- Around line 1-229: Add a test in the subagent execution suite covering cyclic
delegation: save two workflows whose subagent nodes reference each other,
execute the first through runNode or runFlow, and assert the depth/cycle guard
raises a controlled error rather than recursing indefinitely. Keep the existing
fallback and successful nested-execution tests unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 2a408f6e-d417-42fc-9c44-b849f5d92078
📒 Files selected for processing (5)
frontend/src/flow/Inspector.tsxfrontend/src/flow/codegen.tsfrontend/src/flow/runFlow.tsfrontend/src/pages/Index.tsxfrontend/src/test/subagentExecution.test.ts
| const allWorkflows = [...TEMPLATES, ...loadWorkflows()]; | ||
| const subWorkflow = allWorkflows.find(w => w.id === cfg.graph || w.name === cfg.graph); | ||
|
|
||
| if (!subWorkflow) { | ||
| return { | ||
| simulated: true, | ||
| subagent: cfg.graph || "unknown", | ||
| input: interpolate(cfg.input || "", state, globalsList, secretsList), | ||
| note: "subagent workflow not found, fallback to simulated", | ||
| }; | ||
| } | ||
|
|
||
| const rawInput = cfg.input || ""; | ||
| const interpolatedInput = interpolate(rawInput, state, globalsList, secretsList); | ||
|
|
||
| let subInitialState: Record<string, unknown> = { query: interpolatedInput }; | ||
| try { | ||
| const trimmed = interpolatedInput.trim(); | ||
| if (trimmed.startsWith("{") || trimmed.startsWith("[")) { | ||
| const parsed = JSON.parse(trimmed); | ||
| if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { | ||
| subInitialState = parsed as Record<string, unknown>; | ||
| } | ||
| } | ||
| } catch { | ||
| // Fallback to simple query string | ||
| } | ||
|
|
||
| const subLogs: RunLog[] = []; | ||
| const subResultLogs = await runFlow({ | ||
| nodes: subWorkflow.nodes, | ||
| edges: subWorkflow.edges, | ||
| gateways: opts.gateways, | ||
| initialState: subInitialState, | ||
| maxSteps: opts.maxSteps, | ||
| globals: globalsList, | ||
| secrets: secretsList, | ||
| onLog: (subLog) => { | ||
| subLogs.push(subLog); | ||
| }, | ||
| onHumanApproval: opts.onHumanApproval, | ||
| }); | ||
|
|
||
| const lastLogWithSnapshot = [...subResultLogs].reverse().find(l => l.stateSnapshot !== undefined); | ||
| const subFinalOutput = lastLogWithSnapshot?.stateSnapshot?.last_output ?? lastLogWithSnapshot?.output; | ||
|
|
||
| return { | ||
| simulated: true, | ||
| subagent: cfg.graph || "unknown", | ||
| input: interpolate(cfg.input || "", state, globalsList, secretsList), | ||
| note: "subagent execution is schematic", | ||
| subagent: subWorkflow.name, | ||
| subagentId: subWorkflow.id, | ||
| input: subInitialState, | ||
| output: subFinalOutput, | ||
| subLogs, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Add a cycle/depth guard before recursing into a subagent workflow.
RunOptions has no field to track which workflows are already on the current delegation chain, and the recursive call to runFlow (lines 344-356) has nothing that stops workflow A from delegating to workflow B, which delegates back to workflow A. Each runFlow call starts its own while loop and, for its own subagent nodes, recurses again with no bound other than the unrelated maxSteps (which only limits steps within a single flow, not the delegation depth across nested flows).
The Inspector's workflow selector (frontend/src/flow/Inspector.tsx lines 50-78) only excludes the currently active workflow id, so it blocks direct self-reference but not this indirect cycle: when editing workflow B, workflow A is still selectable even if A already delegates to B.
Left unguarded, a two-hop (or longer) delegation cycle causes unbounded recursion in the browser tab, exhausting the call stack and crashing or hanging the run.
Track the chain of workflow ids on RunOptions and reject re-entry into an already-visited workflow (or cap the delegation depth) before recursing.
🛡️ Proposed fix
export interface RunOptions {
nodes: Node<AgentNodeData>[];
edges: Edge[];
gateways: Gateway[];
initialState?: Record<string, unknown>;
maxSteps?: number;
onLog?: (log: RunLog) => void;
stepDelay?: number;
onHumanApproval?: (req: {
nodeId: string;
name: string;
prompt: string;
channel: string;
}) => Promise<string>;
globals?: { key: string; value: string }[];
secrets?: { key: string; value: string }[];
+ /** Internal: workflow ids already on the current subagent delegation chain, used to detect cycles. */
+ subagentChain?: string[];
} case "subagent": {
const allWorkflows = [...TEMPLATES, ...loadWorkflows()];
const subWorkflow = allWorkflows.find(w => w.id === cfg.graph || w.name === cfg.graph);
if (!subWorkflow) {
return {
simulated: true,
subagent: cfg.graph || "unknown",
input: interpolate(cfg.input || "", state, globalsList, secretsList),
note: "subagent workflow not found, fallback to simulated",
};
}
+ const chain = opts.subagentChain ?? [];
+ const MAX_SUBAGENT_DEPTH = 10;
+ if (chain.includes(subWorkflow.id) || chain.length >= MAX_SUBAGENT_DEPTH) {
+ throw new Error(
+ `Subagent delegation cycle or depth limit reached: ${[...chain, subWorkflow.id].join(" → ")}`
+ );
+ }
+
const rawInput = cfg.input || "";
const interpolatedInput = interpolate(rawInput, state, globalsList, secretsList);
...
const subResultLogs = await runFlow({
nodes: subWorkflow.nodes,
edges: subWorkflow.edges,
gateways: opts.gateways,
initialState: subInitialState,
maxSteps: opts.maxSteps,
+ subagentChain: [...chain, subWorkflow.id],
globals: globalsList,
secrets: secretsList,
onLog: (subLog) => {
subLogs.push(subLog);
},
onHumanApproval: opts.onHumanApproval,
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const allWorkflows = [...TEMPLATES, ...loadWorkflows()]; | |
| const subWorkflow = allWorkflows.find(w => w.id === cfg.graph || w.name === cfg.graph); | |
| if (!subWorkflow) { | |
| return { | |
| simulated: true, | |
| subagent: cfg.graph || "unknown", | |
| input: interpolate(cfg.input || "", state, globalsList, secretsList), | |
| note: "subagent workflow not found, fallback to simulated", | |
| }; | |
| } | |
| const rawInput = cfg.input || ""; | |
| const interpolatedInput = interpolate(rawInput, state, globalsList, secretsList); | |
| let subInitialState: Record<string, unknown> = { query: interpolatedInput }; | |
| try { | |
| const trimmed = interpolatedInput.trim(); | |
| if (trimmed.startsWith("{") || trimmed.startsWith("[")) { | |
| const parsed = JSON.parse(trimmed); | |
| if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { | |
| subInitialState = parsed as Record<string, unknown>; | |
| } | |
| } | |
| } catch { | |
| // Fallback to simple query string | |
| } | |
| const subLogs: RunLog[] = []; | |
| const subResultLogs = await runFlow({ | |
| nodes: subWorkflow.nodes, | |
| edges: subWorkflow.edges, | |
| gateways: opts.gateways, | |
| initialState: subInitialState, | |
| maxSteps: opts.maxSteps, | |
| globals: globalsList, | |
| secrets: secretsList, | |
| onLog: (subLog) => { | |
| subLogs.push(subLog); | |
| }, | |
| onHumanApproval: opts.onHumanApproval, | |
| }); | |
| const lastLogWithSnapshot = [...subResultLogs].reverse().find(l => l.stateSnapshot !== undefined); | |
| const subFinalOutput = lastLogWithSnapshot?.stateSnapshot?.last_output ?? lastLogWithSnapshot?.output; | |
| return { | |
| simulated: true, | |
| subagent: cfg.graph || "unknown", | |
| input: interpolate(cfg.input || "", state, globalsList, secretsList), | |
| note: "subagent execution is schematic", | |
| subagent: subWorkflow.name, | |
| subagentId: subWorkflow.id, | |
| input: subInitialState, | |
| output: subFinalOutput, | |
| subLogs, | |
| const allWorkflows = [...TEMPLATES, ...loadWorkflows()]; | |
| const subWorkflow = allWorkflows.find(w => w.id === cfg.graph || w.name === cfg.graph); | |
| if (!subWorkflow) { | |
| return { | |
| simulated: true, | |
| subagent: cfg.graph || "unknown", | |
| input: interpolate(cfg.input || "", state, globalsList, secretsList), | |
| note: "subagent workflow not found, fallback to simulated", | |
| }; | |
| } | |
| const chain = opts.subagentChain ?? []; | |
| const MAX_SUBAGENT_DEPTH = 10; | |
| if (chain.includes(subWorkflow.id) || chain.length >= MAX_SUBAGENT_DEPTH) { | |
| throw new Error( | |
| `Subagent delegation cycle or depth limit reached: ${[...chain, subWorkflow.id].join(" → ")}` | |
| ); | |
| } | |
| const rawInput = cfg.input || ""; | |
| const interpolatedInput = interpolate(rawInput, state, globalsList, secretsList); | |
| let subInitialState: Record<string, unknown> = { query: interpolatedInput }; | |
| try { | |
| const trimmed = interpolatedInput.trim(); | |
| if (trimmed.startsWith("{") || trimmed.startsWith("[")) { | |
| const parsed = JSON.parse(trimmed); | |
| if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { | |
| subInitialState = parsed as Record<string, unknown>; | |
| } | |
| } | |
| } catch { | |
| // Fallback to simple query string | |
| } | |
| const subLogs: RunLog[] = []; | |
| const subResultLogs = await runFlow({ | |
| nodes: subWorkflow.nodes, | |
| edges: subWorkflow.edges, | |
| gateways: opts.gateways, | |
| initialState: subInitialState, | |
| maxSteps: opts.maxSteps, | |
| subagentChain: [...chain, subWorkflow.id], | |
| globals: globalsList, | |
| secrets: secretsList, | |
| onLog: (subLog) => { | |
| subLogs.push(subLog); | |
| }, | |
| onHumanApproval: opts.onHumanApproval, | |
| }); | |
| const lastLogWithSnapshot = [...subResultLogs].reverse().find(l => l.stateSnapshot !== undefined); | |
| const subFinalOutput = lastLogWithSnapshot?.stateSnapshot?.last_output ?? lastLogWithSnapshot?.output; | |
| return { | |
| subagent: subWorkflow.name, | |
| subagentId: subWorkflow.id, | |
| input: subInitialState, | |
| output: subFinalOutput, | |
| subLogs, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/flow/runFlow.ts` around lines 315 - 366, Update RunOptions and
the recursive delegation logic in runFlow to track workflow IDs in the current
delegation chain. Before recursing for subWorkflow, detect whether its ID is
already present and return the existing simulated/fallback result instead of
calling runFlow again; otherwise pass a copied chain containing the subWorkflow
ID into the recursive call. Initialize the chain for top-level runs and preserve
normal delegation for workflows not yet visited.
| const subResultLogs = await runFlow({ | ||
| nodes: subWorkflow.nodes, | ||
| edges: subWorkflow.edges, | ||
| gateways: opts.gateways, | ||
| initialState: subInitialState, | ||
| maxSteps: opts.maxSteps, | ||
| globals: globalsList, | ||
| secrets: secretsList, | ||
| onLog: (subLog) => { | ||
| subLogs.push(subLog); | ||
| }, | ||
| onHumanApproval: opts.onHumanApproval, | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Forward stepDelay to the nested runFlow call.
The recursive runFlow call forwards gateways, maxSteps, globals, secrets, and onHumanApproval, but not stepDelay. When a user selects "Visualized (600ms)" for the parent run, the nested subagent workflow still executes at full speed, which is inconsistent with the parent's visualized stepping and with the newly added nested-log viewer's purpose of showing step-by-step subagent execution.
🔧 Proposed fix
const subResultLogs = await runFlow({
nodes: subWorkflow.nodes,
edges: subWorkflow.edges,
gateways: opts.gateways,
initialState: subInitialState,
maxSteps: opts.maxSteps,
+ stepDelay: opts.stepDelay,
globals: globalsList,
secrets: secretsList,
onLog: (subLog) => {
subLogs.push(subLog);
},
onHumanApproval: opts.onHumanApproval,
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const subResultLogs = await runFlow({ | |
| nodes: subWorkflow.nodes, | |
| edges: subWorkflow.edges, | |
| gateways: opts.gateways, | |
| initialState: subInitialState, | |
| maxSteps: opts.maxSteps, | |
| globals: globalsList, | |
| secrets: secretsList, | |
| onLog: (subLog) => { | |
| subLogs.push(subLog); | |
| }, | |
| onHumanApproval: opts.onHumanApproval, | |
| }); | |
| const subResultLogs = await runFlow({ | |
| nodes: subWorkflow.nodes, | |
| edges: subWorkflow.edges, | |
| gateways: opts.gateways, | |
| initialState: subInitialState, | |
| maxSteps: opts.maxSteps, | |
| stepDelay: opts.stepDelay, | |
| globals: globalsList, | |
| secrets: secretsList, | |
| onLog: (subLog) => { | |
| subLogs.push(subLog); | |
| }, | |
| onHumanApproval: opts.onHumanApproval, | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/flow/runFlow.ts` around lines 344 - 356, Forward the parent
run’s stepDelay option in the recursive runFlow call within the subworkflow
execution path. Add opts.stepDelay alongside the other propagated execution
options so nested workflows preserve the selected visualized stepping delay.
Added fully functioning recursive Subagent node execution. In-browser runFlow now resolves and executes child workflows, returning final sink output to the parent flow and collecting sub-steps. Inspector sidebar renders a dropdown select for other workflows/templates. Run Drawer renders collapsible, indented logs of the child sub-steps. Generated Python/JS code includes descriptive comments of subagents. Includes full unit tests.
PR created automatically by Jules for task 6836238995967300608 started by @Jacobcdsmith
Summary by CodeRabbit
New Features
Bug Fixes
Tests