Skip to content

feat: add timeout option to CLI trigger and improve error handling#1340

Open
sergiofilhowz wants to merge 2 commits intomainfrom
fix/iii-trigger-cli
Open

feat: add timeout option to CLI trigger and improve error handling#1340
sergiofilhowz wants to merge 2 commits intomainfrom
fix/iii-trigger-cli

Conversation

@sergiofilhowz
Copy link
Contributor

@sergiofilhowz sergiofilhowz commented Mar 21, 2026

Summary by CodeRabbit

  • New Features

    • Added --timeout-ms to the trigger command (defaults to 30s).
  • Improvements

    • Payload now defaults to an empty JSON object when omitted.
    • Clearer, structured error messages for timeouts, connection issues, and remote errors.
    • Successful trigger results are pretty-printed when present.
  • Tests

    • Updated tests to cover timeout behavior and adjusted failure expectations for unreachable endpoints.

@vercel
Copy link
Contributor

vercel bot commented Mar 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
iii-website Ready Ready Preview, Comment Mar 21, 2026 5:47pm
motia-docs Ready Ready Preview, Comment Mar 21, 2026 5:47pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 48faa57a-e60a-4d2d-805e-f954b34c17fe

📥 Commits

Reviewing files that changed from the base of the PR and between 7c47628 and ca979bc.

📒 Files selected for processing (1)
  • engine/src/cli_trigger.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • engine/src/cli_trigger.rs

📝 Walkthrough

Walkthrough

Refactored the CLI trigger to use the iii_sdk worker API instead of manual WebSocket handling; added a --timeout-ms argument (default 30_000), changed --payload to default to "{}", simplified control flow, and updated error handling and tests accordingly.

Changes

Cohort / File(s) Summary
CLI Trigger Migration
engine/src/cli_trigger.rs
Replaced manual WebSocket flow with iii_sdk worker API calls (trigger, shutdown_async). Added timeout_ms arg (default 30_000), changed payload default to "{}", removed build_invoke_message, added map_trigger_error, and adjusted result/error rendering.
CLI Tests / E2E
engine/src/main.rs, engine/tests/dlq_redrive_e2e.rs
Updated CLI parsing tests to expect timeout_ms default; removed payload-required assertion; e2e test now passes --timeout-ms 2000 and accepts timeout-related failure messages.

Sequence Diagram(s)

sequenceDiagram
    participant User as CLI User
    participant CLI as iii trigger\n(cli_trigger.rs)
    participant SDK as iii_sdk\nWorker API
    participant Engine as Engine

    rect rgba(100, 150, 255, 0.5)
      Note over User,Engine: Previous flow (manual WebSocket)
      User->>CLI: iii trigger --address --port --payload
      CLI->>Engine: WebSocket connect()
      CLI->>Engine: send("invokefunction", ...)
      Engine->>CLI: send("invocationresult", ...)
      CLI->>User: Print result or error
    end

    rect rgba(150, 200, 100, 0.5)
      Note over User,Engine: New flow (iii_sdk Worker API)
      User->>CLI: iii trigger --address --port --payload --timeout-ms
      CLI->>SDK: register_worker(address, port)
      SDK->>Engine: Connect via SDK
      CLI->>SDK: trigger(function_id, payload, timeout_ms)
      SDK->>Engine: Execute with timeout
      Engine-->>SDK: Result or IIIError
      SDK-->>CLI: Return value or error
      CLI->>User: Print JSON result or structured error (exit 1 on remote error)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 From sockets tangled, I leap to the SDK door,
Timeout set, payload snug, no bytes to score,
I tap the trigger, await the JSON light,
If errors shout, I show them neat and bright,
Hooray — a cleaner hop through code once more!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: adding a timeout option (--timeout-ms) to the CLI trigger command and improving error handling through the new map_trigger_error helper and refactored SDK integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/iii-trigger-cli

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
engine/src/main.rs (1)

129-145: Use this defaults test to cover the new --payload default too.

It still passes --payload "{}", so the parser behavior that changed in this PR—omitting --payload yields {}—isn't pinned anywhere in the clap tests.

