@frankzhang2026/opencode-android-orchestrator 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.1 - 2026-10-09
4
+
5
+ Fixes Android lazy test-output compatibility.
6
+
7
+ - Avoid resolving Android test output providers during project discovery, fixing
8
+ upgrades on builds with mapped ASM test-class outputs.
9
+ - Collect test directory evidence immediately before each Test task executes,
10
+ after producer tasks complete, while retaining skipped/NO-SOURCE validation.
11
+ - Add real Gradle regression coverage for mapped test outputs through discovery
12
+ and baseline/RED/GREEN inventory collection.
13
+
3
14
  ## 1.2.0 - 2026-10-09
4
15
 
5
16
  - Fix Gradle 6.7.1 init-script request handling and dynamic legacy Kotlin source
package/README.md CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  Reusable OpenCode orchestration for macOS Android projects.
4
4
 
5
- Version 1.2.0 includes legacy Gradle compatibility and verification improvements. See
6
- [development validation](release/1.2.0-development-validation.md) for evidence.
5
+ Version 1.2.1 fixes discovery and test collection with lazy Android test outputs.
6
+ The commands below pin the 1.2.1 npm package.
7
+ See [development validation](release/1.2.1-development-validation.md) for evidence.
7
8
 
8
9
  Version `0.2.0` is the first published lifecycle release. Version `0.3.0` adds
9
10
  default all-module orchestration and stronger verification contracts. Version
@@ -79,9 +80,9 @@ project builds retain their configured cache behavior.
79
80
  ## Quick start
80
81
 
81
82
  ```sh
82
- npx @frankzhang2026/opencode-android-orchestrator@1.2.0 init .
83
+ npx @frankzhang2026/opencode-android-orchestrator@1.2.1 init .
83
84
  $EDITOR automation/automation-commit-prefix
84
- npx @frankzhang2026/opencode-android-orchestrator@1.2.0 doctor .
85
+ npx @frankzhang2026/opencode-android-orchestrator@1.2.1 doctor .
85
86
  opencode --agent scheduled-planner .
86
87
  ```
87
88
 
@@ -91,7 +92,7 @@ a task contract without selecting a primary module. To intentionally restrict
91
92
  generated contracts to one module, opt into primary-module scope:
92
93
 
93
94
  ```sh
94
- npx @frankzhang2026/opencode-android-orchestrator@1.2.0 init . \
95
+ npx @frankzhang2026/opencode-android-orchestrator@1.2.1 init . \
95
96
  --module-scope primary \
96
97
  --primary-module :mobile
97
98
  ```
@@ -119,7 +120,7 @@ For an existing manifest-managed installation whose generated module/task
119
120
  lists are incomplete, refresh all derived Gradle data in one upgrade:
120
121
 
121
122
  ```sh
122
- npx @frankzhang2026/opencode-android-orchestrator@1.2.0 upgrade . \
123
+ npx @frankzhang2026/opencode-android-orchestrator@1.2.1 upgrade . \
123
124
  --refresh-gradle-discovery
124
125
  ```
125
126
 
@@ -476,7 +477,7 @@ preparation alone as resource installation;
476
477
  ## Init
477
478
 
478
479
  ```sh
479
- npx @frankzhang2026/opencode-android-orchestrator@1.2.0 init .
480
+ npx @frankzhang2026/opencode-android-orchestrator@1.2.1 init .
480
481
  $EDITOR automation/automation-commit-prefix
481
482
  opencode --agent scheduled-planner .
482
483
  ```
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities-script.d.ts","sourceRoot":"","sources":["../../src/installer/capabilities-script.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,wBAAwB,QAgFpC,CAAC"}
1
+ {"version":3,"file":"capabilities-script.d.ts","sourceRoot":"","sources":["../../src/installer/capabilities-script.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,wBAAwB,QAmFpC,CAAC"}
@@ -41,7 +41,10 @@ gradle.projectsEvaluated {
41
41
  def sourceSets = androidId != null ? get(extension, "sourceSets") : p.extensions.findByName("sourceSets")
42
42
  if (sourceSets == null) throw new GradleException("Cannot inspect source sets for " + key)
43
43
  def tests = p.tasks.withType(org.gradle.api.tasks.testing.Test).toList()
44
- def testOutputs = tests.collectMany { it.testClassesDirs.files.toList() }.collect { it.canonicalPath } as Set
44
+ // Android source sets are classified by name below. Their test output
45
+ // providers may depend on ASM transforms that have not executed yet.
46
+ def testOutputs = androidId != null ? ([] as Set) :
47
+ (tests.collectMany { it.testClassesDirs.files.toList() }.collect { it.canonicalPath } as Set)
45
48
  def buildDir = p.layout.buildDirectory.get().asFile.canonicalFile.toPath()
46
49
  def pathOf = { File file ->
47
50
  def absolute = file.absoluteFile.toPath().normalize()
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities-script.js","sourceRoot":"","sources":["../../src/installer/capabilities-script.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,4EAA4E;AAC5E,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgFjD,CAAC"}
1
+ {"version":3,"file":"capabilities-script.js","sourceRoot":"","sources":["../../src/installer/capabilities-script.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,4EAA4E;AAC5E,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmFjD,CAAC"}
@@ -1,8 +1,8 @@
1
1
  export declare const ORCHESTRATOR_PACKAGE_NAME = "@frankzhang2026/opencode-android-orchestrator";
2
- export declare const ORCHESTRATOR_PACKAGE_VERSION = "1.2.0";
3
- export declare const ORCHESTRATOR_PLUGIN_REFERENCE = "@frankzhang2026/opencode-android-orchestrator@1.2.0";
2
+ export declare const ORCHESTRATOR_PACKAGE_VERSION = "1.2.1";
3
+ export declare const ORCHESTRATOR_PLUGIN_REFERENCE = "@frankzhang2026/opencode-android-orchestrator@1.2.1";
4
4
  export declare const OPENCODE_CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
5
- export declare const REQUIRED_PLUGIN_REFERENCES: readonly ["@frankzhang2026/opencode-android-orchestrator@1.2.0"];
5
+ export declare const REQUIRED_PLUGIN_REFERENCES: readonly ["@frankzhang2026/opencode-android-orchestrator@1.2.1"];
6
6
  export type OpenCodeConfigMergeErrorCode = "AMBIGUOUS_CONFIG" | "CONFIG_NOT_FILE" | "CONFIG_READ_FAILED" | "CONFIG_SYMLINK" | "DUPLICATE_PLUGIN" | "DUPLICATE_PROPERTY" | "INVALID_JSONC" | "INVALID_PLUGIN_ENTRY" | "PLUGIN_NOT_ARRAY" | "PLUGIN_VERSION_CONFLICT" | "ROOT_NOT_OBJECT" | "TARGET_NOT_DIRECTORY";
7
7
  export declare class OpenCodeConfigMergeError extends Error {
8
8
  readonly code: OpenCodeConfigMergeErrorCode;
@@ -2,7 +2,7 @@ import { lstatSync, readFileSync, statSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { applyEdits, findNodeAtLocation, getNodeValue, modify, parseTree, printParseErrorCode, } from "jsonc-parser";
4
4
  export const ORCHESTRATOR_PACKAGE_NAME = "@frankzhang2026/opencode-android-orchestrator";
5
- export const ORCHESTRATOR_PACKAGE_VERSION = "1.2.0";
5
+ export const ORCHESTRATOR_PACKAGE_VERSION = "1.2.1";
6
6
  export const ORCHESTRATOR_PLUGIN_REFERENCE = `${ORCHESTRATOR_PACKAGE_NAME}@${ORCHESTRATOR_PACKAGE_VERSION}`;
7
7
  export const OPENCODE_CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
8
8
  export const REQUIRED_PLUGIN_REFERENCES = [
@@ -1,7 +1,8 @@
1
1
  # Gradle 6.7.1 review fixes (1.2.0)
2
2
 
3
3
  These changes are included in 1.2.0 and are absent from
4
- 1.1.0. Upgrade with the pinned 1.2.0 package. Do not copy individual
4
+ 1.1.0. Use the pinned 1.2.1 package, including the lazy Android
5
+ test-output compatibility fix. Do not copy individual
5
6
  managed files into an existing installation or reuse evidence made with the
6
7
  previous collector. Keep existing evidence and approve a new task after upgrade.
7
8
 
package/docs/MIGRATION.md CHANGED
@@ -1,9 +1,18 @@
1
1
  # Migration guide
2
2
 
3
3
  This guide covers migration to
4
- `@frankzhang2026/opencode-android-orchestrator@1.2.0`. Pin the exact version and
4
+ `@frankzhang2026/opencode-android-orchestrator@1.2.1`. Pin the exact version and
5
5
  prove the migration in a disposable clone before changing a long-lived
6
- repository. Use the pinned Registry commands below.
6
+ repository.
7
+
8
+ ## 1.2.1 lazy test-output compatibility
9
+
10
+ Version 1.2.1 avoids reading Android test output providers during discovery and
11
+ collects test output metadata only when each test task is about to execute, after
12
+ its producer tasks have completed. This supports mapped outputs such as ASM test
13
+ class transforms. Finish or abort active work before upgrading, preserve old
14
+ evidence and approve new tasks with the updated collector. Use
15
+ `upgrade --refresh-gradle-discovery` to refresh the module and source model.
7
16
 
8
17
  ## 1.2.0 compatibility and evidence upgrade
9
18
 
@@ -23,10 +32,10 @@ protocol boundaries.
23
32
 
24
33
  | Current state | Correct command | Important distinction |
25
34
  | --- | --- | --- |
26
- | No orchestrator files or manifest | `npx @frankzhang2026/opencode-android-orchestrator@1.2.0 init .` | Normal new installation; all runtime-detected Android modules and registered debug verification tasks are discovered automatically. |
35
+ | No orchestrator files or manifest | `npx @frankzhang2026/opencode-android-orchestrator@1.2.1 init .` | Normal new installation; all runtime-detected Android modules and registered debug verification tasks are discovered automatically. |
27
36
  | Published `0.1.0` scaffold only | Remove any project-local `@0.1.0` plugin reference after review, then run `init`. | `0.1.0` did not create a usable managed installation and cannot be upgraded. |
28
- | `0.2.0` through `0.10.0` manifest-managed installation with intact managed/backup content | Run the `1.2.0` `upgrade`; add `--refresh-gradle-discovery` when generated module/task lists are incomplete. | Refresh replaces all derived module metadata, source paths, protected build files, and task allowlists from one Gradle runtime snapshot. Module scope, operator policies, user-owned AGENTS content, and an existing commit-prefix sidecar remain preserved. |
29
- | Healthy `1.0.0` through `1.1.0` installation | Stop the queue service, finish or abort retained workspaces, then run the fixed `1.2.0` `upgrade`. | Pending inbox contracts remain durable. Version 1.1.0 adds V4 test inventories, opt-in V5-V8 recovery/continuity and Android build capabilities; older approved contracts retain their authority. |
37
+ | `0.2.0` through `0.10.0` manifest-managed installation with intact managed/backup content | Run the `1.2.1` `upgrade`; add `--refresh-gradle-discovery` when generated module/task lists are incomplete. | Refresh replaces all derived module metadata, source paths, protected build files, and task allowlists from one Gradle runtime snapshot. Module scope, operator policies, user-owned AGENTS content, and an existing commit-prefix sidecar remain preserved. |
38
+ | Healthy `1.0.0` through `1.2.0` installation | Stop the queue service, finish or abort retained workspaces, then run the fixed `1.2.1` `upgrade`. | Pending inbox contracts remain durable. Version 1.1.0 adds V4 test inventories, opt-in V5-V8 recovery/continuity and Android build capabilities; older approved contracts retain their authority. |
30
39
  | Manually copied V3 files, no `.automation-plugin/manifest.json` | Finish active tasks, preserve historical evidence separately, then run `init`. | Exact files can be reused; differing managed files fail as conflicts. |
31
40
  | Healthy older manifest-managed installation | Run `doctor`, then the fixed target version's `upgrade`. | `upgrade` requires a valid installed manifest and intact original backups. |
32
41
  | Healthy current-version manifest | Run `doctor`; repeated `init` or same-version `upgrade` is verification-only and byte-idempotent. | Do not reinstall or delete the manifest. |
@@ -72,7 +81,7 @@ installation of `0.1.0` alone does not require project-file cleanup.
72
81
  Initialize with the fixed version:
73
82
 
74
83
  ```sh
75
- npx @frankzhang2026/opencode-android-orchestrator@1.2.0 init .
84
+ npx @frankzhang2026/opencode-android-orchestrator@1.2.1 init .
76
85
  ```
77
86
 
78
87
  New installations default to all-module scope, so multiple application modules
@@ -120,7 +129,7 @@ Use the lifecycle command selected by the active manifest:
120
129
 
121
130
  ```sh
122
131
  npx --yes --registry=https://registry.npmjs.org/ \
123
- @frankzhang2026/opencode-android-orchestrator@1.2.0 upgrade . --json
132
+ @frankzhang2026/opencode-android-orchestrator@1.2.1 upgrade . --json
124
133
  ```
125
134
 
126
135
  The command-level Registry option is useful when a company-wide npm Registry
@@ -150,7 +159,7 @@ computed includes dynamically or a company convention plugin applied
150
159
 
151
160
  ```sh
152
161
  npx --yes --registry=https://registry.npmjs.org/ \
153
- @frankzhang2026/opencode-android-orchestrator@1.2.0 upgrade . \
162
+ @frankzhang2026/opencode-android-orchestrator@1.2.1 upgrade . \
154
163
  --refresh-gradle-discovery --json
155
164
  ```
156
165
 
@@ -234,7 +243,7 @@ use the queue rather than the old direct Shell commands. See [Queue operation](Q
234
243
  Run all checks from the detected Git root:
235
244
 
236
245
  ```sh
237
- npx @frankzhang2026/opencode-android-orchestrator@1.2.0 doctor .
246
+ npx @frankzhang2026/opencode-android-orchestrator@1.2.1 doctor .
238
247
  opencode debug config
239
248
  opencode debug skill
240
249
  opencode debug agent scheduled-planner
@@ -339,7 +348,7 @@ Reviewer verification commands can retry identified environment failures within
339
348
  separate persisted budgets; model/provider calls and integration are unchanged.
340
349
  Recovery logic extends the existing managed recovery.cjs. Version 1.1.0 has
341
350
  52 managed resources, including the Android project capability validator.
342
- Upgrade with 1.2.0; same-version upgrade does not refresh
351
+ Upgrade with 1.2.1; same-version upgrade does not refresh
343
352
  managed templates. See the V7 policy and retained-evidence workflow in QUEUE.md.
344
353
 
345
354
 
package/docs/QUEUE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Queue and background execution
2
2
 
3
- Version `1.2.0` stores proposals and approved contracts under
3
+ Version `1.2.1` stores proposals and approved contracts under
4
4
  `<git-common-dir>/automation-runtime/inbox/queue.json`. A contract is runnable
5
5
  only after its full plan, version, digest, target branch and commit policy are
6
6
  approved and durably recorded. Planning reads a fixed `planningHead`, so another
@@ -134,7 +134,7 @@ It does not run `clean` or rerun all dependency tasks. Coder, Reviewer and local
134
134
  integration perform the configured full suite and build gates. Evidence records
135
135
  fresh-test logs, configured tasks and elapsed seconds.
136
136
 
137
- The 1.2.0 templates generate task-contract schema V4 with verification
137
+ The 1.2.1 templates generate task-contract schema V4 with verification
138
138
  version 2. The 1.0.5 package
139
139
  does not include this protocol. Every verification case
140
140
  has a stable ID, a one-based acceptance-criterion reference, an evidence source,
package/docs/SECURITY.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Security model
2
2
 
3
3
  This document describes the security properties of
4
- `@frankzhang2026/opencode-android-orchestrator@1.2.0`. The lifecycle foundation
4
+ `@frankzhang2026/opencode-android-orchestrator@1.2.1`. The lifecycle foundation
5
5
  completed the real OpenCode `1.14.22` and `1.15.13` release matrix in `0.2.0`;
6
6
  `1.0.0` retains that compatibility boundary.
7
7
 
@@ -1,7 +1,7 @@
1
1
  # Troubleshooting
2
2
 
3
3
  Use this guide for
4
- `@frankzhang2026/opencode-android-orchestrator@1.2.0`.
4
+ `@frankzhang2026/opencode-android-orchestrator@1.2.1`.
5
5
 
6
6
  ## Start with read-only evidence
7
7
 
@@ -11,7 +11,7 @@ From the repository root, capture:
11
11
  git status --short --branch
12
12
  git rev-parse HEAD
13
13
  opencode --version
14
- npx @frankzhang2026/opencode-android-orchestrator@1.2.0 doctor . --json
14
+ npx @frankzhang2026/opencode-android-orchestrator@1.2.1 doctor . --json
15
15
  ```
16
16
 
17
17
  If installation never completed, doctor will correctly report a missing or
@@ -51,9 +51,9 @@ command-scoped override:
51
51
 
52
52
  ```sh
53
53
  npm --registry=https://registry.npmjs.org/ view \
54
- @frankzhang2026/opencode-android-orchestrator@1.2.0 version
54
+ @frankzhang2026/opencode-android-orchestrator@1.2.1 version
55
55
  npx --yes --registry=https://registry.npmjs.org/ \
56
- @frankzhang2026/opencode-android-orchestrator@1.2.0 upgrade . --json
56
+ @frankzhang2026/opencode-android-orchestrator@1.2.1 upgrade . --json
57
57
  ```
58
58
 
59
59
  This leaves the company's saved npm configuration unchanged. Use the option
@@ -83,7 +83,7 @@ Git-backed Superpowers plugin at runtime.
83
83
  | Invalid `--long-command-timeout-ms` | The value is not an integer from `120000` through `7200000`. | Use the `1800000` ms default or pass an intentional bounded value to `init`/`upgrade`; do not edit the generated config directly. |
84
84
  | Android SDK failure | No valid explicit SDK, `ANDROID_HOME`, `ANDROID_SDK_ROOT`, or `local.properties` `sdk.dir` was found. | Configure one real SDK root containing `platforms/` and `build-tools/`. Do not publish `local.properties`. |
85
85
  | Missing `git`, `jq`, `rg`, `shasum`, or Java | Required deterministic command is unavailable on `PATH`. | Install or restore the missing command, record its version, and rerun the read-only checks. |
86
- | `Bundled Orchestrator skill is unavailable` | The installed `1.2.0` package is incomplete, damaged, or loaded from an unsupported partial copy. | Reinstall the exact package, inspect its `resources/third-party/superpowers-v6.2.0/skills/` entries, restart OpenCode, and rerun `opencode debug skill`. Do not add an external Superpowers plugin as a fallback. |
86
+ | `Bundled Orchestrator skill is unavailable` | The installed `1.2.1` package is incomplete, damaged, or loaded from an unsupported partial copy. | Reinstall the exact package, inspect its `resources/third-party/superpowers-v6.2.0/skills/` entries, restart OpenCode, and rerun `opencode debug skill`. Do not add an external Superpowers plugin as a fallback. |
87
87
  | `current process does not own this task queue execution` immediately after Coder start on 1.0.1 | OpenCode created the tool shell in a separate process group, so 1.0.1 rejected a legitimate Worker descendant. | Upgrade to 1.0.2 or later, restart OpenCode, then use the approved resume or abort workflow for the retained task. Do not edit the queue or lease files. |
88
88
  | The exact Superpowers v6.2.0 plugin remains after upgrade | That entry existed in the verified pre-install OpenCode file and is therefore user-owned. | Leave it in place or remove it as a separate reviewed configuration change. Upgrade only removes the old Orchestrator-managed entry. |
89
89
 
@@ -100,7 +100,7 @@ silence of `./gradlew tasks --all --console=plain | rg ...` in a large build.
100
100
  For an existing installation, run:
101
101
 
102
102
  ```sh
103
- npx @frankzhang2026/opencode-android-orchestrator@1.2.0 upgrade . \
103
+ npx @frankzhang2026/opencode-android-orchestrator@1.2.1 upgrade . \
104
104
  --refresh-gradle-discovery
105
105
  ```
106
106
 
@@ -109,7 +109,7 @@ least `1800000` milliseconds. A higher timeout already supplied by the caller
109
109
  is preserved; unrelated Bash commands are unchanged. To configure one hour,
110
110
  run `upgrade . --long-command-timeout-ms 3600000` on a healthy installation.
111
111
  If a command still reports `120000 ms`, confirm that the project manifest and
112
- OpenCode plugin reference are both `1.2.0`, restart the OpenCode session so the
112
+ OpenCode plugin reference are both `1.2.1`, restart the OpenCode session so the
113
113
  plugin reloads, and rerun doctor before attempting recovery.
114
114
 
115
115
  After installation, inspect OpenCode discovery separately:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frankzhang2026/opencode-android-orchestrator",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Reusable OpenCode orchestration for Android projects",
5
5
  "license": "MIT",
6
6
  "author": "frankzhang2026",
@@ -27,46 +27,56 @@ append([kind: 'start', phase: request.phase, gradleVersion: gradle.gradleVersion
27
27
  def filtersByTask = request.targets.groupBy { it.gradleTask }.collectEntries { path, targets ->
28
28
  [(path): targets.collect { it.filter }.unique().sort()]
29
29
  }
30
- gradle.taskGraph.whenReady { graph ->
31
- filtersByTask.each { path, filters ->
32
- def task = graph.allTasks.find { it.path == path }
33
- if (!(task instanceof Test)) throw new GradleException("Inventory target must resolve to an exact Test task: ${path}")
34
- def roots = [] as Set
35
- def classes = task.testClassesDirs.files.collect { it.canonicalFile } as Set
36
- def sourceSets = task.project.extensions.findByName('sourceSets')
37
- sourceSets?.each { sourceSet ->
38
- if (sourceSet.output.classesDirs.files.any { classes.contains(it.canonicalFile) }) {
39
- roots.addAll(sourceSet.allSource.srcDirs)
40
- roots.addAll(sourceSet.resources.srcDirs)
41
- }
30
+ // Keep dry-run source discovery independent of Android output providers.
31
+ // Real output metadata is collected after producer tasks have executed.
32
+ def recordTask = { task, boolean discovery ->
33
+ def path = task.path
34
+ def filters = filtersByTask[path]
35
+ def roots = [] as Set
36
+ def android = task.project.extensions.findByName('android')
37
+ // Dry-run discovery cannot execute producer tasks. Android source roots
38
+ // come from its DSL; class-output paths are recorded on the real run.
39
+ def classes = discovery && android != null ? ([] as Set) :
40
+ (task.testClassesDirs.files.collect { it.canonicalFile } as Set)
41
+ def sourceSets = android == null ? task.project.extensions.findByName('sourceSets') : null
42
+ sourceSets?.each { sourceSet ->
43
+ if (sourceSet.output.classesDirs.files.any { classes.contains(it.canonicalFile) }) {
44
+ roots.addAll(sourceSet.allSource.srcDirs)
45
+ roots.addAll(sourceSet.resources.srcDirs)
42
46
  }
43
- // AGP variants expose the source providers actually used by each unit test.
44
- def android = task.project.extensions.findByName('android')
45
- if (android != null && android.hasProperty('unitTestVariants')) {
46
- android.unitTestVariants.each { variant ->
47
- if (task.name == "test${variant.name.capitalize()}") {
48
- variant.sourceSets.each { provider ->
49
- roots.addAll(provider.javaDirectories)
50
- roots.addAll(provider.resourcesDirectories)
51
- if (provider.hasProperty('kotlinDirectories')) roots.addAll(provider.kotlinDirectories)
52
- }
47
+ }
48
+ // AGP variants expose the source providers actually used by each unit test.
49
+ if (android != null && android.hasProperty('unitTestVariants')) {
50
+ android.unitTestVariants.each { variant ->
51
+ if (task.name == "test${variant.name.capitalize()}") {
52
+ variant.sourceSets.each { provider ->
53
+ roots.addAll(provider.javaDirectories)
54
+ roots.addAll(provider.resourcesDirectories)
55
+ if (provider.hasProperty('kotlinDirectories')) roots.addAll(provider.kotlinDirectories)
53
56
  }
54
57
  }
55
58
  }
56
- // Modern AGP also exposes variant-specific test source sets through its DSL.
57
- // Only unit-test providers are classified here, never production providers.
58
- if (android != null && android.hasProperty('sourceSets')) {
59
- android.sourceSets.findAll { it.name == 'test' || it.name.startsWith('test') && it.name.length() > 4 && Character.isUpperCase(it.name.charAt(4)) }.each { sourceSet ->
60
- roots.addAll(sourceSet.java.srcDirs)
61
- roots.addAll(sourceSet.resources.srcDirs)
62
- if (sourceSet.hasProperty('kotlin')) roots.addAll(sourceSet.kotlin.srcDirs)
63
- roots.addAll(sourceSet.assets.srcDirs)
64
- roots.addAll(sourceSet.res.srcDirs)
65
- }
59
+ }
60
+ // Modern AGP also exposes variant-specific test source sets through its DSL.
61
+ // Only unit-test providers are classified here, never production providers.
62
+ if (android != null && android.hasProperty('sourceSets')) {
63
+ android.sourceSets.findAll { it.name == 'test' || it.name.startsWith('test') && it.name.length() > 4 && Character.isUpperCase(it.name.charAt(4)) }.each { sourceSet ->
64
+ roots.addAll(sourceSet.java.srcDirs)
65
+ roots.addAll(sourceSet.resources.srcDirs)
66
+ if (sourceSet.hasProperty('kotlin')) roots.addAll(sourceSet.kotlin.srcDirs)
67
+ roots.addAll(sourceSet.assets.srcDirs)
68
+ roots.addAll(sourceSet.res.srcDirs)
66
69
  }
67
- append([kind: 'task', taskPath: path, filters: filters,
68
- sourceRoots: roots.collect { it.absoluteFile.toPath().normalize().toString() }.sort(),
69
- testClassesDirs: classes.collect { it.toString() }.sort()])
70
+ }
71
+ append([kind: 'task', taskPath: path, filters: filters,
72
+ sourceRoots: roots.collect { it.absoluteFile.toPath().normalize().toString() }.sort(),
73
+ testClassesDirs: classes.collect { it.toString() }.sort()])
74
+ }
75
+ gradle.taskGraph.whenReady { graph ->
76
+ filtersByTask.each { path, filters ->
77
+ def task = graph.allTasks.find { it.path == path }
78
+ if (!(task instanceof Test)) throw new GradleException("Inventory target must resolve to an exact Test task: ${path}")
79
+ if (gradle.startParameter.dryRun) recordTask(task, true)
70
80
  task.outputs.upToDateWhen { false }
71
81
  task.outputs.doNotCacheIf('Orchestrator inventory requires actual test execution') { true }
72
82
  task.ignoreFailures = true
@@ -97,6 +107,10 @@ gradle.taskGraph.whenReady { graph ->
97
107
  })
98
108
  }
99
109
  }
110
+ // beforeTask also records skipped/NO-SOURCE tasks for inventory validation.
111
+ gradle.taskGraph.beforeTask { task ->
112
+ if (filtersByTask.containsKey(task.path)) recordTask(task, false)
113
+ }
100
114
  gradle.taskGraph.afterTask { task, state ->
101
115
  if (filtersByTask.containsKey(task.path)) append([kind: 'taskEnd', taskPath: task.path,
102
116
  executed: state.executed, skipped: state.skipped, noSource: state.noSource,