From 3fae8fc157fd2b40111486e9204b1def5b8ce6c7 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 16 Jun 2026 10:14:34 -0400 Subject: [PATCH] Upstream 2026.06.16 --- .trailblaze-sync | 2 +- build.gradle.kts | 4 + docs/CLI.md | 10 +- .../devlog/2026-03-06-trail-yaml-v2-syntax.md | 10 +- gradle/dependency-security.gradle.kts | 36 ++ .../references/session-logs-inspection.md | 2 +- .../trailblaze/agent/MultiAgentV3Runner.kt | 2 +- .../agent/MultiAgentV3TestAgentRunner.kt | 2 +- .../agent/trail/TrailGoalPlanner.kt | 2 +- .../android/AndroidTrailblazeRule.kt | 2 +- .../logs/client/TrailblazeSessionManager.kt | 2 +- .../xyz/block/trailblaze/util/StringUtils.kt | 2 +- .../block/trailblaze/yaml/TrailComparator.kt | 2 +- .../client/TrailblazeSessionManagerTest.kt | 6 +- .../yaml/TrailblazeRecordingGeneratorTest.kt | 8 +- .../dependencies/runtimeClasspath.txt | 6 +- .../dependencies/runtimeClasspath.txt | 6 +- .../block/trailblaze/cli/ResultsCommand.kt | 10 +- .../host/TrailblazeHostYamlRunner.kt | 2 +- .../host/rules/BaseHostTrailblazeTest.kt | 2 +- .../ui/recordings/RecordedTrailsRepoJvm.kt | 8 +- .../help-trailblaze-results-show.txt | 4 +- .../help-trailblaze-results.txt | 11 +- .../cli-output-baselines/help-trailblaze.txt | 6 +- .../block/trailblaze/logs/model/SessionId.kt | 2 +- .../trailblaze/recordings/TrailRecordings.kt | 4 +- .../trailblaze/logs/model/SessionIdTest.kt | 10 +- .../model/SessionStartedMemorySnapshotTest.kt | 2 +- .../report/models/TestResultCell.kt | 2 +- .../endpoints/GenerateReportEndpoint.kt | 2 +- .../mcp/TrailblazeMcpSessionContext.kt | 17 +- .../trailblaze/mcp/newtools/ConfigToolSet.kt | 15 - .../mcp/newtools/ToolSetManagementToolSet.kt | 204 ----------- .../mcp/toolsets/DynamicToolSetManager.kt | 335 ------------------ .../mcp/toolsets/ToolSetCategory.kt | 36 +- .../newtools/ToolSetManagementToolSetTest.kt | 233 ------------ .../toolsets/ToolSetCategoryMappingTest.kt | 2 +- .../ui/composables/PlatformIconUtils.kt | 2 +- .../ui/composables/PlatformRecordingsChips.kt | 2 +- .../trailblaze/ui/composables/PriorityChip.kt | 2 +- .../trailblaze/ui/composables/TestItemCard.kt | 2 +- .../xyz/block/trailblaze/ui/icons/TextIcon.kt | 4 +- .../trailblaze/ui/recordings/ExistingTrail.kt | 2 +- .../trailblaze/ui/tabs/trails/TrailCard.kt | 2 +- 44 files changed, 116 insertions(+), 911 deletions(-) create mode 100644 gradle/dependency-security.gradle.kts delete mode 100644 trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/newtools/ToolSetManagementToolSet.kt delete mode 100644 trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/toolsets/DynamicToolSetManager.kt delete mode 100644 trailblaze-server/src/test/kotlin/xyz/block/trailblaze/mcp/newtools/ToolSetManagementToolSetTest.kt diff --git a/.trailblaze-sync b/.trailblaze-sync index 0b22831d..8b723da3 100644 --- a/.trailblaze-sync +++ b/.trailblaze-sync @@ -1 +1 @@ -593c43b070fc9636da56b078c7beb062d44bc242 +1582142180765d5b1686d984deea619218ab4343 diff --git a/build.gradle.kts b/build.gradle.kts index 442171ec..7b09a480 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -54,6 +54,10 @@ subprojects { // Apply shared git version computation apply(from = "gradle/git-version.gradle.kts") +// Apply shared dependency-resolution security overrides (single source of truth, +// shared with the internal root build). +apply(from = "gradle/dependency-security.gradle.kts") + subprojects .forEach { it.plugins.withId("com.android.library") { diff --git a/docs/CLI.md b/docs/CLI.md index dea45f74..7b45b2f4 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -79,7 +79,7 @@ It does not reap device-scoped per-device sessions; use `app --stop` for those. | `session` | Manage the current device session — save it as a replayable trail, inspect steps, end it | | `report` | Generate an HTML report for session recordings, plus a best-effort JSON summary, and optionally MP4/GIF/WebP exports for a single session. JSON-only failures log a warning and still exit 0 — HTML is the primary artifact and is what gates the exit code. | | `waypoint` | Match named app locations (waypoints) against captured screen state. | -| `results` | Query the persisted test-result index for a TestRail case. Passing a positional `` (e.g. `trailblaze results C12345 --device android-phone`) is equivalent to the explicit `trailblaze results show ` form — picocli routes the bare case-id straight to the `show` subcommand. | +| `results` | Query the persisted test-result index for a test case. Passing a positional `` (e.g. `trailblaze results C12345 --device android-phone`) is equivalent to the explicit `trailblaze results show ` form — picocli routes the bare case-id straight to the `show` subcommand. | | `config` | View and set configuration (target app, device defaults, AI provider) | | `device` | List and connect devices (Android, iOS, Web) | | `show` | Open the multi-device live grid (/devices/all) in your default browser | @@ -997,7 +997,7 @@ trailblaze waypoint shortcut verify [OPTIONS] ### `trailblaze results` -Query the persisted test-result index for a TestRail case. Passing a positional `` (e.g. `trailblaze results C12345 --device android-phone`) is equivalent to the explicit `trailblaze results show ` form — picocli routes the bare case-id straight to the `show` subcommand. +Query the persisted test-result index for a test case. Passing a positional `` (e.g. `trailblaze results C12345 --device android-phone`) is equivalent to the explicit `trailblaze results show ` form — picocli routes the bare case-id straight to the `show` subcommand. **Synopsis:** @@ -1010,7 +1010,7 @@ trailblaze results show | Argument | Description | Required | |----------|-------------|----------| -| `<>` | TestRail case ID. When supplied without a subcommand, this routes to `show `. Case-insensitive; the leading `C` is required (e.g. C12345). | No | +| `<>` | Test-case ID. When supplied without a subcommand, this routes to `show `. Case-insensitive; the leading `C` is required (e.g. C12345). | No | **Options:** @@ -1028,7 +1028,7 @@ trailblaze results show ### `trailblaze results show` -Show the recorded result for a TestRail case ID +Show the recorded result for a test case ID **Synopsis:** @@ -1040,7 +1040,7 @@ trailblaze results show [OPTIONS] <> | Argument | Description | Required | |----------|-------------|----------| -| `<>` | TestRail case ID, e.g. C12345. Case-insensitive; the leading `C` is required. | Yes | +| `<>` | Test-case ID, e.g. C12345. Case-insensitive; the leading `C` is required. | Yes | **Options:** diff --git a/docs/devlog/2026-03-06-trail-yaml-v2-syntax.md b/docs/devlog/2026-03-06-trail-yaml-v2-syntax.md index 11fe35f2..97bc73a2 100644 --- a/docs/devlog/2026-03-06-trail-yaml-v2-syntax.md +++ b/docs/devlog/2026-03-06-trail-yaml-v2-syntax.md @@ -55,7 +55,7 @@ The `trailhead` is everything about the starting point: what this trail is, how ```yaml # ── Trailhead: identity, configuration, and setup ────────────────── trailhead: - id: testrail/suite_71172/section_838052/case_4837714 + id: regression/suite_71172/section_838052/case_4837714 title: Verify user cannot load more than $2,000 onto a Gift Card within 24 hours priority: P0 @@ -79,7 +79,7 @@ trailhead: metadata: caseId: "4837714" sectionId: "838052" - testRailUrl: https://testrail.example.com/index.php?/cases/view/12345 + sourceUrl: https://tracker.example.com/cases/view/12345 # Setup objectives (checkpoint for recording iteration) setup: @@ -375,7 +375,7 @@ This part of the original v2 design (CLI flows that walk authors through picking - Optional `--target= --platform=

` to narrow. - The implementation noun stays `tool` everywhere: `TrailblazeTool` (Kotlin class), `./trailblaze toolbox` (CLI), `./trailblaze tool ` (invocation), `tools:` (YAML key). The TOOLS section in the listing is the same word as the implementation type — the overload is harmless because section descriptions disambiguate the role. - Plain-text output for now; structured output (YAML / JSON) deferred until a concrete need surfaces. -- Specific funnel surfaces (interactive flows in `./trailblaze blaze`, scaffolding defaults, the on-demand pipeline's TestRail → trail generator) are NOT settled here. They will be designed against a working discovery surface first. +- Specific funnel surfaces (interactive flows in `./trailblaze blaze`, scaffolding defaults, the on-demand pipeline's external-test-case → trail generator) are NOT settled here. They will be designed against a working discovery surface first. The principle the funnel work will hold to: **make it easy to do things the right way.** Discovery comes first so authors can SEE what's available; funneling them through workflows is a follow-on once discovery is solid. @@ -412,7 +412,7 @@ trailhead: **NL-only `blaze.yaml` (recorder materializes `tools:` on first run):** ```yaml config: - id: testrail/suite_71172/section_946176/case_5552497 + id: regression/suite_71172/section_946176/case_5552497 target: myapp platform: android trailhead: @@ -439,7 +439,7 @@ Parses and runs. Soft warning at lint time; no failure. ### Open questions for follow-up devlogs -1. **Funnel design** — how the authoring surfaces (`./trailblaze blaze`, scaffolding, the TestRail generator) walk authors through trailhead selection. Discovery via `./trailblaze toolbox trailheads` is the foundation; the workflow design happens after that lands. +1. **Funnel design** — how the authoring surfaces (`./trailblaze blaze`, scaffolding, the test-case generator) walk authors through trailhead selection. Discovery via `./trailblaze toolbox trailheads` is the foundation; the workflow design happens after that lands. 2. **Default trailhead per target** — each registered target needs a designated default trailhead tool (e.g. `myapp` defaults to `myapp_launchAppSignedInWithAccount(key: defaults/standard-account)`). Lives in a registry of some kind; format TBD. 3. **`@LLMDescription` audit** — for the existing trailhead-tagged tool classes, ensure each description is decision-grade. Some are; others need a pass. 4. **Validation lint behavior** — `./trailblaze toolbox trailheads`-based lint warnings vs CI-gating. Default warn-only; opt-in `--strict` for teams that want PR-blocking. diff --git a/gradle/dependency-security.gradle.kts b/gradle/dependency-security.gradle.kts new file mode 100644 index 00000000..60148e14 --- /dev/null +++ b/gradle/dependency-security.gradle.kts @@ -0,0 +1,36 @@ +import org.gradle.api.artifacts.VersionCatalogsExtension + +// Single source of truth for dependency-resolution security overrides. +// +// Applied from BOTH root builds so the internal and open-source project +// configurations resolve identical versions: +// - internal root: apply(from = "opensource/gradle/dependency-security.gradle.kts") +// - open-source root: apply(from = "gradle/dependency-security.gradle.kts") +// +// Keeping the force here (in the mirrored opensource/ tree) rather than inline in a +// single root means the open-source mirror gets the same resolution the internal +// build does. Otherwise the dependency-guard baselines — generated against the +// internal resolution and then synced — fail in open-source CI because the mirror +// resolves the un-forced transitive version. + +val micrometerVersion = + extensions.getByType() + .named("libs") + .findVersion("micrometer") + .get() + .requiredVersion + +// micrometer is a transitive dependency of dev.mobile:maestro-utils, which pins the +// vulnerable 1.13.4; Trailblaze does not depend on it directly. Force the +// catalog-pinned version (CVE-2026-40984, CWE-770: Allocation of Resources Without +// Limits or Throttling) across every configuration in every subproject. +subprojects { + configurations.all { + resolutionStrategy.eachDependency { + if (requested.group == "io.micrometer") { + useVersion(micrometerVersion) + because("CVE-2026-40984: micrometer-core < 1.15.12 is vulnerable to resource exhaustion") + } + } + } +} diff --git a/skills/trailblaze/references/session-logs-inspection.md b/skills/trailblaze/references/session-logs-inspection.md index bd563bf9..715c8058 100644 --- a/skills/trailblaze/references/session-logs-inspection.md +++ b/skills/trailblaze/references/session-logs-inspection.md @@ -250,7 +250,7 @@ trailblaze-models/src/commonMain/kotlin/xyz/block/trailblaze/logs/client/Trailbl ``` When a new log type lands or fields change, that file is the source of truth — update this -reference and `scripts/testrail-build-analysis/analyze.py` accordingly. +reference accordingly. ## Related references diff --git a/trailblaze-agent/src/main/java/xyz/block/trailblaze/agent/MultiAgentV3Runner.kt b/trailblaze-agent/src/main/java/xyz/block/trailblaze/agent/MultiAgentV3Runner.kt index 69420a24..e9a779f1 100644 --- a/trailblaze-agent/src/main/java/xyz/block/trailblaze/agent/MultiAgentV3Runner.kt +++ b/trailblaze-agent/src/main/java/xyz/block/trailblaze/agent/MultiAgentV3Runner.kt @@ -262,7 +262,7 @@ class MultiAgentV3Runner private constructor( sessionId: SessionId = TrailblazeSessionManager.generateSessionId("trail"), initialActionHistory: List = emptyList(), /** - * The overall test case title (e.g. TestRail case name) that encompasses all steps. + * The overall test case title (e.g. an external test-case name) that encompasses all steps. * * Passed as [RecommendationContext.overallObjective] so the inner agent can reason * about each step in the context of the broader test goal and detect impossible diff --git a/trailblaze-agent/src/main/java/xyz/block/trailblaze/agent/MultiAgentV3TestAgentRunner.kt b/trailblaze-agent/src/main/java/xyz/block/trailblaze/agent/MultiAgentV3TestAgentRunner.kt index 7a103f59..402931b6 100644 --- a/trailblaze-agent/src/main/java/xyz/block/trailblaze/agent/MultiAgentV3TestAgentRunner.kt +++ b/trailblaze-agent/src/main/java/xyz/block/trailblaze/agent/MultiAgentV3TestAgentRunner.kt @@ -24,7 +24,7 @@ import xyz.block.trailblaze.yaml.PromptStep * @param v3Runner The underlying V3 runner * @param screenStateProvider Provider for current screen state * @param sessionIdProvider Provider for the current session ID - * @param caseTitleProvider Returns the current test case title (e.g. TestRail case name). + * @param caseTitleProvider Returns the current test case title (e.g. an external test-case name). * Invoked per step so the title can change between trails when the runner is reused. * The value is forwarded as [RecommendationContext.overallObjective] so the inner agent * can detect impossible steps early instead of exhausting all retries. diff --git a/trailblaze-agent/src/main/java/xyz/block/trailblaze/agent/trail/TrailGoalPlanner.kt b/trailblaze-agent/src/main/java/xyz/block/trailblaze/agent/trail/TrailGoalPlanner.kt index 91e9f1fd..442daa25 100644 --- a/trailblaze-agent/src/main/java/xyz/block/trailblaze/agent/trail/TrailGoalPlanner.kt +++ b/trailblaze-agent/src/main/java/xyz/block/trailblaze/agent/trail/TrailGoalPlanner.kt @@ -70,7 +70,7 @@ class TrailStepPlanner( private val availableToolsProvider: () -> List = { emptyList() }, private val initialActionHistory: List = emptyList(), /** - * The overall test case title (e.g. TestRail case name) that encompasses all steps. + * The overall test case title (e.g. an external test-case name) that encompasses all steps. * * When non-null, this is passed as [RecommendationContext.overallObjective] so the inner * agent can reason about each step in the context of the broader test goal. This enables diff --git a/trailblaze-android/src/main/java/xyz/block/trailblaze/android/AndroidTrailblazeRule.kt b/trailblaze-android/src/main/java/xyz/block/trailblaze/android/AndroidTrailblazeRule.kt index c89a2993..d9272b03 100644 --- a/trailblaze-android/src/main/java/xyz/block/trailblaze/android/AndroidTrailblazeRule.kt +++ b/trailblaze-android/src/main/java/xyz/block/trailblaze/android/AndroidTrailblazeRule.kt @@ -420,7 +420,7 @@ open class AndroidTrailblazeRule( private val agentImplementation: AgentImplementation = InstrumentationArgUtil.agentImplementation() /** - * Title of the trail currently executing (e.g. TestRail case name). + * Title of the trail currently executing (e.g. an external test-case name). * * Set at [runSuspend] entry before any step runs, then forwarded via * [caseTitleProvider] in the V3 runner so the inner agent receives the diff --git a/trailblaze-common/src/jvmAndAndroid/kotlin/xyz/block/trailblaze/logs/client/TrailblazeSessionManager.kt b/trailblaze-common/src/jvmAndAndroid/kotlin/xyz/block/trailblaze/logs/client/TrailblazeSessionManager.kt index 902983ee..e556dc99 100644 --- a/trailblaze-common/src/jvmAndAndroid/kotlin/xyz/block/trailblaze/logs/client/TrailblazeSessionManager.kt +++ b/trailblaze-common/src/jvmAndAndroid/kotlin/xyz/block/trailblaze/logs/client/TrailblazeSessionManager.kt @@ -294,7 +294,7 @@ class TrailblazeSessionManager( * * Format: `YYYY_MM_DD_HH_MM_SS__`, sanitized via [SessionId.sanitized]. * - * The full test name (including TestRail suite/section/case suffixes) is + * The full test name (including external suite/section/case suffixes) is * preserved so downstream tooling can reliably map session IDs back to test * identifiers. Both host-generated and externally-provided IDs flow through * [SessionId.sanitized] so a host-generated ID passed to the on-device handler diff --git a/trailblaze-common/src/jvmAndAndroid/kotlin/xyz/block/trailblaze/util/StringUtils.kt b/trailblaze-common/src/jvmAndAndroid/kotlin/xyz/block/trailblaze/util/StringUtils.kt index ca2458f6..41d67db0 100644 --- a/trailblaze-common/src/jvmAndAndroid/kotlin/xyz/block/trailblaze/util/StringUtils.kt +++ b/trailblaze-common/src/jvmAndAndroid/kotlin/xyz/block/trailblaze/util/StringUtils.kt @@ -29,7 +29,7 @@ fun toPascalCaseIdentifier(input: String, prefixIfStartsWithDigit: String = "C") * * Not for session IDs. For session-ID sanitization use * `SessionId.sanitized` — it is idempotent and preserves long suffixes - * (e.g., TestRail `__suite__section__case`), which this helper would + * (e.g., an external `__suite__section__case` identifier), which this helper would * collapse into a single underscore. */ fun toSnakeCaseIdentifier(input: String, prefixIfStartsWithDigit: String = "c_"): String { diff --git a/trailblaze-common/src/jvmAndAndroid/kotlin/xyz/block/trailblaze/yaml/TrailComparator.kt b/trailblaze-common/src/jvmAndAndroid/kotlin/xyz/block/trailblaze/yaml/TrailComparator.kt index 523d6e6c..80d78332 100644 --- a/trailblaze-common/src/jvmAndAndroid/kotlin/xyz/block/trailblaze/yaml/TrailComparator.kt +++ b/trailblaze-common/src/jvmAndAndroid/kotlin/xyz/block/trailblaze/yaml/TrailComparator.kt @@ -106,7 +106,7 @@ class TrailComparator { /** * Compare two trails and return a detailed comparison result. * - * @param expected The expected/reference trail items (e.g., from TestRail) + * @param expected The expected/reference trail items (e.g., from an external test-management system) * @param actual The actual trail items (e.g., from a recorded trail file) * @param compareConfig Whether to compare config blocks (default: true) * @return A [TrailComparisonResult] containing any differences found diff --git a/trailblaze-common/src/jvmAndAndroidTest/kotlin/xyz/block/trailblaze/logs/client/TrailblazeSessionManagerTest.kt b/trailblaze-common/src/jvmAndAndroidTest/kotlin/xyz/block/trailblaze/logs/client/TrailblazeSessionManagerTest.kt index 0576ded9..70133a77 100644 --- a/trailblaze-common/src/jvmAndAndroidTest/kotlin/xyz/block/trailblaze/logs/client/TrailblazeSessionManagerTest.kt +++ b/trailblaze-common/src/jvmAndAndroidTest/kotlin/xyz/block/trailblaze/logs/client/TrailblazeSessionManagerTest.kt @@ -15,7 +15,7 @@ class TrailblazeSessionManagerTest { val sessionId = generateSessionId(longSeed) assert(sessionId.value.contains("__suite_1__section_2__case_3")) { - "TestRail-style suffix must survive generateSessionId; got: ${sessionId.value}" + "Long suite/section/case suffix must survive generateSessionId; got: ${sessionId.value}" } assert(sessionId.value.length > 200) { "Expected un-truncated session id; got length ${sessionId.value.length}" @@ -23,9 +23,9 @@ class TrailblazeSessionManagerTest { } @Test - fun `createSessionWithId preserves long TestRail IDs without truncation`() { + fun `createSessionWithId preserves long external IDs without truncation`() { // Regression pin: the previous 100-char cap on externally-provided override - // IDs also dropped TestRail suffixes. Now both paths go through the single + // IDs also dropped long suite/section/case suffixes. Now both paths go through the single // SessionId.sanitized source of truth. val longOverride = SessionId( "2026_04_20_11_16_18_example_suite_long_test_name_" + diff --git a/trailblaze-common/src/jvmAndAndroidTest/kotlin/xyz/block/trailblaze/yaml/TrailblazeRecordingGeneratorTest.kt b/trailblaze-common/src/jvmAndAndroidTest/kotlin/xyz/block/trailblaze/yaml/TrailblazeRecordingGeneratorTest.kt index 736cb070..cf4c2c05 100644 --- a/trailblaze-common/src/jvmAndAndroidTest/kotlin/xyz/block/trailblaze/yaml/TrailblazeRecordingGeneratorTest.kt +++ b/trailblaze-common/src/jvmAndAndroidTest/kotlin/xyz/block/trailblaze/yaml/TrailblazeRecordingGeneratorTest.kt @@ -989,11 +989,11 @@ class TrailblazeRecordingGeneratorTest { @Test fun roundTripFullTrailFile() { val config = TrailConfig( - id = "testrail/suite_123/case_456", + id = "regression/suite_123/case_456", title = "User can log in", priority = "P0", context = "Account email: test@example.com", - metadata = mapOf("testRailCaseId" to "456"), + metadata = mapOf("caseId" to "456"), ) val promptsYaml = """ |- prompts: @@ -1045,11 +1045,11 @@ class TrailblazeRecordingGeneratorTest { assertThat(decoded.size).isEqualTo(3) // config + tools + prompts val decodedConfig = (decoded[0] as TrailYamlItem.ConfigTrailItem).config - assertThat(decodedConfig.id).isEqualTo("testrail/suite_123/case_456") + assertThat(decodedConfig.id).isEqualTo("regression/suite_123/case_456") assertThat(decodedConfig.title).isEqualTo("User can log in") assertThat(decodedConfig.priority).isEqualTo("P0") assertThat(decodedConfig.context).isEqualTo("Account email: test@example.com") - assertThat(decodedConfig.metadata).isEqualTo(mapOf("testRailCaseId" to "456")) + assertThat(decodedConfig.metadata).isEqualTo(mapOf("caseId" to "456")) val decodedTools = (decoded[1] as TrailYamlItem.ToolTrailItem).tools assertThat(decodedTools.size).isEqualTo(1) diff --git a/trailblaze-desktop/dependencies/runtimeClasspath.txt b/trailblaze-desktop/dependencies/runtimeClasspath.txt index f3a39836..e75d3855 100644 --- a/trailblaze-desktop/dependencies/runtimeClasspath.txt +++ b/trailblaze-desktop/dependencies/runtimeClasspath.txt @@ -255,9 +255,9 @@ io.ktor:ktor-websocket-serialization-jvm:3.3.3 io.ktor:ktor-websocket-serialization:3.3.3 io.ktor:ktor-websockets-jvm:3.3.3 io.ktor:ktor-websockets:3.3.3 -io.micrometer:micrometer-commons:1.13.4 -io.micrometer:micrometer-core:1.13.4 -io.micrometer:micrometer-observation:1.13.4 +io.micrometer:micrometer-commons:1.15.12 +io.micrometer:micrometer-core:1.15.12 +io.micrometer:micrometer-observation:1.15.12 io.modelcontextprotocol:kotlin-sdk-client-jvm:0.11.1 io.modelcontextprotocol:kotlin-sdk-client:0.11.1 io.modelcontextprotocol:kotlin-sdk-core-jvm:0.11.1 diff --git a/trailblaze-host/dependencies/runtimeClasspath.txt b/trailblaze-host/dependencies/runtimeClasspath.txt index f3a39836..e75d3855 100644 --- a/trailblaze-host/dependencies/runtimeClasspath.txt +++ b/trailblaze-host/dependencies/runtimeClasspath.txt @@ -255,9 +255,9 @@ io.ktor:ktor-websocket-serialization-jvm:3.3.3 io.ktor:ktor-websocket-serialization:3.3.3 io.ktor:ktor-websockets-jvm:3.3.3 io.ktor:ktor-websockets:3.3.3 -io.micrometer:micrometer-commons:1.13.4 -io.micrometer:micrometer-core:1.13.4 -io.micrometer:micrometer-observation:1.13.4 +io.micrometer:micrometer-commons:1.15.12 +io.micrometer:micrometer-core:1.15.12 +io.micrometer:micrometer-observation:1.15.12 io.modelcontextprotocol:kotlin-sdk-client-jvm:0.11.1 io.modelcontextprotocol:kotlin-sdk-client:0.11.1 io.modelcontextprotocol:kotlin-sdk-core-jvm:0.11.1 diff --git a/trailblaze-host/src/main/java/xyz/block/trailblaze/cli/ResultsCommand.kt b/trailblaze-host/src/main/java/xyz/block/trailblaze/cli/ResultsCommand.kt index cb4653a1..f519a73a 100644 --- a/trailblaze-host/src/main/java/xyz/block/trailblaze/cli/ResultsCommand.kt +++ b/trailblaze-host/src/main/java/xyz/block/trailblaze/cli/ResultsCommand.kt @@ -26,7 +26,7 @@ import java.util.concurrent.Callable /** * Query persisted test results from a flat-file index repo. * - * The index repo is a key-value store on GitHub: two JSON files per (TestRail case, device + * The index repo is a key-value store on GitHub: two JSON files per (test case, device * profile) cell at * `results/testrail///latest.json` (every terminal run) * `results/testrail///latest_success.json` (passing runs only) @@ -52,7 +52,7 @@ import java.util.concurrent.Callable name = "results", mixinStandardHelpOptions = true, description = [ - "Query the persisted test-result index for a TestRail case. " + + "Query the persisted test-result index for a test case. " + "Passing a positional `` (e.g. `trailblaze results C12345 --device android-phone`) " + "is equivalent to the explicit `trailblaze results show ` form — picocli routes " + "the bare case-id straight to the `show` subcommand.", @@ -76,7 +76,7 @@ class ResultsCommand : Callable { arity = "0..1", paramLabel = "", description = [ - "TestRail case ID. When supplied without a subcommand, this routes to `show `. " + + "Test-case ID. When supplied without a subcommand, this routes to `show `. " + "Case-insensitive; the leading `C` is required (e.g. C12345).", ], ) @@ -153,7 +153,7 @@ class ResultsCommand : Callable { @Command( name = "show", mixinStandardHelpOptions = true, - description = ["Show the recorded result for a TestRail case ID"], + description = ["Show the recorded result for a test case ID"], ) class ResultsShowCommand : Callable { @@ -177,7 +177,7 @@ class ResultsShowCommand : Callable { arity = "1", paramLabel = "", description = [ - "TestRail case ID, e.g. C12345. Case-insensitive; the leading `C` is required.", + "Test-case ID, e.g. C12345. Case-insensitive; the leading `C` is required.", ], ) lateinit var caseId: String diff --git a/trailblaze-host/src/main/java/xyz/block/trailblaze/host/TrailblazeHostYamlRunner.kt b/trailblaze-host/src/main/java/xyz/block/trailblaze/host/TrailblazeHostYamlRunner.kt index f37fe46e..ac369b24 100644 --- a/trailblaze-host/src/main/java/xyz/block/trailblaze/host/TrailblazeHostYamlRunner.kt +++ b/trailblaze-host/src/main/java/xyz/block/trailblaze/host/TrailblazeHostYamlRunner.kt @@ -1593,7 +1593,7 @@ object TrailblazeHostYamlRunner { if (promptSteps.isEmpty()) { throw TrailblazeException( "Trail has no executable prompt steps — this would be a false positive pass. " + - "Add steps to this trail file or the TestRail case.", + "Add steps to this trail file or the source test case.", ) } diff --git a/trailblaze-host/src/main/java/xyz/block/trailblaze/host/rules/BaseHostTrailblazeTest.kt b/trailblaze-host/src/main/java/xyz/block/trailblaze/host/rules/BaseHostTrailblazeTest.kt index 183eb62c..2e8e49d5 100644 --- a/trailblaze-host/src/main/java/xyz/block/trailblaze/host/rules/BaseHostTrailblazeTest.kt +++ b/trailblaze-host/src/main/java/xyz/block/trailblaze/host/rules/BaseHostTrailblazeTest.kt @@ -379,7 +379,7 @@ abstract class BaseHostTrailblazeTest( } /** - * The title of the trail currently being executed (e.g. TestRail case name). + * The title of the trail currently being executed (e.g. an external test-case name). * * Updated at the start of each [runTrail] call so [MultiAgentV3TestAgentRunner] can * forward it as [RecommendationContext.overallObjective] for every step. This lets the diff --git a/trailblaze-host/src/main/java/xyz/block/trailblaze/ui/recordings/RecordedTrailsRepoJvm.kt b/trailblaze-host/src/main/java/xyz/block/trailblaze/ui/recordings/RecordedTrailsRepoJvm.kt index b7593df5..0af4ff3e 100644 --- a/trailblaze-host/src/main/java/xyz/block/trailblaze/ui/recordings/RecordedTrailsRepoJvm.kt +++ b/trailblaze-host/src/main/java/xyz/block/trailblaze/ui/recordings/RecordedTrailsRepoJvm.kt @@ -21,7 +21,7 @@ import xyz.block.trailblaze.util.Console /** * JVM implementation of RecordingsRepo that saves recordings to the file system. * - * All trails are stored directly under the trails directory (e.g., trails/testrail/suite_123/...). + * All trails are stored directly under the trails directory (e.g., trails/regression/suite_123/...). * * @param trailsDirectory The root directory for all trails. Defaults to ~/.trailblaze/trails */ @@ -57,7 +57,7 @@ class RecordedTrailsRepoJvm( val directoryPath: String val fileName: String if (trailConfig.id != null) { - // trailConfig.id is the trail path (e.g., "testrail/suite_123/section_456/case_789") + // trailConfig.id is the trail path (e.g., "regression/suite_123/section_456/case_789") // The directory IS the trail identity, so filename is just platform-classifiers val trailPath = trailConfig.id!! // Prepend save subdirectory if configured, otherwise save at root @@ -99,7 +99,7 @@ class RecordedTrailsRepoJvm( override fun getExistingTrails(sessionInfo: SessionInfo): List { val trailConfig = sessionInfo.trailConfig ?: TrailConfig() - // trailConfig.id is the trail path (e.g., "testrail/suite_123/section_456/case_789") + // trailConfig.id is the trail path (e.g., "regression/suite_123/section_456/case_789") // The directory IS the trail identity val trailPath = trailConfig.id ?: return emptyList() @@ -133,7 +133,7 @@ class RecordedTrailsRepoJvm( } override fun getWatchDirectoryForSession(sessionInfo: SessionInfo): String? { - // trailConfig.id is the trail path (e.g., "testrail/suite_123/section_456/case_789") + // trailConfig.id is the trail path (e.g., "regression/suite_123/section_456/case_789") val trailPath = sessionInfo.trailConfig?.id ?: return null // Return the directory if it exists diff --git a/trailblaze-host/src/test/resources/cli-output-baselines/help-trailblaze-results-show.txt b/trailblaze-host/src/test/resources/cli-output-baselines/help-trailblaze-results-show.txt index e82e670e..3d21c854 100644 --- a/trailblaze-host/src/test/resources/cli-output-baselines/help-trailblaze-results-show.txt +++ b/trailblaze-host/src/test/resources/cli-output-baselines/help-trailblaze-results-show.txt @@ -7,8 +7,8 @@ # ---------------------------------------------------------------------- Usage: trailblaze results show [-hV] [--all-devices] [--json] [--latest] [--device=] [--repo=] -Show the recorded result for a TestRail case ID - TestRail case ID, e.g. C12345. Case-insensitive; the +Show the recorded result for a test case ID + Test-case ID, e.g. C12345. Case-insensitive; the leading `C` is required. --all-devices Print a one-line summary for every device profile that has a recorded result for this case. diff --git a/trailblaze-host/src/test/resources/cli-output-baselines/help-trailblaze-results.txt b/trailblaze-host/src/test/resources/cli-output-baselines/help-trailblaze-results.txt index d6db61e9..e0ad0a6d 100644 --- a/trailblaze-host/src/test/resources/cli-output-baselines/help-trailblaze-results.txt +++ b/trailblaze-host/src/test/resources/cli-output-baselines/help-trailblaze-results.txt @@ -8,14 +8,13 @@ Usage: trailblaze results [-hV] [--all-devices] [--json] [--latest] [--device=] [--repo=] [] [COMMAND] -Query the persisted test-result index for a TestRail case. Passing a positional +Query the persisted test-result index for a test case. Passing a positional `` (e.g. `trailblaze results C12345 --device android-phone`) is equivalent to the explicit `trailblaze results show ` form — picocli routes the bare case-id straight to the `show` subcommand. - [] TestRail case ID. When supplied without a - subcommand, this routes to `show `. - Case-insensitive; the leading `C` is required (e. - g. C12345). + [] Test-case ID. When supplied without a subcommand, + this routes to `show `. Case-insensitive; + the leading `C` is required (e.g. C12345). --all-devices Forwarded to `show --all-devices`. --device= Forwarded to `show --device`. See `trailblaze results show --help` for the full flag set. @@ -25,4 +24,4 @@ routes the bare case-id straight to the `show` subcommand. --repo= Forwarded to `show --repo`. -V, --version Print version information and exit. Commands: - show Show the recorded result for a TestRail case ID + show Show the recorded result for a test case ID diff --git a/trailblaze-host/src/test/resources/cli-output-baselines/help-trailblaze.txt b/trailblaze-host/src/test/resources/cli-output-baselines/help-trailblaze.txt index a02c2837..275e9ac7 100644 --- a/trailblaze-host/src/test/resources/cli-output-baselines/help-trailblaze.txt +++ b/trailblaze-host/src/test/resources/cli-output-baselines/help-trailblaze.txt @@ -30,9 +30,9 @@ Trailblaze - AI-powered device automation the exit code. waypoint Match named app locations (waypoints) against captured screen state. - results Query the persisted test-result index for a TestRail case. - Passing a positional `` (e.g. `trailblaze results - C12345 --device android-phone`) is equivalent to the explicit + results Query the persisted test-result index for a test case. Passing a + positional `` (e.g. `trailblaze results C12345 + --device android-phone`) is equivalent to the explicit `trailblaze results show ` form — picocli routes the bare case-id straight to the `show` subcommand. config View and set configuration (target app, device defaults, AI diff --git a/trailblaze-models/src/commonMain/kotlin/xyz/block/trailblaze/logs/model/SessionId.kt b/trailblaze-models/src/commonMain/kotlin/xyz/block/trailblaze/logs/model/SessionId.kt index bc249e34..cfe9c28f 100644 --- a/trailblaze-models/src/commonMain/kotlin/xyz/block/trailblaze/logs/model/SessionId.kt +++ b/trailblaze-models/src/commonMain/kotlin/xyz/block/trailblaze/logs/model/SessionId.kt @@ -60,7 +60,7 @@ value class SessionId(val value: String) { * override — otherwise host and device would write to two different session * directories. * - * Does **not** truncate: the full input (including long TestRail + * Does **not** truncate: the full input (including long external * `__suite__section__case` suffixes) is preserved so downstream tooling can * map session IDs back to test identifiers. */ diff --git a/trailblaze-models/src/commonMain/kotlin/xyz/block/trailblaze/recordings/TrailRecordings.kt b/trailblaze-models/src/commonMain/kotlin/xyz/block/trailblaze/recordings/TrailRecordings.kt index 5f3156f9..4b0ec907 100644 --- a/trailblaze-models/src/commonMain/kotlin/xyz/block/trailblaze/recordings/TrailRecordings.kt +++ b/trailblaze-models/src/commonMain/kotlin/xyz/block/trailblaze/recordings/TrailRecordings.kt @@ -13,7 +13,7 @@ object TrailRecordings { * ``` * trails/ * └── generated/ <- Subdirectory (configurable) - * └── testrail/suite_123/section_456/case_789/ <- Trail ID (directory = trail identity) + * └── regression/suite_123/section_456/case_789/ <- Trail ID (directory = trail identity) * ├── blaze.yaml (or trailblaze.yaml) <- Source of truth: NL steps (no recordings) * ├── ios-iphone.trail.yaml <- Recording for iOS iPhone * ├── ios-ipad.trail.yaml <- Recording for iOS iPad @@ -23,7 +23,7 @@ object TrailRecordings { * Trail directory structure (without subdirectories - open source default): * ``` * trails/ - * └── testrail/suite_123/section_456/case_789/ <- Trail ID (directory = trail identity) + * └── regression/suite_123/section_456/case_789/ <- Trail ID (directory = trail identity) * ├── blaze.yaml (or trailblaze.yaml) * ├── ios-iphone.trail.yaml * └── android.trail.yaml diff --git a/trailblaze-models/src/jvmTest/kotlin/xyz/block/trailblaze/logs/model/SessionIdTest.kt b/trailblaze-models/src/jvmTest/kotlin/xyz/block/trailblaze/logs/model/SessionIdTest.kt index 1e59e204..817ff6a4 100644 --- a/trailblaze-models/src/jvmTest/kotlin/xyz/block/trailblaze/logs/model/SessionIdTest.kt +++ b/trailblaze-models/src/jvmTest/kotlin/xyz/block/trailblaze/logs/model/SessionIdTest.kt @@ -24,20 +24,20 @@ class SessionIdTest { } @Test - fun `sanitized preserves long TestRail-style suffixes without truncation`() { + fun `sanitized preserves long external-style suffixes without truncation`() { // Regression pin: the previous 100-char cap dropped __suite/__section/__case // suffixes that downstream result-mapping tooling relies on. - val longTestRailId = + val longExternalId = "2026_04_20_11_16_18_example_suite_long_test_name_" + "verify_that_the_action_buttons_appear_for_active_items" + "__suite_1__section_2__case_3_1234" - val sanitized = SessionId.sanitized(longTestRailId) + val sanitized = SessionId.sanitized(longExternalId) - assertEquals(longTestRailId.length, sanitized.value.length) + assertEquals(longExternalId.length, sanitized.value.length) assertTrue( sanitized.value.contains("__suite_1__section_2__case_3"), - "TestRail suffix must survive sanitization; got: ${sanitized.value}", + "Long suite/section/case suffix must survive sanitization; got: ${sanitized.value}", ) } diff --git a/trailblaze-models/src/jvmTest/kotlin/xyz/block/trailblaze/logs/model/SessionStartedMemorySnapshotTest.kt b/trailblaze-models/src/jvmTest/kotlin/xyz/block/trailblaze/logs/model/SessionStartedMemorySnapshotTest.kt index 31b6b2e6..ecef82bc 100644 --- a/trailblaze-models/src/jvmTest/kotlin/xyz/block/trailblaze/logs/model/SessionStartedMemorySnapshotTest.kt +++ b/trailblaze-models/src/jvmTest/kotlin/xyz/block/trailblaze/logs/model/SessionStartedMemorySnapshotTest.kt @@ -20,7 +20,7 @@ import kotlin.test.assertTrue * 3. A legacy JSON envelope written BEFORE these fields existed (no * `resolvedInitialMemory` / `sensitiveMemoryKeys` keys present) still decodes — * the defaults kick in. This is what makes the field addition backwards-compatible - * for session logs already on disk and in TestRail uploads. + * for session logs already on disk and in external test-result uploads. */ class SessionStartedMemorySnapshotTest { diff --git a/trailblaze-report/src/main/java/xyz/block/trailblaze/report/models/TestResultCell.kt b/trailblaze-report/src/main/java/xyz/block/trailblaze/report/models/TestResultCell.kt index d9be38f0..bd1de184 100644 --- a/trailblaze-report/src/main/java/xyz/block/trailblaze/report/models/TestResultCell.kt +++ b/trailblaze-report/src/main/java/xyz/block/trailblaze/report/models/TestResultCell.kt @@ -30,7 +30,7 @@ import kotlinx.serialization.Serializable * writer should preserve unknown fields from the existing on-disk file (see how * `TestResultsRepoPublisher.writeCellFile` merges into existing content) so a vN reader * can still find the fields it expects. - * @property test_case_id Identifier for the case (e.g. `C12345` for TestRail). Path-partitioned + * @property test_case_id Identifier for the case (e.g. `C12345`). Path-partitioned * in the index repo so `git log results/testrail///latest.json` shows that * cell's full history. * @property device Device profile the case ran on (e.g. `android-phone`). One cell per diff --git a/trailblaze-server/src/main/java/xyz/block/trailblaze/logs/server/endpoints/GenerateReportEndpoint.kt b/trailblaze-server/src/main/java/xyz/block/trailblaze/logs/server/endpoints/GenerateReportEndpoint.kt index 23d75209..bdbdb608 100644 --- a/trailblaze-server/src/main/java/xyz/block/trailblaze/logs/server/endpoints/GenerateReportEndpoint.kt +++ b/trailblaze-server/src/main/java/xyz/block/trailblaze/logs/server/endpoints/GenerateReportEndpoint.kt @@ -157,7 +157,7 @@ object GenerateReportEndpoint { /** * Hashes [input] to a fixed-length hex string suitable for use in a filename. * Keeps `trailblaze_live_report_.html` under common 255-byte filename - * limits regardless of how long the session id is (TestRail-style ids in + * limits regardless of how long the session id is (externally-sourced ids in * particular can be very long). */ private fun shortHash(input: String): String { diff --git a/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/TrailblazeMcpSessionContext.kt b/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/TrailblazeMcpSessionContext.kt index 447b29a3..69ac6178 100644 --- a/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/TrailblazeMcpSessionContext.kt +++ b/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/TrailblazeMcpSessionContext.kt @@ -22,7 +22,6 @@ import kotlinx.serialization.json.JsonPrimitive import xyz.block.trailblaze.agent.TwoTierAgentConfig import xyz.block.trailblaze.devices.TrailblazeDeviceId import xyz.block.trailblaze.mcp.models.McpSessionId -import xyz.block.trailblaze.mcp.toolsets.ToolLoadingStrategy import xyz.block.trailblaze.util.Console /** @@ -43,7 +42,7 @@ const val TRAILBLAZE_CLI_CLIENT_NAME: String = "TrailblazeCLI" /** * Controls which tools are exposed to MCP clients. * - * - [FULL]: All tools registered (device management, TestRail, Buildkite, primitive tools, etc.) + * - [FULL]: All tools registered (device management, primitive tools, host integrations, etc.) * - [MINIMAL]: Only high-level tools: device, blaze, verify, ask, trail. * Designed for external MCP clients (Claude Code, Goose) that should use * Trailblaze as a black-box automation engine. @@ -176,22 +175,11 @@ class TrailblazeMcpSessionContext( /** * Tool profile controlling which tools are exposed to the MCP client. * - * - `FULL` (default): All tools registered (device management, TestRail, Buildkite, etc.) + * - `FULL` (default): All tools registered (device management, primitive tools, host integrations, etc.) * - `MINIMAL`: Only device, blaze, verify, ask, trail. For external MCP clients. */ var toolProfile: McpToolProfile = McpToolProfile.FULL, - /** - * Strategy for how tools are loaded and presented to the LLM. - * - * - `ALL_TOOLS` (default): All tool categories enabled upfront. Maximum reliability — - * the LLM always has every tool available without needing to request more. - * - `PROGRESSIVE`: Start with minimal tools (CORE_INTERACTION + OBSERVATION) and let - * the LLM request additional categories via the `toolbox()` MCP tool. Saves tokens - * but requires the LLM to correctly identify when it needs more tools. - */ - @Volatile var toolLoadingStrategy: ToolLoadingStrategy = ToolLoadingStrategy.ALL_TOOLS, - /** * Transport mode for internal agent tool execution. * @@ -540,7 +528,6 @@ class TrailblazeMcpSessionContext( appendLine("Agent implementation: ${agentImplementation.name}") appendLine("Include primitive tools: $includePrimitiveTools") appendLine("Tool profile: ${toolProfile.name}") - appendLine("Tool loading strategy: ${toolLoadingStrategy.name}") twoTierAgentConfig?.let { config -> appendLine("Two-tier agent: ${if (config.enabled) "ENABLED" else "disabled"}") if (config.enabled) { diff --git a/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/newtools/ConfigToolSet.kt b/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/newtools/ConfigToolSet.kt index f26281d7..910a5b1f 100644 --- a/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/newtools/ConfigToolSet.kt +++ b/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/newtools/ConfigToolSet.kt @@ -14,7 +14,6 @@ import xyz.block.trailblaze.mcp.TrailblazeMcpBridge import xyz.block.trailblaze.mcp.TrailblazeMcpMode import xyz.block.trailblaze.mcp.TrailblazeMcpSessionContext import xyz.block.trailblaze.mcp.ViewHierarchyVerbosity -import xyz.block.trailblaze.mcp.toolsets.ToolLoadingStrategy import xyz.block.trailblaze.util.Console /** @@ -230,13 +229,6 @@ class ConfigToolSet( sessionContext?.viewHierarchyVerbosity = verbosity null } - KEY_TOOL_LOADING_STRATEGY -> { - val strategy = - ToolLoadingStrategy.entries.find { it.name.equals(value, ignoreCase = true) } - ?: return "Invalid strategy: $value" - sessionContext?.toolLoadingStrategy = strategy - null - } KEY_TOOL_PROFILE -> { val profile = McpToolProfile.entries.find { it.name.equals(value, ignoreCase = true) } ?: return "Invalid tool profile: $value" @@ -270,7 +262,6 @@ class ConfigToolSet( const val KEY_AGENT_IMPLEMENTATION = "agentImplementation" const val KEY_SCREENSHOT_FORMAT = "screenshotFormat" const val KEY_VIEW_HIERARCHY_VERBOSITY = "viewHierarchyVerbosity" - const val KEY_TOOL_LOADING_STRATEGY = "toolLoadingStrategy" const val KEY_MODE = "mode" const val KEY_TOOL_PROFILE = "toolProfile" @@ -308,7 +299,6 @@ class ConfigToolSet( values[KEY_MODE] = ctx.mode.name values[KEY_SCREENSHOT_FORMAT] = ctx.screenshotFormat.name values[KEY_VIEW_HIERARCHY_VERBOSITY] = ctx.viewHierarchyVerbosity.name - values[KEY_TOOL_LOADING_STRATEGY] = ctx.toolLoadingStrategy.name values[KEY_TOOL_PROFILE] = ctx.toolProfile.name } @@ -372,11 +362,6 @@ class ConfigToolSet( description = "Detail level for view hierarchy data", validValues = ViewHierarchyVerbosity.entries.map { it.name }, ), - ConfigKeyDef( - key = KEY_TOOL_LOADING_STRATEGY, - description = "How tools are loaded (ALL_TOOLS or PROGRESSIVE)", - validValues = ToolLoadingStrategy.entries.map { it.name }, - ), ConfigKeyDef( key = KEY_TOOL_PROFILE, description = "Which tools are exposed (FULL or MINIMAL)", diff --git a/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/newtools/ToolSetManagementToolSet.kt b/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/newtools/ToolSetManagementToolSet.kt deleted file mode 100644 index e691ebd0..00000000 --- a/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/newtools/ToolSetManagementToolSet.kt +++ /dev/null @@ -1,204 +0,0 @@ -package xyz.block.trailblaze.mcp.newtools - -import ai.koog.agents.core.tools.annotations.LLMDescription -import ai.koog.agents.core.tools.annotations.Tool -import ai.koog.agents.core.tools.reflect.ToolSet -import xyz.block.trailblaze.mcp.toolsets.DynamicToolSetManager -import xyz.block.trailblaze.mcp.toolsets.ToolSetCategory -import xyz.block.trailblaze.mcp.toolsets.generateCategorySummaryForLLM - -/** - * Minimal MCP tool for enabling additional tool categories. - * - * This is the default toolset - provides just ONE tool: `tools`. - * All category info is in the tool description - no need for separate list/get tools. - */ -@Suppress("unused") -class ToolSetManagementToolSet( - private val toolSetManager: DynamicToolSetManager?, -) : ToolSet { - - @LLMDescription( - """ - Enable additional tool categories when you need more capabilities. - - Categories: - - OBSERVATION: Screen state tools (getScreenState, viewHierarchy) - - CORE_INTERACTION: Tap, swipe, type, press keys - - NAVIGATION: Back, scroll, launch app, open URL - - VERIFICATION: Assert element visible, compare values - - MEMORY: Store/recall values across steps - - ALL: Everything (large context - use sparingly) - - Examples: - - tools(enable=["OBSERVATION"]) → adds screen inspection - - tools(enable=["CORE_INTERACTION", "NAVIGATION"]) → manual control - """ - ) - @Tool - fun tools( - @LLMDescription("Categories to enable: OBSERVATION, CORE_INTERACTION, NAVIGATION, VERIFICATION, MEMORY, ALL") - enable: List, - ): String { - val manager = toolSetManager - ?: return "Error: Tool management not available in this session" - - if (enable.isEmpty()) { - return "Error: No categories provided" - } - - return manager.setCategories(enable.toSet()) - } -} - -/** - * Advanced MCP tools for fine-grained toolset management. - * - * This toolset is NOT registered by default. Register it explicitly when you need: - * - List categories and see what's enabled - * - Quick presets (useMinimalTools, useStandardTools, useTestingTools) - * - Incremental category changes (addToolCategory, removeToolCategory) - * - Focus mode (focusOnCategory) - * - Reset to defaults (resetToolCategories) - * - * For most use cases, the minimal [ToolSetManagementToolSet] is sufficient. - */ -@Suppress("unused") -class AdvancedToolSetManagementToolSet( - private val toolSetManager: DynamicToolSetManager?, -) : ToolSet { - - @LLMDescription("List all available tool categories with detailed descriptions.") - @Tool - fun listToolCategories(): String { - return generateCategorySummaryForLLM() - } - - @LLMDescription("Get the currently enabled tool categories and count of available tools.") - @Tool - fun getEnabledCategories(): String { - val manager = toolSetManager - ?: return "Error: Tool management not available in this session" - - return manager.describeEnabledCategories() - } - - @LLMDescription( - """ - Add a tool category to the currently enabled set. - - Use this when you need additional tools without removing your current ones. - """ - ) - @Tool - fun addToolCategory( - @LLMDescription("Category to add") - category: ToolSetCategory, - ): String { - val manager = toolSetManager - ?: return "Error: Tool management not available in this session" - - return manager.addCategory(category) - } - - @LLMDescription( - """ - Remove a tool category from the currently enabled set. - - Use this to reduce context size by removing tools you no longer need. - At least one category must remain enabled. - """ - ) - @Tool - fun removeToolCategory( - @LLMDescription("Category to remove") - category: ToolSetCategory, - ): String { - val manager = toolSetManager - ?: return "Error: Tool management not available in this session" - - return manager.removeCategory(category) - } - - @LLMDescription( - """ - Focus on a single tool category, disabling all others. - - Use this when you want to narrow down to just one type of task. - This minimizes context size for focused work. - """ - ) - @Tool - fun focusOnCategory( - @LLMDescription("The single category to focus on") - category: ToolSetCategory, - ): String { - val manager = toolSetManager - ?: return "Error: Tool management not available in this session" - - return manager.focusOnCategory(category) - } - - @LLMDescription( - """ - Reset to the default tool categories. - - Default categories: CORE_INTERACTION, OBSERVATION - """ - ) - @Tool - fun resetToolCategories(): String { - val manager = toolSetManager - ?: return "Error: Tool management not available in this session" - - return manager.resetToDefault() - } - - @LLMDescription( - """ - Quick preset: Enable minimal tools for simple interaction tasks. - - Enables: CORE_INTERACTION only - Best for: Simple tap/type tasks with known coordinates - """ - ) - @Tool - fun useMinimalTools(): String { - val manager = toolSetManager - ?: return "Error: Tool management not available in this session" - - return manager.setCategories(ToolSetCategory.MINIMAL) - } - - @LLMDescription( - """ - Quick preset: Enable standard tools for typical automation. - - Enables: CORE_INTERACTION, NAVIGATION, OBSERVATION - Best for: General navigation and interaction tasks - """ - ) - @Tool - fun useStandardTools(): String { - val manager = toolSetManager - ?: return "Error: Tool management not available in this session" - - return manager.setCategories(ToolSetCategory.STANDARD) - } - - @LLMDescription( - """ - Quick preset: Enable testing tools for verification workflows. - - Enables: CORE_INTERACTION, NAVIGATION, OBSERVATION, VERIFICATION, MEMORY - Best for: Test automation with assertions and state tracking - """ - ) - @Tool - fun useTestingTools(): String { - val manager = toolSetManager - ?: return "Error: Tool management not available in this session" - - return manager.setCategories(ToolSetCategory.TESTING) - } -} diff --git a/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/toolsets/DynamicToolSetManager.kt b/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/toolsets/DynamicToolSetManager.kt deleted file mode 100644 index 94bb034b..00000000 --- a/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/toolsets/DynamicToolSetManager.kt +++ /dev/null @@ -1,335 +0,0 @@ -package xyz.block.trailblaze.mcp.toolsets - -import io.modelcontextprotocol.kotlin.sdk.server.Server -import xyz.block.trailblaze.mcp.TrailblazeMcpSessionContext -import xyz.block.trailblaze.mcp.models.McpSessionId -import xyz.block.trailblaze.toolcalls.KoogToolExt -import xyz.block.trailblaze.toolcalls.ResolvedToolSet -import xyz.block.trailblaze.toolcalls.TrailblazeToolDescriptor -import xyz.block.trailblaze.toolcalls.toKoogToolDescriptor -import xyz.block.trailblaze.toolcalls.TrailblazeKoogTool.Companion.toTrailblazeToolDescriptor - -/** - * Manages dynamic tool registration for MCP sessions. - * - * Enables the two-tier tool management pattern: - * 1. Parent LLM selects initial categories when spawning a subagent - * 2. Subagent can swap categories as it works through the task - * - * When categories change, this manager: - * - Computes the new set of tools - * - Registers/unregisters tools with the MCP server - * - Sends `tools/list_changed` notification to client - * - * Usage: - * ```kotlin - * val manager = DynamicToolSetManager(mcpServer, sessionContext) - * - * // Parent selects initial categories - * manager.setCategories(setOf(CORE_INTERACTION, NAVIGATION)) - * - * // Subagent discovers it needs verification tools - * manager.addCategory(VERIFICATION) - * - * // Subagent focuses on just verification - * manager.setCategories(setOf(VERIFICATION)) - * ``` - */ -class DynamicToolSetManager( - private val mcpServer: Server, - private val sessionContext: TrailblazeMcpSessionContext, - private val mcpSessionId: McpSessionId, - /** - * Invoked (outside the synchronization lock) each time the registered tool set changes. - * Carries both class-backed and YAML-defined tool names so the downstream MCP registrar - * can advertise both halves — the regression guarded by this contract is "class-only - * consumers silently drop YAML-defined tools like pressBack." - */ - private val onToolsChanged: (ResolvedToolSet) -> Unit, -) { - /** - * Currently enabled categories for this session. - * - * When `includePrimitiveTools = false` (default for external clients): - * - No primitive tool categories are enabled initially - * - External clients use high-level tools like runPrompt() instead - * - * When `includePrimitiveTools = true` (for internal self-connection): - * - Categories depend on the [ToolLoadingStrategy]: - * - ALL_TOOLS: All categories enabled (maximum reliability) - * - PROGRESSIVE: Default categories based on mode (CORE_INTERACTION + OBSERVATION) - * - TRAILBLAZE_AS_AGENT only gets SESSION regardless of strategy - */ - private val lock = Any() - - private var enabledCategories: MutableSet = - if (sessionContext.includePrimitiveTools) { - ToolSetCategory.getDefaultCategoriesForMode( - sessionContext.mode, - sessionContext.toolLoadingStrategy, - ).toMutableSet() - } else { - // External clients start with no primitive tools - // They should use runPrompt() for UI automation - mutableSetOf() - } - - /** - * Currently registered tool surface (class-backed + YAML-defined). - */ - private var registeredTools: ResolvedToolSet = ResolvedToolSet( - toolClasses = emptySet(), - yamlToolNames = emptySet(), - ) - - /** - * Gets the currently enabled categories. - */ - fun getEnabledCategories(): Set = synchronized(lock) { enabledCategories.toSet() } - - /** - * Gets the tool descriptors for currently registered tools. - * - * Used by the inner agent (screen analyzer) to present available tools - * to the LLM in a type-safe manner. - */ - fun getCurrentToolDescriptors(): List = synchronized(lock) { - val classDescriptors = registeredTools.toolClasses.mapNotNull { toolClass -> - toolClass.toKoogToolDescriptor()?.toTrailblazeToolDescriptor() - } - val yamlDescriptors = KoogToolExt.buildDescriptorsForYamlDefined(registeredTools.yamlToolNames) - .map { it.toTrailblazeToolDescriptor() } - classDescriptors + yamlDescriptors - } - - /** - * Gets a human-readable description of enabled categories. - */ - fun describeEnabledCategories(): String = synchronized(lock) { - buildString { - appendLine("Enabled Tool Categories:") - enabledCategories.forEach { category -> - appendLine("- ${category.displayName}: ${category.description.take(80)}...") - } - appendLine() - appendLine("Total tools available: ${registeredTools.toolClasses.size + registeredTools.yamlToolNames.size}") - } - } - - /** - * Sets the enabled categories, replacing any existing ones. - * Categories are filtered based on the current session mode. - * - * Note: When includePrimitiveTools is false, this will warn that primitive tools - * should not be enabled for external clients. - * - * @param categories The new set of categories to enable - * @return Description of what changed - */ - fun setCategories(categories: Set): String { - val changedTools: ResolvedToolSet? - val result = synchronized(lock) { - // Warn if trying to enable primitive tools when includePrimitiveTools is false - val primitiveWarning = if (!sessionContext.includePrimitiveTools && categories.isNotEmpty()) { - "WARNING: Enabling primitive tools for external MCP clients is not recommended.\n" + - "Use runPrompt() for UI automation instead, or set includePrimitiveTools=true for internal use.\n\n" - } else "" - - val allowedCategories = ToolSetCategory.getCategoriesForMode(sessionContext.mode) - val filteredCategories = categories.intersect(allowedCategories) - - // If requested categories aren't allowed in this mode, warn the user - val disallowedCategories = categories - allowedCategories - val warnings = if (disallowedCategories.isNotEmpty()) { - "Warning: Categories ${disallowedCategories.joinToString { it.displayName }} are not available in ${sessionContext.mode.name} mode.\n" - } else "" - - val previousCategories = enabledCategories.toSet() - enabledCategories = filteredCategories.ifEmpty { - // Ensure at least default categories for this mode and strategy - ToolSetCategory.getDefaultCategoriesForMode(sessionContext.mode, sessionContext.toolLoadingStrategy) - }.toMutableSet() - - val added = enabledCategories - previousCategories - val removed = previousCategories - enabledCategories - - changedTools = computeToolRefresh() - - buildString { - append(primitiveWarning) - append(warnings) - if (added.isNotEmpty()) { - appendLine("Added categories: ${added.joinToString { it.displayName }}") - } - if (removed.isNotEmpty()) { - appendLine("Removed categories: ${removed.joinToString { it.displayName }}") - } - appendLine("Active categories: ${enabledCategories.joinToString { it.displayName }.ifEmpty { "(none)" }}") - appendLine("Tools available: ${registeredTools.toolClasses.size + registeredTools.yamlToolNames.size}") - appendLine("Mode: ${sessionContext.mode.name}") - appendLine("Include primitive tools: ${sessionContext.includePrimitiveTools}") - } - } - // Invoke callback outside the lock to prevent potential deadlock - changedTools?.let { onToolsChanged(it) } - return result - } - - /** - * Adds a category to the enabled set. - * - * @param category The category to add - * @return Description of what changed - */ - fun addCategory(category: ToolSetCategory): String { - val changedTools: ResolvedToolSet? - val result = synchronized(lock) { - if (category in enabledCategories) { - return "Category '${category.displayName}' is already enabled." - } - - enabledCategories.add(category) - changedTools = computeToolRefresh() - - "Added '${category.displayName}'. Active categories: ${enabledCategories.joinToString { it.displayName }}" - } - changedTools?.let { onToolsChanged(it) } - return result - } - - /** - * Removes a category from the enabled set. - * - * @param category The category to remove - * @return Description of what changed - */ - fun removeCategory(category: ToolSetCategory): String { - val changedTools: ResolvedToolSet? - val result = synchronized(lock) { - if (category !in enabledCategories) { - return "Category '${category.displayName}' is not currently enabled." - } - - if (enabledCategories.size == 1) { - return "Cannot remove the last category. At least one must remain enabled." - } - - enabledCategories.remove(category) - changedTools = computeToolRefresh() - - "Removed '${category.displayName}'. Active categories: ${enabledCategories.joinToString { it.displayName }}" - } - changedTools?.let { onToolsChanged(it) } - return result - } - - /** - * Swaps to a specific category, disabling all others. - * Useful when the subagent wants to focus on one type of task. - * - * @param category The category to focus on - * @return Description of what changed - */ - fun focusOnCategory(category: ToolSetCategory): String { - return setCategories(setOf(category)) - } - - /** - * Resets to the default categories for the current mode and loading strategy. - * Respects the includePrimitiveTools setting. - */ - fun resetToDefault(): String { - return if (sessionContext.includePrimitiveTools) { - setCategories( - ToolSetCategory.getDefaultCategoriesForMode( - sessionContext.mode, - sessionContext.toolLoadingStrategy, - ), - ) - } else { - setCategories(emptySet()) - } - } - - /** - * Called when the session mode changes. - * Resets categories to the default for the new mode and loading strategy. - * Respects the includePrimitiveTools setting. - */ - fun onModeChanged() { - val changedTools: ResolvedToolSet? - synchronized(lock) { - enabledCategories = if (sessionContext.includePrimitiveTools) { - ToolSetCategory.getDefaultCategoriesForMode( - sessionContext.mode, - sessionContext.toolLoadingStrategy, - ).toMutableSet() - } else { - mutableSetOf() - } - changedTools = computeToolRefresh() - } - changedTools?.let { onToolsChanged(it) } - } - - /** - * Called when includePrimitiveTools setting changes. - * Enables or disables primitive tool categories accordingly. - */ - fun onIncludePrimitiveToolsChanged() { - val changedTools: ResolvedToolSet? - synchronized(lock) { - if (sessionContext.includePrimitiveTools) { - // Enable default categories for the current mode and strategy - enabledCategories = ToolSetCategory.getDefaultCategoriesForMode( - sessionContext.mode, - sessionContext.toolLoadingStrategy, - ).toMutableSet() - } else { - // Disable all primitive tool categories - enabledCategories = mutableSetOf() - } - changedTools = computeToolRefresh() - } - changedTools?.let { onToolsChanged(it) } - } - - /** - * Lists all available categories with descriptions. - * Useful for the subagent to understand what's available. - */ - fun listAvailableCategories(): String = generateCategorySummaryForLLM() - - /** - * Refreshes the registered tools based on current categories. - * Called internally when categories change (always within synchronized(lock)). - * - * Updates [registeredTools] inside the lock but defers the [onToolsChanged] - * callback to after the lock is released, preventing potential deadlock if the - * callback reads back into this manager. - * - * @return The new resolved tool set if it changed, or null if no change. - */ - private fun computeToolRefresh(): ResolvedToolSet? { - val newTools = ToolSetCategoryMapping.resolve(enabledCategories) - - return if (newTools != registeredTools) { - registeredTools = newTools - newTools - } else { - null - } - } - - /** - * Initializes with the current categories. - * Call this after creating the manager to register initial tools. - */ - fun initialize() { - val changedTools: ResolvedToolSet? - synchronized(lock) { - changedTools = computeToolRefresh() - } - changedTools?.let { onToolsChanged(it) } - } -} diff --git a/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/toolsets/ToolSetCategory.kt b/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/toolsets/ToolSetCategory.kt index 3410bd79..c94766e5 100644 --- a/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/toolsets/ToolSetCategory.kt +++ b/trailblaze-server/src/main/java/xyz/block/trailblaze/mcp/toolsets/ToolSetCategory.kt @@ -9,23 +9,6 @@ import xyz.block.trailblaze.toolcalls.TrailblazeToolSet import xyz.block.trailblaze.toolcalls.TrailblazeToolSetCatalog import kotlin.reflect.KClass -/** - * Strategy for how tools are loaded and presented to the LLM. - * - * - [ALL_TOOLS]: Send all tools upfront. No progressive disclosure. This is the default - * to maximize reliability — the LLM always has every tool available. - * - [PROGRESSIVE]: Start with minimal tools and let the LLM request more categories - * as needed via the `toolbox()` MCP tool. Saves tokens but may cause regressions - * if the LLM doesn't request the right categories. - */ -enum class ToolLoadingStrategy { - /** Send all tools upfront. Default — maximizes reliability. */ - ALL_TOOLS, - - /** Start minimal, LLM requests more as needed. Saves tokens. */ - PROGRESSIVE, -} - /** * Categories of tools that can be enabled/disabled for subagents. * @@ -190,23 +173,6 @@ enum class ToolSetCategory( TrailblazeMcpMode.TRAILBLAZE_AS_AGENT -> setOf(SESSION) } - /** - * Returns the default categories for a given mode and loading strategy. - * - * When [ToolLoadingStrategy.ALL_TOOLS], all categories are enabled upfront. - * When [ToolLoadingStrategy.PROGRESSIVE], the minimal default set is used. - */ - fun getDefaultCategoriesForMode( - mode: TrailblazeMcpMode, - strategy: ToolLoadingStrategy, - ): Set = when (strategy) { - ToolLoadingStrategy.ALL_TOOLS -> when (mode) { - TrailblazeMcpMode.MCP_CLIENT_AS_AGENT -> setOf(ALL) - TrailblazeMcpMode.TRAILBLAZE_AS_AGENT -> setOf(SESSION) - } - ToolLoadingStrategy.PROGRESSIVE -> getDefaultCategoriesForMode(mode) - } - } } @@ -276,7 +242,7 @@ object ToolSetCategoryMapping { * accidentally consume only half. Prefer this over the split [getToolClasses] / * [getYamlToolNames] pair when you need a complete tool surface — every live MCP entrypoint * (DirectMcpToolExecutor, SubagentOrchestrator, the inner-agent fallback in - * TrailblazeMcpServer, DynamicToolSetManager) must advertise both, and the regression this + * TrailblazeMcpServer) must advertise both, and the regression this * API guards against is exactly the "class-only lookup silently drops YAML-defined tools" * bug that shipped with the pressBack migration. * diff --git a/trailblaze-server/src/test/kotlin/xyz/block/trailblaze/mcp/newtools/ToolSetManagementToolSetTest.kt b/trailblaze-server/src/test/kotlin/xyz/block/trailblaze/mcp/newtools/ToolSetManagementToolSetTest.kt deleted file mode 100644 index d480db8c..00000000 --- a/trailblaze-server/src/test/kotlin/xyz/block/trailblaze/mcp/newtools/ToolSetManagementToolSetTest.kt +++ /dev/null @@ -1,233 +0,0 @@ -package xyz.block.trailblaze.mcp.newtools - -import io.modelcontextprotocol.kotlin.sdk.server.Server -import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions -import io.modelcontextprotocol.kotlin.sdk.types.Implementation -import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities -import org.junit.Test -import xyz.block.trailblaze.mcp.TrailblazeMcpMode -import xyz.block.trailblaze.mcp.TrailblazeMcpSessionContext -import xyz.block.trailblaze.mcp.models.McpSessionId -import xyz.block.trailblaze.mcp.toolsets.DynamicToolSetManager -import xyz.block.trailblaze.mcp.toolsets.ToolLoadingStrategy -import xyz.block.trailblaze.mcp.toolsets.ToolSetCategory -import xyz.block.trailblaze.toolcalls.ResolvedToolSet -import kotlin.test.assertContains -import kotlin.test.assertEquals - -/** - * Tests for [ToolSetManagementToolSet] and [AdvancedToolSetManagementToolSet]. - * - * Verifies that tool category management works correctly: - * - Enabling categories via the `tools` tool - * - Error handling when manager is null - * - Advanced management: add, remove, focus, reset, presets - */ -class ToolSetManagementToolSetTest { - - private val testSessionId = McpSessionId("test-session") - - private fun createSessionContext( - includePrimitiveTools: Boolean = true, - mode: TrailblazeMcpMode = TrailblazeMcpMode.MCP_CLIENT_AS_AGENT, - ) = TrailblazeMcpSessionContext( - mcpServerSession = null, - mcpSessionId = testSessionId, - mode = mode, - includePrimitiveTools = includePrimitiveTools, - ) - - private fun createMcpServer(): Server = Server( - Implementation(name = "test", version = "1.0"), - ServerOptions(capabilities = ServerCapabilities()), - ) - - private var lastToolsChanged: ResolvedToolSet = ResolvedToolSet( - toolClasses = emptySet(), - yamlToolNames = emptySet(), - ) - - private fun createManager( - sessionContext: TrailblazeMcpSessionContext = createSessionContext(), - ): DynamicToolSetManager { - return DynamicToolSetManager( - mcpServer = createMcpServer(), - sessionContext = sessionContext, - mcpSessionId = testSessionId, - onToolsChanged = { lastToolsChanged = it }, - ) - } - - // ── ToolSetManagementToolSet ────────────────────────────────────────────── - - @Test - fun `tools returns error when manager is null`() { - val toolSet = ToolSetManagementToolSet(toolSetManager = null) - val result = toolSet.tools(enable = listOf(ToolSetCategory.CORE_INTERACTION)) - assertContains(result, "not available") - } - - @Test - fun `tools returns error when empty categories provided`() { - val manager = createManager() - val toolSet = ToolSetManagementToolSet(toolSetManager = manager) - val result = toolSet.tools(enable = emptyList()) - assertContains(result, "No categories provided") - } - - @Test - fun `tools enables requested categories`() { - val manager = createManager() - val toolSet = ToolSetManagementToolSet(toolSetManager = manager) - - val result = toolSet.tools( - enable = listOf(ToolSetCategory.CORE_INTERACTION, ToolSetCategory.NAVIGATION), - ) - - assertContains(result, "Active categories") - val enabled = manager.getEnabledCategories() - assertEquals(true, enabled.contains(ToolSetCategory.CORE_INTERACTION)) - assertEquals(true, enabled.contains(ToolSetCategory.NAVIGATION)) - } - - // ── AdvancedToolSetManagementToolSet ─────────────────────────────────────── - - @Test - fun `listToolCategories returns category descriptions`() { - val toolSet = AdvancedToolSetManagementToolSet(toolSetManager = null) - val result = toolSet.listToolCategories() - // Should list all categories with descriptions - assertContains(result, "CORE_INTERACTION") - } - - @Test - fun `addToolCategory returns error when manager is null`() { - val toolSet = AdvancedToolSetManagementToolSet(toolSetManager = null) - val result = toolSet.addToolCategory(ToolSetCategory.VERIFICATION) - assertContains(result, "not available") - } - - @Test - fun `addToolCategory adds category to enabled set`() { - val manager = createManager() - manager.initialize() - val toolSet = AdvancedToolSetManagementToolSet(toolSetManager = manager) - - val result = toolSet.addToolCategory(ToolSetCategory.MEMORY) - assertContains(result, "Added") - assertEquals(true, manager.getEnabledCategories().contains(ToolSetCategory.MEMORY)) - } - - @Test - fun `removeToolCategory removes category from enabled set`() { - val manager = createManager() - // Set multiple categories so we can remove one - manager.setCategories(setOf(ToolSetCategory.CORE_INTERACTION, ToolSetCategory.NAVIGATION)) - val toolSet = AdvancedToolSetManagementToolSet(toolSetManager = manager) - - val result = toolSet.removeToolCategory(ToolSetCategory.NAVIGATION) - assertContains(result, "Removed") - assertEquals(false, manager.getEnabledCategories().contains(ToolSetCategory.NAVIGATION)) - } - - @Test - fun `removeToolCategory refuses to remove last category`() { - val manager = createManager() - manager.setCategories(setOf(ToolSetCategory.CORE_INTERACTION)) - val toolSet = AdvancedToolSetManagementToolSet(toolSetManager = manager) - - val result = toolSet.removeToolCategory(ToolSetCategory.CORE_INTERACTION) - assertContains(result, "Cannot remove the last category") - } - - @Test - fun `focusOnCategory switches to single category`() { - val manager = createManager() - manager.setCategories( - setOf(ToolSetCategory.CORE_INTERACTION, ToolSetCategory.NAVIGATION, ToolSetCategory.MEMORY), - ) - val toolSet = AdvancedToolSetManagementToolSet(toolSetManager = manager) - - toolSet.focusOnCategory(ToolSetCategory.VERIFICATION) - assertEquals(setOf(ToolSetCategory.VERIFICATION), manager.getEnabledCategories()) - } - - @Test - fun `resetToolCategories restores defaults for ALL_TOOLS strategy`() { - val sessionContext = createSessionContext(includePrimitiveTools = true) - // Default strategy is ALL_TOOLS - val manager = createManager(sessionContext) - manager.setCategories(setOf(ToolSetCategory.MEMORY)) - val toolSet = AdvancedToolSetManagementToolSet(toolSetManager = manager) - - toolSet.resetToolCategories() - val defaults = ToolSetCategory.getDefaultCategoriesForMode( - sessionContext.mode, - ToolLoadingStrategy.ALL_TOOLS, - ) - assertEquals(defaults, manager.getEnabledCategories()) - assertEquals(true, manager.getEnabledCategories().contains(ToolSetCategory.ALL)) - } - - @Test - fun `resetToolCategories restores defaults for PROGRESSIVE strategy`() { - val sessionContext = createSessionContext(includePrimitiveTools = true).apply { - toolLoadingStrategy = ToolLoadingStrategy.PROGRESSIVE - } - val manager = createManager(sessionContext) - manager.setCategories(setOf(ToolSetCategory.MEMORY)) - val toolSet = AdvancedToolSetManagementToolSet(toolSetManager = manager) - - toolSet.resetToolCategories() - val defaults = ToolSetCategory.getDefaultCategoriesForMode( - sessionContext.mode, - ToolLoadingStrategy.PROGRESSIVE, - ) - assertEquals(defaults, manager.getEnabledCategories()) - assertEquals(true, manager.getEnabledCategories().contains(ToolSetCategory.CORE_INTERACTION)) - assertEquals(true, manager.getEnabledCategories().contains(ToolSetCategory.OBSERVATION)) - } - - @Test - fun `useMinimalTools enables only CORE_INTERACTION`() { - val manager = createManager() - val toolSet = AdvancedToolSetManagementToolSet(toolSetManager = manager) - - toolSet.useMinimalTools() - assertEquals(true, manager.getEnabledCategories().contains(ToolSetCategory.CORE_INTERACTION)) - } - - @Test - fun `useStandardTools enables standard set`() { - val manager = createManager() - val toolSet = AdvancedToolSetManagementToolSet(toolSetManager = manager) - - toolSet.useStandardTools() - val enabled = manager.getEnabledCategories() - assertEquals(true, enabled.contains(ToolSetCategory.CORE_INTERACTION)) - assertEquals(true, enabled.contains(ToolSetCategory.NAVIGATION)) - assertEquals(true, enabled.contains(ToolSetCategory.OBSERVATION)) - } - - @Test - fun `useTestingTools enables testing set`() { - val manager = createManager() - val toolSet = AdvancedToolSetManagementToolSet(toolSetManager = manager) - - toolSet.useTestingTools() - val enabled = manager.getEnabledCategories() - assertEquals(true, enabled.contains(ToolSetCategory.VERIFICATION)) - assertEquals(true, enabled.contains(ToolSetCategory.MEMORY)) - } - - @Test - fun `getEnabledCategories returns current state`() { - val manager = createManager() - manager.setCategories(setOf(ToolSetCategory.CORE_INTERACTION)) - val toolSet = AdvancedToolSetManagementToolSet(toolSetManager = manager) - - val result = toolSet.getEnabledCategories() - assertContains(result, "Enabled Tool Categories") - assertContains(result, "Core Interaction") - } -} diff --git a/trailblaze-server/src/test/kotlin/xyz/block/trailblaze/mcp/toolsets/ToolSetCategoryMappingTest.kt b/trailblaze-server/src/test/kotlin/xyz/block/trailblaze/mcp/toolsets/ToolSetCategoryMappingTest.kt index fb047b4b..8727a189 100644 --- a/trailblaze-server/src/test/kotlin/xyz/block/trailblaze/mcp/toolsets/ToolSetCategoryMappingTest.kt +++ b/trailblaze-server/src/test/kotlin/xyz/block/trailblaze/mcp/toolsets/ToolSetCategoryMappingTest.kt @@ -9,7 +9,7 @@ import kotlin.test.assertTrue * Contract tests for the new bundled category resolvers on [ToolSetCategoryMapping]. * * These pin the shape the production consumers (DirectMcpToolExecutor, SubagentOrchestrator, - * inner-agent fallback in TrailblazeMcpServer, DynamicToolSetManager) rely on — namely that + * inner-agent fallback in TrailblazeMcpServer) rely on — namely that * `resolve(...)` covers both class-backed and YAML-defined tools in one call, so a class-only * refactor can't silently drop the YAML half like the pressBack migration did. */ diff --git a/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/PlatformIconUtils.kt b/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/PlatformIconUtils.kt index 4d85a63e..11a44be8 100644 --- a/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/PlatformIconUtils.kt +++ b/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/PlatformIconUtils.kt @@ -67,7 +67,7 @@ fun TrailblazeDevicePlatform.getIcon(): ImageVector = when (this) { * Returns the appropriate icon for a trail source type, or null if none. * - HANDWRITTEN: Person icon (manually written test) * - GENERATED: SmartToy/robot icon (AI-generated test) - * - TESTRAIL: No icon (default/external source) + * - Other sources: No icon (default/external source) */ fun TrailSourceType.getIcon(): ImageVector? = when (this) { TrailSourceType.HANDWRITTEN -> Icons.Default.Person diff --git a/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/PlatformRecordingsChips.kt b/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/PlatformRecordingsChips.kt index f50d68ea..c649426b 100644 --- a/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/PlatformRecordingsChips.kt +++ b/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/PlatformRecordingsChips.kt @@ -44,7 +44,7 @@ import xyz.block.trailblaze.yaml.TrailSourceType /** * Common data class representing a recording's platform and classifier information. - * Used to share UI components between TrailCard and TestRailCard. + * Used to share UI components across trail and test-case cards. */ data class RecordingDisplayInfo( /** diff --git a/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/PriorityChip.kt b/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/PriorityChip.kt index 686b38c7..6ecc4fcf 100644 --- a/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/PriorityChip.kt +++ b/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/PriorityChip.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.Composable /** * Chip component for displaying priority levels with color-coded styling. - * Used by both TrailsBrowser and TestRail views for consistent UX. + * Used by both the trails browser and test-case views for consistent UX. * * @param priorityShortName The priority identifier (e.g., "P0", "P1", "P2", "P3", "P4", "Smoke") */ diff --git a/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/TestItemCard.kt b/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/TestItemCard.kt index cade02f2..44ad0a3d 100644 --- a/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/TestItemCard.kt +++ b/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/composables/TestItemCard.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.unit.dp /** * Shared card component for displaying test items (trails or test cases). - * Used by both TrailsBrowser and TestRail views for consistent UX. + * Used by both the trails browser and test-case views for consistent UX. * * @param title The title of the test item (optional - if null, id becomes primary) * @param id The unique identifier / path of the test item diff --git a/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/icons/TextIcon.kt b/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/icons/TextIcon.kt index 8a20b70d..b2a78dc8 100644 --- a/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/icons/TextIcon.kt +++ b/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/icons/TextIcon.kt @@ -16,8 +16,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp /** - * TestRail icon showing "TR" text - * Simple and clear representation for TestRail + * Renders a short text label (e.g. "TR") inside a rounded badge. + * A simple, clear text-based icon for sources that have no brand glyph. */ @Composable fun TextIcon( diff --git a/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/recordings/ExistingTrail.kt b/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/recordings/ExistingTrail.kt index 0e26a7c4..ee9e3835 100644 --- a/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/recordings/ExistingTrail.kt +++ b/trailblaze-ui/src/commonMain/kotlin/xyz/block/trailblaze/ui/recordings/ExistingTrail.kt @@ -8,7 +8,7 @@ import xyz.block.trailblaze.recordings.TrailRecordings * * @param absolutePath The absolute path to the trail file * @param relativePath The path relative to the trails directory - * (e.g., "testrail/suite_123/section_456/case_789/ios-iphone.trail.yaml") + * (e.g., "regression/suite_123/section_456/case_789/ios-iphone.trail.yaml") * @param fileName The name of the file (e.g., "ios-iphone.trail.yaml" or "trailblaze.yaml") */ data class ExistingTrail( diff --git a/trailblaze-ui/src/jvmMain/kotlin/xyz/block/trailblaze/ui/tabs/trails/TrailCard.kt b/trailblaze-ui/src/jvmMain/kotlin/xyz/block/trailblaze/ui/tabs/trails/TrailCard.kt index ec75c1eb..d11adf57 100644 --- a/trailblaze-ui/src/jvmMain/kotlin/xyz/block/trailblaze/ui/tabs/trails/TrailCard.kt +++ b/trailblaze-ui/src/jvmMain/kotlin/xyz/block/trailblaze/ui/tabs/trails/TrailCard.kt @@ -9,7 +9,7 @@ import xyz.block.trailblaze.ui.composables.TestItemCard /** * A card displaying a trail with its variants. - * Uses the shared TestItemCard for consistent UX with TestRail views. + * Uses the shared TestItemCard for consistent UX with test-case views. * * @param trail The trail to display * @param isSelected Whether this trail is currently selected