🧪 Suggested change
     let cli = Cli::try_parse_from([
         "iii",
         "trigger",
         "--function-id",
         "test::fn",
-        "--payload",
-        "{}",
     ])
     .expect("should parse with defaults");

     match cli.command {
         Some(Commands::Trigger(args)) => {
+            assert_eq!(args.payload, "{}");
             assert_eq!(args.address, "localhost");
             assert_eq!(args.port, DEFAULT_PORT);
             assert_eq!(args.timeout_ms, 30_000);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@engine/src/main.rs` around lines 129 - 145, The test function
trigger_uses_defaults_for_address_and_port currently passes "--payload"
explicitly, so it doesn't verify the new default behavior; update the
Cli::try_parse_from invocation in trigger_uses_defaults_for_address_and_port to
omit the "--payload" and its "{}" value, then add an assertion on the parsed
Trigger args (Commands::Trigger -> args) that args.payload equals "{}" in
addition to the existing address, port and timeout checks so the parser's new
default payload is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@engine/src/cli_trigger.rs`:
- Around line 78-80: Update the IIIError::Timeout match arm in
engine/src/cli_trigger.rs so the error message does not assume the engine is
unreachable; instead produce a balanced message that mentions both possibilities
(no response from the engine at the address/port OR the invocation handler
exceeded the configured timeout) and, if the timeout_ms value is available in
the current context, include that value and suggest increasing it or checking
handler duration; modify the IIIError::Timeout arm to log this combined
diagnostic text rather than the current "Is the engine running at the given
address and port?" message.
- Around line 112-125: The test run_trigger_unreachable_engine_times_out assumes
an unopened localhost port will produce a timeout, but it often fails fast with
connection errors; update the test in run_trigger_unreachable_engine_times_out
to accept both timeout and connection/connect-related failures by relaxing the
assertion on result.unwrap_err() (e.g., check for "Timed out" or "timeout" OR
common connection error strings such as "connection refused", "Connection
refused", "connect", "failed to connect"), or alternatively change TriggerArgs
to use a blackholed address (e.g., 10.255.255.1) to reliably trigger a timeout;
modify the assertion in the test that currently references run_trigger and
TriggerArgs to reflect either accepted error patterns or the new address.

In `@engine/tests/dlq_redrive_e2e.rs`:
- Around line 623-629: The stderr check is too loose and case-sensitive: convert
stderr (the String from output.stderr) to lowercase and assert against specific
normalized phrases (e.g., "failed to connect", "connection refused", "timed
out", "timeout") instead of the generic "connect" token so spurious matches like
"is not connected" don't pass; update the assert that references
stderr.contains(...) to use the lowercased stderr variable and tighten the
substrings to those explicit failure messages.

---

Nitpick comments:
In `@engine/src/main.rs`:
- Around line 129-145: The test function
trigger_uses_defaults_for_address_and_port currently passes "--payload"
explicitly, so it doesn't verify the new default behavior; update the
Cli::try_parse_from invocation in trigger_uses_defaults_for_address_and_port to
omit the "--payload" and its "{}" value, then add an assertion on the parsed
Trigger args (Commands::Trigger -> args) that args.payload equals "{}" in
addition to the existing address, port and timeout checks so the parser's new
default payload is covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fa2740e8-3600-413b-a638-8d292fa1d527

📥 Commits

Reviewing files that changed from the base of the PR and between 0b1e8a1 and 7c47628.

📒 Files selected for processing (3)
  • engine/src/cli_trigger.rs
  • engine/src/main.rs
  • engine/tests/dlq_redrive_e2e.rs

Comment on lines +78 to +80
IIIError::Timeout => {
anyhow::anyhow!("Timed out waiting for the engine (no response within the timeout). Is the engine running at the given address and port?")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don't diagnose every timeout as an unreachable engine.

timeout_ms is documented on Lines 29-31 as the max wait for the invocation result, so this branch also covers slow handlers on a healthy engine. The current message sends users straight to address/port debugging even when the real problem is a long-running function.

💡 Suggested wording
         IIIError::Timeout => {
-            anyhow::anyhow!("Timed out waiting for the engine (no response within the timeout). Is the engine running at the given address and port?")
+            anyhow::anyhow!(
+                "Invocation timed out before a result was received. The engine may be unreachable, or the function may have exceeded the configured timeout."
+            )
         }
📝 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.

Suggested change
IIIError::Timeout => {
anyhow::anyhow!("Timed out waiting for the engine (no response within the timeout). Is the engine running at the given address and port?")
}
IIIError::Timeout => {
anyhow::anyhow!(
"Invocation timed out before a result was received. The engine may be unreachable, or the function may have exceeded the configured timeout."
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@engine/src/cli_trigger.rs` around lines 78 - 80, Update the IIIError::Timeout
match arm in engine/src/cli_trigger.rs so the error message does not assume the
engine is unreachable; instead produce a balanced message that mentions both
possibilities (no response from the engine at the address/port OR the invocation
handler exceeded the configured timeout) and, if the timeout_ms value is
available in the current context, include that value and suggest increasing it
or checking handler duration; modify the IIIError::Timeout arm to log this
combined diagnostic text rather than the current "Is the engine running at the
given address and port?" message.

Comment on lines +112 to +125
async fn run_trigger_unreachable_engine_times_out() {
let args = TriggerArgs {
function_id: "test::fn".to_string(),
payload: "{}".to_string(),
address: "localhost".to_string(),
port: 19999,
timeout_ms: 800,
};
let result = run_trigger(&args).await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("Failed to connect"),
"expected connection error, got: {}",
err.contains("Timed out") || err.contains("timeout"),
"expected timeout when engine is unreachable, got: {}",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

An unopened localhost port will not reliably surface as a timeout.

localhost:19999 usually fails fast with a WebSocket/connect error; a timeout is more typical for a blackholed address. The timeout-only assertion makes this test flaky across environments.

🧪 Suggested test hardening
-        let err = result.unwrap_err().to_string();
+        let err = result.unwrap_err().to_string().to_lowercase();
         assert!(
-            err.contains("Timed out") || err.contains("timeout"),
-            "expected timeout when engine is unreachable, got: {}",
+            err.contains("timed out")
+                || err.contains("timeout")
+                || err.contains("failed to connect")
+                || err.contains("connection refused")
+                || err.contains("websocket error"),
+            "expected a timeout or connect error when engine is unreachable, got: {}",
             err,
         );
📝 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.

Suggested change
async fn run_trigger_unreachable_engine_times_out() {
let args = TriggerArgs {
function_id: "test::fn".to_string(),
payload: "{}".to_string(),
address: "localhost".to_string(),
port: 19999,
timeout_ms: 800,
};
let result = run_trigger(&args).await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("Failed to connect"),
"expected connection error, got: {}",
err.contains("Timed out") || err.contains("timeout"),
"expected timeout when engine is unreachable, got: {}",
async fn run_trigger_unreachable_engine_times_out() {
let args = TriggerArgs {
function_id: "test::fn".to_string(),
payload: "{}".to_string(),
address: "localhost".to_string(),
port: 19999,
timeout_ms: 800,
};
let result = run_trigger(&args).await;
assert!(result.is_err());
let err = result.unwrap_err().to_string().to_lowercase();
assert!(
err.contains("timed out")
|| err.contains("timeout")
|| err.contains("failed to connect")
|| err.contains("connection refused")
|| err.contains("websocket error"),
"expected a timeout or connect error when engine is unreachable, got: {}",
err,
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@engine/src/cli_trigger.rs` around lines 112 - 125, The test
run_trigger_unreachable_engine_times_out assumes an unopened localhost port will
produce a timeout, but it often fails fast with connection errors; update the
test in run_trigger_unreachable_engine_times_out to accept both timeout and
connection/connect-related failures by relaxing the assertion on
result.unwrap_err() (e.g., check for "Timed out" or "timeout" OR common
connection error strings such as "connection refused", "Connection refused",
"connect", "failed to connect"), or alternatively change TriggerArgs to use a
blackholed address (e.g., 10.255.255.1) to reliably trigger a timeout; modify
the assertion in the test that currently references run_trigger and TriggerArgs
to reflect either accepted error patterns or the new address.

Comment on lines 623 to +629
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Failed to connect") || stderr.contains("connect"),
"Should mention connection failure, got: {}",
stderr.contains("Failed to connect")
|| stderr.contains("connect")
|| stderr.contains("Timed out")
|| stderr.contains("timeout"),
"Should mention connection failure or timeout, got: {}",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Normalize and tighten the stderr match here.

stderr.contains("connect") will also pass on iii is not connected, which hides a trigger-before-connect regression, and it still misses common Connection refused text because the match is case-sensitive.

🧪 Suggested assertion
-    let stderr = String::from_utf8_lossy(&output.stderr);
+    let stderr = String::from_utf8_lossy(&output.stderr).to_lowercase();
     assert!(
-        stderr.contains("Failed to connect")
-            || stderr.contains("connect")
-            || stderr.contains("Timed out")
+        stderr.contains("failed to connect")
+            || stderr.contains("connection refused")
+            || stderr.contains("timed out")
             || stderr.contains("timeout"),
         "Should mention connection failure or timeout, got: {}",
         stderr
     );
📝 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.

Suggested change
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Failed to connect") || stderr.contains("connect"),
"Should mention connection failure, got: {}",
stderr.contains("Failed to connect")
|| stderr.contains("connect")
|| stderr.contains("Timed out")
|| stderr.contains("timeout"),
"Should mention connection failure or timeout, got: {}",
let stderr = String::from_utf8_lossy(&output.stderr).to_lowercase();
assert!(
stderr.contains("failed to connect")
|| stderr.contains("connection refused")
|| stderr.contains("timed out")
|| stderr.contains("timeout"),
"Should mention connection failure or timeout, got: {}",
stderr
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@engine/tests/dlq_redrive_e2e.rs` around lines 623 - 629, The stderr check is
too loose and case-sensitive: convert stderr (the String from output.stderr) to
lowercase and assert against specific normalized phrases (e.g., "failed to
connect", "connection refused", "timed out", "timeout") instead of the generic
"connect" token so spurious matches like "is not connected" don't pass; update
the assert that references stderr.contains(...) to use the lowercased stderr
variable and tighten the substrings to those explicit failure messages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant