Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions .github/pr_run_ios_contacts_trails.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ echo "Installing TypeScript SDK devDependencies (esbuild)..."
(cd sdks/typescript && bun install --frozen-lockfile) \
|| { echo "ERROR: bun install failed in sdks/typescript"; TEST_FAILED=true; }

# Export config dir before the first Gradle invocation so the Gradle daemon
# starts with it in its environment. JavaExec subprocesses inherit the daemon's
# environment, not the caller's shell, so the export must precede the daemon's
# first start (triggered by :trailblaze-desktop:jar below).
export TRAILBLAZE_CONFIG_DIR="$(pwd)/examples/ios-contacts/trails/config"
echo "TRAILBLAZE_CONFIG_DIR=$TRAILBLAZE_CONFIG_DIR"

# Pre-compile the Trailblaze desktop module so the daemon starts within the
# 110s port-ready window below.
if [ "$TEST_FAILED" != "true" ]; then
Expand Down Expand Up @@ -75,6 +82,10 @@ if [ "$TEST_FAILED" != "true" ]; then
./trailblaze trail -d ios \
trails/ios-contacts/test-search-by-first-name/ios-iphone.trail.yaml \
|| TEST_FAILED=true

./trailblaze trail -d ios \
trails/ios-contacts/test-search-no-results/ios-iphone.trail.yaml \
|| TEST_FAILED=true
else
echo "Skipping test execution because setup failed"
fi
Expand Down
18 changes: 18 additions & 0 deletions .github/pr_run_wikipedia_trails.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ echo "Installing TypeScript SDK devDependencies (esbuild)..."
(cd sdks/typescript && bun install --frozen-lockfile) \
|| { echo "ERROR: bun install failed in sdks/typescript"; TEST_FAILED=true; }

# Export config dir before the first Gradle invocation so the Gradle daemon
# starts with it in its environment. JavaExec subprocesses inherit the daemon's
# environment, not the caller's shell, so the export must precede the daemon's
# first start (triggered by :trailblaze-desktop:jar below).
export TRAILBLAZE_CONFIG_DIR="$(pwd)/examples/wikipedia/trails/config"
echo "TRAILBLAZE_CONFIG_DIR=$TRAILBLAZE_CONFIG_DIR"

# Pre-compile the Trailblaze desktop module so the daemon starts within the
# 110s port-ready window below. Without this, `./trailblaze app …` does a cold
# Kotlin compile (4+ min on CI) inside the backgrounded process, the wait loop
Expand All @@ -33,6 +40,17 @@ if [ "$TEST_FAILED" != "true" ]; then
./gradlew :trailblaze-desktop:jar || { echo "ERROR: Failed to build Trailblaze desktop"; TEST_FAILED=true; }
fi

# Pre-install Playwright Chromium so the browser is cached before the trail
# runs. The JVM Playwright library (version 1.50.0) and the npm package use the
# same ~/.cache/ms-playwright browser cache, so pre-installing via bunx avoids
# the download happening inside DaemonClient's hardcoded 1800s poll window.
if [ "$TEST_FAILED" != "true" ]; then
echo "Pre-installing Playwright Chromium..."
bunx playwright@1.59.0 install chromium \
&& echo "✓ Playwright Chromium pre-installed" \
|| echo "WARNING: Playwright pre-install failed — download will happen during trail execution"
fi

if [ "$TEST_FAILED" != "true" ]; then
# Start the Trailblaze daemon in the background. `app --foreground --headless`
# blocks the process, so we background it with `&` and poll /ping until ready.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ios-contacts-trails.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
# Xcode 16 which includes iOS 18 simulator runtimes.
ios-contacts-trails:
runs-on: macos-15
timeout-minutes: 45
timeout-minutes: 60
env:
# Recordings-only — LLM is never invoked. A non-empty sentinel value is
# required to satisfy the provider/client wiring.
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/wikipedia-trails.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
# pre-recorded tool sequences.
wikipedia-trails:
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 60
env:
# Recordings-only — LLM is never invoked. A non-empty sentinel value is
# required to satisfy the provider/client wiring (see android-tests-host-rpc
Expand Down Expand Up @@ -52,7 +52,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y \
libasound2 \
libasound2t64 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libcups2 \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,14 @@ object TrailblazeWorkspaceConfigResolver {
if (envOverride != null) {
val envDir = File(envOverride)
if (envDir.isDirectory) {
// When the configDir has its own trailblaze.yaml, prefer it as the config file so
// pack targets declared there (e.g. ios-contacts/packs/contacts) are loaded instead
// of the workspace-root config's targets (which may point to a different pack of the
// same id with different tools). Falls back to the walk-up configFile when absent.
val envConfigFile = File(envDir, TrailblazeConfigPaths.CONFIG_FILENAME).takeIf { it.isFile }
return ResolvedTrailblazeWorkspaceConfig(
workspaceRoot = workspaceRoot,
configFile = configFile,
configFile = envConfigFile ?: configFile,
configDir = envDir,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,8 +490,8 @@ class DaemonClient(
/** Poll interval when waiting for daemon */
const val POLL_INTERVAL_MS = 500L

/** Overall timeout for polling a run to completion (30 minutes) */
const val RUN_POLL_TIMEOUT_MS = 30 * 60 * 1000L
/** Overall timeout for polling a run to completion (60 minutes) */
const val RUN_POLL_TIMEOUT_MS = 60 * 60 * 1000L

/**
* Max consecutive poll errors before falling back to a /ping health check.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,18 @@ class DaemonScriptedToolBundler(
appendLine(" return result;")
appendLine(" },")
appendLine("};")
// Attach a `tools` Proxy so scripted tools that use the `client.tools.<name>(args)`
// authoring surface (the SDK's TrailblazeClient.tools property) work correctly at
// runtime. Without this, `client.tools` is undefined and any `client.tools.X()`
// call throws "cannot read property 'X' of undefined". The Proxy maps any string
// property access to a callable that delegates to `__client.callTool(prop, args)`,
// mirroring the SDK's own `createToolsProxy` implementation.
appendLine("__client.tools = new Proxy({}, {")
appendLine(" get: function(target, prop) {")
appendLine(" if (typeof prop !== \"string\") return undefined;")
appendLine(" return function(args) { return __client.callTool(prop, args); };")
appendLine(" }")
appendLine("});")
appendLine()
// Normalize user return values into the `{content: [...]}` envelope `QuickJsToolHost`
// parses. Authors typically `return "Launched X."` from their handlers; the host
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,24 @@ class DaemonScriptedToolBundlerTest {
)
}

@Test
fun `synthesized wrapper exposes client_tools proxy for client_tools_X authoring surface`() {
// Pins the fix for the runtime failure "cannot read property 'X' of undefined"
// that occurs when a scripted tool uses `client.tools.launchApp(...)` instead of
// `client.callTool("launchApp", ...)`. The wrapper's `__client` shim must expose
// a `tools` Proxy that maps property accesses to `callTool` dispatches.
val src = writeTinyTs("tools-proxy-source.ts", exportName = "doSomething")
val wrapper = bundler.synthesizeWrapper(src, toolName = "doSomething")
assertTrue(
wrapper.contains("__client.tools = new Proxy"),
"expected wrapper to attach client.tools Proxy; got:\n$wrapper",
)
assertTrue(
wrapper.contains("return function(args) { return __client.callTool(prop, args); }"),
"expected tools Proxy to delegate to __client.callTool; got:\n$wrapper",
)
}

// --- helpers ---

private fun writeTinyTs(
Expand Down
19 changes: 2 additions & 17 deletions trails/ios-contacts/test-search-no-results/ios-iphone.trail.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,5 @@
- contacts_ios_searchContacts:
query: ZzZzNoSuchContact
openFirstResult: false
- tapOnElementBySelector:
reason: To search for 'ZzZzNoSuchContact', I need to tap the 'Search' field (q410), which is visible at the bottom of the screen.
selector:
textRegex: Search
nodeSelector:
iosMaestro:
hintTextRegex: Search
- inputText:
text: ZzZzNoSuchContact
reasoning: Now that the search field is focused, I will input the search term 'ZzZzNoSuchContact' into the search field to trigger the search and look for 'No Results'.
- assertVisibleBySelector:
reason: The screen displays 'No Results for “ZzZzNoSuchContact”', which verifies 'No Results' is visible as required by the objective.
selector:
textRegex: No Results for “ZzZzNoSuchContact”
nodeSelector:
iosMaestro:
accessibilityTextRegex: No Results for “ZzZzNoSuchContact”
- assertVisibleWithAccessibilityText:
accessibilityText: "No Results"
Loading