Skip to content

feat(orchestrator): test workflow engine — suite definitions, taxonomy filters, structured results#790

Closed
frostebite wants to merge 5 commits intomainfrom
feature/test-workflow-engine
Closed

feat(orchestrator): test workflow engine — suite definitions, taxonomy filters, structured results#790
frostebite wants to merge 5 commits intomainfrom
feature/test-workflow-engine

Conversation

@frostebite
Copy link
Member

@frostebite frostebite commented Mar 5, 2026

Summary

Adds a fully implemented test workflow engine to the Orchestrator module. This enables YAML-based test suite definitions with multi-dimensional taxonomy filtering, structured result reporting, and sequential execution dependencies. Modeled on battle-tested patterns from production Unity projects with 300+ tests across multiple suites.

Implementation

Component Description
TestSuiteParser Parses YAML suite definitions with ordered runs and needs: dependencies. Uses Kahn's topological sort for correct execution ordering.
TaxonomyFilterService 7 built-in taxonomy dimensions (Scope, Maturity, FeedbackSpeed, Rigor, Determinism, Execution, IsolationLevel) with regex pattern matching and hierarchical dot-notation support.
TestResultReporter Parses JUnit XML and Unity JSON test output formats. Generates per-suite and per-run result aggregation with timing data. Produces GitHub-compatible Markdown summary.
TestWorkflowService Orchestrates the full test workflow: suite loading, taxonomy filtering, execution entry via workflow inputs, and multi-format result output.

New action.yml inputs (5)

Input Purpose
testSuitePath Path to YAML test suite definition file
testSuiteEvent CI event name for suite selection (pr, push, release)
testTaxonomyPath Path to custom taxonomy definition YAML
testResultFormat Test result output format: junit, json, or both
testResultPath Path for test result output files

Example suite definition

name: pull-request
event: pr
runs:
  - name: fast-feedback
    mode: EditMode
    taxonomy:
      FeedbackSpeed: Fast
      Maturity: Trusted
    timeout: 300
  - name: integration
    mode: PlayMode
    needs: [fast-feedback]
    taxonomy:
      Scope: Integration
      Rigor: Standard
    timeout: 600

Test coverage

43 unit tests covering:

  • YAML suite parsing and validation
  • Topological sort for run dependencies
  • Taxonomy filter matching (exact, regex, hierarchical)
  • JUnit XML and Unity JSON result parsing
  • Markdown summary generation
  • Full orchestration workflow

Related Issues

Documentation

Test plan

  • All 43 new tests pass
  • All existing tests pass — no regressions
  • tsc --noEmit — no type errors
  • CI builds on push

Summary by CodeRabbit

  • New Features

    • Test workflow engine: YAML test suite definitions with dependency resolution, parallel execution, taxonomy filtering, and test orchestration.
    • New inputs to specify test suite, taxonomy, result format, and result output path.
    • Test reporting: aggregated results and multi-format outputs (JUnit, JSON, Markdown).
  • Documentation

    • Added README documenting the Test Workflow Engine and specification reference.
  • Chores

    • CI/workflow updates: mac job now continues on error and workflows favor the main branch as the source.

Tracking:

Initial scaffold for the test workflow engine service directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@frostebite frostebite added enhancement New feature or request orchestrator Orchestrator module labels Mar 5, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 5, 2026

📝 Walkthrough

Walkthrough

Adds a Test Workflow Engine: new action inputs and BuildParameters, a test-workflow module (types, parser, taxonomy filter, result reporter, orchestrator service), unit tests and README, and an early-exit path in the CLI to execute YAML test suites and emit structured results.

Changes

Cohort / File(s) Summary
Action inputs
action.yml
Added five optional inputs: testSuitePath, testSuiteEvent, testTaxonomyPath, testResultFormat (default junit), testResultPath (default ./test-results). Minor formatting tweak to customJob description.
Parameter plumbing
src/model/input.ts, src/model/build-parameters.ts
Added Input getters and corresponding BuildParameters fields for the five new test-related inputs; defaults applied for format and path.
CLI entrypoint
src/index.ts
Imported TestWorkflowService and added an early-exit flow: if testSuitePath is set, run the test suite, aggregate failures, mark build as failed on non-zero failures, and return.
Public API & types
src/model/orchestrator/services/test-workflow/index.ts, src/model/orchestrator/services/test-workflow/test-workflow-types.ts
New barrel export and type definitions for test suites, runs, taxonomy, and TestResult/TestFailure shapes.
Test suite parsing & ordering
src/model/orchestrator/services/test-workflow/test-suite-parser.ts
New YAML parser and validator that normalizes runs, validates fields and dependencies, detects cycles, and resolves parallel execution groups via topological sort.
Taxonomy filtering
src/model/orchestrator/services/test-workflow/taxonomy-filter-service.ts
New service to load built-in + custom taxonomy definitions, build Unity --testFilter args, and match test metadata against multi-dimension filters (CSV, regex, hierarchical prefixes).
Result reporting
src/model/orchestrator/services/test-workflow/test-result-reporter.ts
New reporter to parse JUnit XML and Unity JSON, normalize TestResult shape, extract failures, generate Markdown summary, and write JSON/XML outputs.
Orchestration service
src/model/orchestrator/services/test-workflow/test-workflow-service.ts
New service to execute suites and runs: build Unity CLI args, run Unity with timeouts, parse/write results, run groups sequentially with concurrent runs inside groups, and aggregate results.
Tests & docs
src/model/orchestrator/services/test-workflow/test-workflow.test.ts, src/model/orchestrator/services/test/README.md
Comprehensive unit tests for parser, taxonomy filter, reporter, and workflow; README documenting the Test Workflow Engine and workflow behavior.
Workflows & branch logic
.github/workflows/*, src/model/orchestrator/*
Changed CI/workflow branch fallbacks to prefer main and continue-on-error for macOS job; updated some test overrides to use main instead of orchestrator-develop.

Sequence Diagram(s)

sequenceDiagram
  participant CI as CI Action (index.ts)
  participant Parser as TestSuiteParser
  participant Orchestrator as TestWorkflowService
  participant Unity as Unity Engine
  participant Reporter as TestResultReporter
  participant FS as Filesystem

  CI->>Orchestrator: executeTestSuite(suitePath, parameters)
  Orchestrator->>Parser: parseSuiteFile(suitePath)
  Parser-->>Orchestrator: TestSuiteDefinition + run groups
  Orchestrator->>Orchestrator: iterate groups (sequential)
  Orchestrator->>Unity: spawn process with buildUnityArgs(run)
  Unity-->>Reporter: emit test result (XML/JSON)
  Reporter->>FS: writeResults(results, outputPath, format)
  Reporter-->>Orchestrator: TestResult
  Orchestrator-->>CI: aggregated results (failures count)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #788 — Implements the Test Workflow Engine objectives: suite definitions, taxonomy filters, and structured results (matches new module and inputs).
  • #795 — Related tracking for Next-Gen features; changes implement the test-workflow items referenced there.

Possibly related PRs

Suggested reviewers

  • webbertakken
  • GabLeRoux
  • AndrewKahr

Poem

🐰 I hopped through YAML, filters, and runs,
I sorted needs beneath CI suns.
I spawn the engine, collect every test,
Write results to files so teams can rest.
Hooray — the Test Engine hops into the sun!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly describes the main feature added: a test workflow engine with suite definitions, taxonomy filters, and structured result reporting.
Description check ✅ Passed PR description is comprehensive, covering implementation details, new inputs, example YAML, test coverage, and related issues. Follows template structure.
Linked Issues check ✅ Passed All major objectives from issue #788 are implemented: YAML suite definitions with dependency resolution, multi-dimensional taxonomy filtering, multiple execution modes, structured result parsing/reporting, and new action inputs.
Out of Scope Changes check ✅ Passed Changes to build workflows and branch references align with Orchestrator evolution. Test workflow implementation stays focused on suite parsing, taxonomy filtering, and result reporting.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/test-workflow-engine

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/model/orchestrator/services/test/README.md (1)

1-5: Consider expanding this scaffold README with a minimal contract section.

Since this is the only in-repo doc for the new engine, adding a short “Current status / Not yet implemented / Planned inputs-outputs” section would prevent misuse and set expectations for draft behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test/README.md` around lines 1 - 5, Add a
short "Contract" section under the "Test Workflow Engine" README (e.g., a header
like "Current status / Not yet implemented / Planned inputs-outputs") that
clearly lists the current implementation status, which features are
unimplemented, and the minimal expected inputs and outputs (YAML test suite
definition shape, taxonomy filter format, and the structure of test results).
Keep it concise: one bullet for status, one for inputs (fields/types), one for
outputs (fields/types), and one for planned extensions so consumers know what to
rely on when using the draft engine.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/model/orchestrator/services/test/README.md`:
- Line 5: Replace the ambiguous line "See GitHub Issue for full specification."
with a direct reference to the issue by appending the issue number or full URL
(for example replace that exact string with "See GitHub Issue `#788`" or "See
https://github.com/<owner>/<repo>/issues/788" as appropriate) so readers can
navigate directly to the spec; edit the README.md entry containing "See GitHub
Issue for full specification." to include that issue identifier.

---

Nitpick comments:
In `@src/model/orchestrator/services/test/README.md`:
- Around line 1-5: Add a short "Contract" section under the "Test Workflow
Engine" README (e.g., a header like "Current status / Not yet implemented /
Planned inputs-outputs") that clearly lists the current implementation status,
which features are unimplemented, and the minimal expected inputs and outputs
(YAML test suite definition shape, taxonomy filter format, and the structure of
test results). Keep it concise: one bullet for status, one for inputs
(fields/types), one for outputs (fields/types), and one for planned extensions
so consumers know what to rely on when using the draft engine.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9736fe28-d54e-4745-9fa7-fc713706ef09

📥 Commits

Reviewing files that changed from the base of the PR and between 9d47543 and 8d81236.

📒 Files selected for processing (1)
  • src/model/orchestrator/services/test/README.md


Service for YAML-based test suite definitions, taxonomy filtering, and structured test results.

See GitHub Issue for full specification.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a direct issue reference/link.

“See GitHub Issue” is ambiguous without the issue number/URL, so readers can’t quickly navigate to the spec. Please link the exact issue (e.g., #788) in this line.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test/README.md` at line 5, Replace the
ambiguous line "See GitHub Issue for full specification." with a direct
reference to the issue by appending the issue number or full URL (for example
replace that exact string with "See GitHub Issue `#788`" or "See
https://github.com/<owner>/<repo>/issues/788" as appropriate) so readers can
navigate directly to the spec; edit the README.md entry containing "See GitHub
Issue for full specification." to include that issue identifier.

…omy filtering, and structured results (#790)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link

github-actions bot commented Mar 5, 2026

Cat Gif

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (6)
src/model/orchestrator/services/test-workflow/test-suite-parser.ts (3)

87-95: Use TypeError for type validation errors.

Similar to the needs check, use TypeError when validating that filters is a key-value object.

♻️ Suggested fix
     if (raw.filters !== undefined) {
       if (typeof raw.filters !== 'object' || Array.isArray(raw.filters)) {
-        throw new Error(`Run '${raw.name}': 'filters' must be a key-value object`);
+        throw new TypeError(`Run '${raw.name}': 'filters' must be a key-value object`);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test-workflow/test-suite-parser.ts` around
lines 87 - 95, The validation for raw.filters currently throws a generic Error;
change it to throw a TypeError to match the needs check style — i.e., where you
detect that raw.filters is not an object or is an array, throw a TypeError with
the same message (e.g., in the block handling raw.filters and assigning
run.filters); keep the rest of the logic (initializing run.filters and copying
entries) unchanged.

222-262: Consider using undefined instead of null for consistency.

The project's ESLint configuration (unicorn/no-null) prefers undefined over null. This affects the return type and several return statements in detectCircularDependencies.

♻️ Suggested fix
-  private static detectCircularDependencies(suite: TestSuiteDefinition): string | null {
+  private static detectCircularDependencies(suite: TestSuiteDefinition): string | undefined {
     const adjacency = new Map<string, string[]>();
     for (const run of suite.runs) {
       adjacency.set(run.name, run.needs ?? []);
     }

     const visited = new Set<string>();
     const visiting = new Set<string>();

-    const dfs = (node: string, path: string[]): string | null => {
+    const dfs = (node: string, path: string[]): string | undefined => {
       if (visiting.has(node)) {
         const cycleStart = path.indexOf(node);
         const cycle = path.slice(cycleStart).concat(node);
+
         return `Circular dependency: ${cycle.join(' -> ')}`;
       }
       if (visited.has(node)) {
-        return null;
+        return undefined;
       }
       // ... rest of function
       visiting.delete(node);
       visited.add(node);
-      return null;
+
+      return undefined;
     };

     for (const run of suite.runs) {
       const result = dfs(run.name, []);
       if (result) return result;
     }

-    return null;
+    return undefined;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test-workflow/test-suite-parser.ts` around
lines 222 - 262, The detectCircularDependencies function currently returns null
in several places; change its return type to string | undefined and update the
inner dfs function signature to return string | undefined, replacing all "return
null" with "return undefined" (and any callers that explicitly compare to null
to handle undefined or use falsy checks). Update the final return to return
undefined instead of null and ensure any code that calls
detectCircularDependencies (or checks dfs results) is adjusted accordingly to
accept undefined.

64-69: Use TypeError for type validation errors.

Per ESLint unicorn/prefer-type-error, when throwing errors for type checks (checking if needs is an array), use TypeError instead of the generic Error.

♻️ Suggested fix
     if (raw.needs !== undefined) {
       if (!Array.isArray(raw.needs)) {
-        throw new Error(`Run '${raw.name}': 'needs' must be an array of strings`);
+        throw new TypeError(`Run '${raw.name}': 'needs' must be an array of strings`);
       }
       run.needs = raw.needs;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test-workflow/test-suite-parser.ts` around
lines 64 - 69, The code in test-suite-parser.ts currently throws a generic Error
when raw.needs fails the array type check; change that to throw a TypeError to
satisfy the unicorn/prefer-type-error rule—replace the throw new Error(...) in
the block that checks Array.isArray(raw.needs) with throw new TypeError(...) and
keep the same message, leaving the surrounding logic that assigns run.needs =
raw.needs intact.
src/model/orchestrator/services/test-workflow/test-workflow-types.ts (1)

23-25: Use camelCase for the interface property name.

The property extensible_groups uses snake_case which violates TypeScript naming conventions. Consider renaming to extensibleGroups and handling the YAML field name mapping during parsing.

♻️ Suggested fix
 export interface TaxonomyDefinition {
-  extensible_groups: TaxonomyDimension[];
+  extensibleGroups: TaxonomyDimension[];
 }

Then update the parser to map from extensible_groups in YAML to extensibleGroups in the typed object.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test-workflow/test-workflow-types.ts` around
lines 23 - 25, Rename the TaxonomyDefinition interface property
extensible_groups to camelCase extensibleGroups and update any references to
TaxonomyDefinition to use extensibleGroups; then modify the YAML parsing/mapping
logic (where the YAML is deserialized into the typed object) to map the incoming
extensible_groups field to extensibleGroups so external snake_case remains
supported while the TypeScript model uses camelCase.
src/model/orchestrator/services/test-workflow/test-workflow.test.ts (1)

173-220: Consider renaming the callback parameter for clarity.

Per ESLint unicorn/prevent-abbreviations, the variable e in the filter callbacks should be renamed to error for clarity.

♻️ Suggested fix
-      expect(errors.some((e) => e.includes('Duplicate'))).toBe(true);
+      expect(errors.some((error) => error.includes('Duplicate'))).toBe(true);

Apply similarly to lines 207 and 217.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test-workflow/test-workflow.test.ts` around
lines 173 - 220, Rename the short callback parameter `e` to `error` in the .some
filter callbacks that inspect the `errors` array in the test cases for
TestSuiteParser.validateSuite (the expectations that currently use
errors.some((e) => ...)); update both occurrences checking 'Duplicate' and
'editMode' (and the one checking 'depends on itself' if present) so the callback
reads errors.some((error) => error.includes(...)) to satisfy the
unicorn/prevent-abbreviations rule and improve clarity.
src/model/orchestrator/services/test-workflow/test-result-reporter.ts (1)

27-52: Regex-based XML parsing is fragile; consider an XML parser for production.

The regex patterns assume well-formed XML with standard double-quoted attributes. Edge cases like escaped quotes inside attribute values, single-quoted attributes, or malformed XML could cause incorrect parsing.

For a scaffold this is acceptable, but consider using a lightweight XML parser (e.g., fast-xml-parser) before production release.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test-workflow/test-result-reporter.ts` around
lines 27 - 52, Replace the fragile regex-based parsing in parseJUnitXml with a
real XML parser (e.g., fast-xml-parser or DOMParser): parse xmlContent into an
object, locate the testsuite element, and read its attributes to set runName,
totalTests, failureCount, skippedCount, and duration (preserve existing fallback
defaults). Ensure the parser handles both single- and double-quoted attributes
and escaped characters, and keep the TestResult shape returned by parseJUnitXml
unchanged for callers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/index.ts`:
- Around line 21-36: When the test workflow path is taken
(buildParameters.testSuitePath and TestWorkflowService.executeTestSuite), the
function currently returns without setting CI outputs; call
Output.setBuildVersion and Output.setAndroidVersionCode with appropriate
sentinel or empty values and set Output.setEngineExitCode to 0 on all-passing
tests or a non-zero sentinel (e.g., 1) when totalFailed > 0 before returning, so
downstream steps always have defined outputs; update the code around the
TestWorkflowService.executeTestSuite block to compute totalFailed then set these
three outputs accordingly prior to the early return.

In `@src/model/orchestrator/services/test-workflow/taxonomy-filter-service.ts`:
- Line 30: The code copies TaxonomyFilterService.BUILT_IN_DIMENSIONS shallowly
causing mutations of inner objects when merging custom dimensions; in
loadTaxonomy, avoid mutating BUILT_IN_DIMENSIONS by deep-cloning its entries
before merging (e.g., create a new dimensions array from
TaxonomyFilterService.BUILT_IN_DIMENSIONS where each dimension object and its
values array are copied), and when merging custom dimensions replace
existing.values with a new array (or create a new merged object) instead of
calling existing.values.push(value) so built-in state is never mutated across
calls.
- Around line 160-169: The RegExp construction in the taxonomy filter block (the
trimmed.startsWith('/') && trimmed.endsWith('/') branch) uses untrusted input
and is vulnerable to ReDoS; replace unsafe RegExp usage by either: 1) switching
to a linear-time regex engine such as the re2 package (create an RE2 instance
instead of new RegExp for the pattern), or 2) if re2 cannot be introduced,
enforce strict runtime protections by (a) validating and bounding the pattern
length (reject patterns above a safe threshold), and (b) executing the regex
test off the main thread with a timeout (e.g., a Worker/worker_threads task that
returns false on timeout) to avoid blocking the event loop; update the catch
behavior to treat rejected/timeout cases as literal matches and surface a debug
log message for rejected patterns.

In `@src/model/orchestrator/services/test-workflow/test-result-reporter.ts`:
- Around line 278-282: The CDATA block written in TestResultReporter (where
lines.push is used to output failure.stackTrace) can be broken if
failure.stackTrace contains "]]>"; update the reporter to sanitize/split the
stack trace before wrapping in <![CDATA[...]]> by replacing every occurrence of
"]]>" with "]]]]><![CDATA[>" (or otherwise escape/split the terminator) so the
CDATA section remains valid; ensure this transformation is applied to the value
used in the lines.push call that currently writes
`<![CDATA[${failure.stackTrace}]]>` and keep using TestResultReporter.escapeXml
for other XML content as appropriate.

In `@src/model/orchestrator/services/test-workflow/test-suite-parser.ts`:
- Around line 64-69: The needs array is assigned without element type checks;
update the validation in the block that checks raw.needs (the code referencing
raw.name and assigning run.needs) to ensure every element is a string: iterate
over raw.needs, confirm typeof item === 'string' for each entry, and if any
element fails, throw a descriptive Error including the run name and the
offending index/value (e.g., "Run '...': 'needs' element at index X must be a
string"). Only assign run.needs = raw.needs after all elements pass validation.

In `@src/model/orchestrator/services/test-workflow/test-workflow-service.ts`:
- Around line 43-44: The group concurrency is being defeated because
TestWorkflowService.executeTestRun uses synchronous child process calls
(execSync) while callers use Promise.all; replace execSync inside executeTestRun
with an async non-blocking alternative (e.g., promisified child_process.exec or
a spawn wrapped in a Promise) so executeTestRun truly returns a Promise that
resolves/rejects based on the child process exit, capturing stdout/stderr, exit
code, and errors; update any related timeout/cleanup logic to use the async
handle and ensure callers using Promise.all can run runs in parallel.
- Around line 188-199: When both run.editMode and run.playMode are true, the
current branch only configures EditMode and silently skips PlayMode; update
test-workflow-service to perform two sequential Unity invocations instead: build
args for EditMode (as currently done) and call executeTestRun/executeTestSuite
for that run, then build args for PlayMode (with '-runTests' and '-testPlatform
PlayMode') and call executeTestRun/executeTestSuite again; ensure you
propagate/aggregate results or errors from both invocations (e.g., fail the
overall run if either invocation fails) and reference the existing run object
and functions executeTestRun and executeTestSuite when locating where to add the
second invocation and result handling.
- Around line 227-237: The path builder uses params.editorVersion directly and
can produce "undefined" in returned paths; validate params.editorVersion at the
start of the routine (the function that returns platform-specific Unity paths)
and either throw a clear error or substitute a sensible fallback before
constructing the paths. Specifically, check params.editorVersion for
undefined/empty and handle it prior to the win32/darwin/Linux returns so
Unity.exe/Unity.app/Unity path strings are never built with "undefined" — update
the function that contains the platform branch (references to
params.editorVersion and the three return statements for Windows, macOS, and
Linux) to perform this validation and return/raise accordingly.
- Around line 63-65: The code unsafely asserts params.testResultFormat when
calling TestResultReporter.writeResults; instead validate
params.testResultFormat against allowed values ('junit','json','both') and
map/normalize it to a safe union before calling writeResults. In the block
around resultFormat/resultPath, replace the assertion with a small validation:
read params.testResultFormat into resultFormat, if it matches one of the allowed
strings use it, otherwise set a sensible default (e.g., 'both'), then pass that
validated value to TestResultReporter.writeResults.
- Around line 92-103: The command string built in TestWorkflowService (using
unityPath, args and resultFile) is vulnerable to shell injection because
execSync runs via the shell; instead, replace the execSync(command, ...) call
with execFileSync (or execFile) and pass unityPath as the file and the
individual arguments (including the test result path derived from
params.testResultPath/run.name) as an array so special characters are not
interpreted by a shell; keep the same options (timeout, cwd, stdio, encoding)
when invoking execFileSync and continue using
TestWorkflowService.resolveUnityPath to resolve the binary path.

---

Nitpick comments:
In `@src/model/orchestrator/services/test-workflow/test-result-reporter.ts`:
- Around line 27-52: Replace the fragile regex-based parsing in parseJUnitXml
with a real XML parser (e.g., fast-xml-parser or DOMParser): parse xmlContent
into an object, locate the testsuite element, and read its attributes to set
runName, totalTests, failureCount, skippedCount, and duration (preserve existing
fallback defaults). Ensure the parser handles both single- and double-quoted
attributes and escaped characters, and keep the TestResult shape returned by
parseJUnitXml unchanged for callers.

In `@src/model/orchestrator/services/test-workflow/test-suite-parser.ts`:
- Around line 87-95: The validation for raw.filters currently throws a generic
Error; change it to throw a TypeError to match the needs check style — i.e.,
where you detect that raw.filters is not an object or is an array, throw a
TypeError with the same message (e.g., in the block handling raw.filters and
assigning run.filters); keep the rest of the logic (initializing run.filters and
copying entries) unchanged.
- Around line 222-262: The detectCircularDependencies function currently returns
null in several places; change its return type to string | undefined and update
the inner dfs function signature to return string | undefined, replacing all
"return null" with "return undefined" (and any callers that explicitly compare
to null to handle undefined or use falsy checks). Update the final return to
return undefined instead of null and ensure any code that calls
detectCircularDependencies (or checks dfs results) is adjusted accordingly to
accept undefined.
- Around line 64-69: The code in test-suite-parser.ts currently throws a generic
Error when raw.needs fails the array type check; change that to throw a
TypeError to satisfy the unicorn/prefer-type-error rule—replace the throw new
Error(...) in the block that checks Array.isArray(raw.needs) with throw new
TypeError(...) and keep the same message, leaving the surrounding logic that
assigns run.needs = raw.needs intact.

In `@src/model/orchestrator/services/test-workflow/test-workflow-types.ts`:
- Around line 23-25: Rename the TaxonomyDefinition interface property
extensible_groups to camelCase extensibleGroups and update any references to
TaxonomyDefinition to use extensibleGroups; then modify the YAML parsing/mapping
logic (where the YAML is deserialized into the typed object) to map the incoming
extensible_groups field to extensibleGroups so external snake_case remains
supported while the TypeScript model uses camelCase.

In `@src/model/orchestrator/services/test-workflow/test-workflow.test.ts`:
- Around line 173-220: Rename the short callback parameter `e` to `error` in the
.some filter callbacks that inspect the `errors` array in the test cases for
TestSuiteParser.validateSuite (the expectations that currently use
errors.some((e) => ...)); update both occurrences checking 'Duplicate' and
'editMode' (and the one checking 'depends on itself' if present) so the callback
reads errors.some((error) => error.includes(...)) to satisfy the
unicorn/prevent-abbreviations rule and improve clarity.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e32772fd-3f03-43a9-b322-58f7262ac3f1

📥 Commits

Reviewing files that changed from the base of the PR and between 8d81236 and 1186717.

⛔ Files ignored due to path filters (2)
  • dist/index.js is excluded by !**/dist/**
  • dist/index.js.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (11)
  • action.yml
  • src/index.ts
  • src/model/build-parameters.ts
  • src/model/input.ts
  • src/model/orchestrator/services/test-workflow/index.ts
  • src/model/orchestrator/services/test-workflow/taxonomy-filter-service.ts
  • src/model/orchestrator/services/test-workflow/test-result-reporter.ts
  • src/model/orchestrator/services/test-workflow/test-suite-parser.ts
  • src/model/orchestrator/services/test-workflow/test-workflow-service.ts
  • src/model/orchestrator/services/test-workflow/test-workflow-types.ts
  • src/model/orchestrator/services/test-workflow/test-workflow.test.ts

Comment on lines +21 to +36

// If a test suite path is provided, use the test workflow engine
// instead of the standard build execution path
if (buildParameters.testSuitePath) {
core.info('[TestWorkflow] Test suite path detected, using test workflow engine');
const results = await TestWorkflowService.executeTestSuite(buildParameters.testSuitePath, buildParameters);

const totalFailed = results.reduce((sum, r) => sum + r.failed, 0);
if (totalFailed > 0) {
core.setFailed(`Test workflow completed with ${totalFailed} failure(s)`);
} else {
core.info('[TestWorkflow] All test runs passed');
}

return;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider setting outputs for consistency with the build path.

The test workflow path doesn't call Output.setBuildVersion, Output.setAndroidVersionCode, or Output.setEngineExitCode before returning. If downstream workflows depend on these outputs, they'll be undefined when using the test workflow path.

If this is intentional (test mode doesn't produce builds), consider documenting this or setting a sentinel value for engineExitCode based on test results.

💡 Optional: Set exit code output for consistency
       const totalFailed = results.reduce((sum, r) => sum + r.failed, 0);
       if (totalFailed > 0) {
         core.setFailed(`Test workflow completed with ${totalFailed} failure(s)`);
       } else {
         core.info('[TestWorkflow] All test runs passed');
       }
+
+      // Set exit code output for workflow consistency
+      await Output.setEngineExitCode(totalFailed > 0 ? 1 : 0);

       return;
     }
🧰 Tools
🪛 ESLint

[error] 28-28: Array#reduce() is not allowed

(unicorn/no-array-reduce)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 21 - 36, When the test workflow path is taken
(buildParameters.testSuitePath and TestWorkflowService.executeTestSuite), the
function currently returns without setting CI outputs; call
Output.setBuildVersion and Output.setAndroidVersionCode with appropriate
sentinel or empty values and set Output.setEngineExitCode to 0 on all-passing
tests or a non-zero sentinel (e.g., 1) when totalFailed > 0 before returning, so
downstream steps always have defined outputs; update the code around the
TestWorkflowService.executeTestSuite block to compute totalFailed then set these
three outputs accordingly prior to the early return.

* from an optional taxonomy file.
*/
static loadTaxonomy(filePath?: string): TaxonomyDimension[] {
const dimensions = [...TaxonomyFilterService.BUILT_IN_DIMENSIONS];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Mutation of BUILT_IN_DIMENSIONS inner arrays corrupts state across calls.

The BUILT_IN_DIMENSIONS array is copied shallowly on line 30, but when merging custom dimensions with existing built-in ones (lines 41-47), existing.values.push(value) mutates the original BUILT_IN_DIMENSIONS entries. Since readonly only prevents reassignment of the array reference (not mutation of contained objects), subsequent calls to loadTaxonomy will see the accumulated values from all previous calls.

🐛 Proposed fix: deep-copy dimensions to avoid mutation
 static loadTaxonomy(filePath?: string): TaxonomyDimension[] {
-  const dimensions = [...TaxonomyFilterService.BUILT_IN_DIMENSIONS];
+  const dimensions = TaxonomyFilterService.BUILT_IN_DIMENSIONS.map((d) => ({
+    name: d.name,
+    values: [...d.values],
+  }));

   if (filePath && fs.existsSync(filePath)) {

Also applies to: 37-47

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test-workflow/taxonomy-filter-service.ts` at
line 30, The code copies TaxonomyFilterService.BUILT_IN_DIMENSIONS shallowly
causing mutations of inner objects when merging custom dimensions; in
loadTaxonomy, avoid mutating BUILT_IN_DIMENSIONS by deep-cloning its entries
before merging (e.g., create a new dimensions array from
TaxonomyFilterService.BUILT_IN_DIMENSIONS where each dimension object and its
values array are copied), and when merging custom dimensions replace
existing.values with a new array (or create a new merged object) instead of
calling existing.values.push(value) so built-in state is never mutated across
calls.

Comment on lines +160 to +169
if (trimmed.startsWith('/') && trimmed.endsWith('/') && trimmed.length > 2) {
const pattern = trimmed.slice(1, -1);
try {
const regex = new RegExp(pattern);
return regex.test(testValue);
} catch {
// Invalid regex, treat as literal
return testValue === trimmed;
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the file to understand the full context
cat -n src/model/orchestrator/services/test-workflow/taxonomy-filter-service.ts | sed -n '140,180p'

Repository: game-ci/unity-builder

Length of output: 1484


🏁 Script executed:

# Search for the function containing this code to understand data flow
ast-grep --pattern 'function $name($$$) {
  $$$
  if (trimmed.startsWith("/")) {
    $$$
  }
  $$$
}'

Repository: game-ci/unity-builder

Length of output: 47


🌐 Web query:

ReDoS regex denial of service Node.js safe-regex library validation 2024 2025

💡 Result:

  • What safe-regex actually validates (Node.js/JS ReDoS): it’s a heuristic checker that flags potentially catastrophic backtracking by analyzing the regex’s structure—specifically it “limit[s] the star height to 1” and (optionally) caps total repetitions via opts.limit (default 25). It returns false for invalid regex strings. It does not measure runtime on attacker-crafted inputs. [1]
  • Accuracy in practice: the maintainer explicitly warns the module has both false positives and false negatives, and recommends vuln-regex-detector for improved accuracy. [1]
  • 2024–2025 maintenance reality: safe-regex’s latest version is 2.1.1 and it was last updated/published ~6 years ago (i.e., not actively maintained through 2024–2025), even though it remains widely depended upon. [2]
  • If you’re using regex for untrusted-input validation in Node.js (2024–2025 guidance):
    • Treat safe-regex as a coarse lint, not a security guarantee; confirm reachability by untrusted input and add input length limits/timeouts where possible. [1]
    • Prefer linear-time engines (e.g., RE2 bindings) where compatible; Node’s default regex engine is the built-in backtracking engine, and switching to a linear-time engine is a common mitigation recommended by security guidance. [1][3]
  • If you need an alternative package: safe-regex2 exists and is actively versioned (e.g., listed with a current “latest” release by third-party package intel), but it’s still in the same general space of “detection” rather than a guaranteed defense. [4]

Sources:
[1] davisjam/safe-regex README (GitHub) (github.com)
[2] Snyk package page for safe-regex (version/last updated) (security.snyk.io)
[3] Ajv security notes on regex/ReDoS + linear-time engines (ajv.js.org)
[4] Aikido Intel package page for safe-regex2 (latest version listing) (intel.aikido.dev)

Citations:


🌐 Web query:

Node.js timeout regex execution catastrophic backtracking prevention

💡 Result:

Practical ways to prevent catastrophic backtracking (ReDoS) in Node.js

  1. Prefer a linear-time regex engine for untrusted input
  • Use RE2 bindings (no backtracking → avoids catastrophic backtracking by design): the re2 npm package (“node-re2”). It’s commonly recommended specifically to mitigate ReDoS in Node.js. [1]
  1. Enable V8’s non-backtracking fallback / linear engine (where available)
  • V8 added an experimental non-backtracking RegExp engine and exposes controls such as:
    • --regexp-backtracks-before-fallback=N (fallback triggers after “excessive” backtracking)
    • --enable-experimental-regexp-engine (enables the experimental engine features, including a non-standard l “linear” flag in V8) [2]
  • Whether/how you can use these depends on your Node.js build/version and runtime flags, but this is the closest thing to an “engine-level” mitigation inside V8. [2]
  1. Do not rely on “timeouting” a normal JS RegExp on the main thread
  • JavaScript’s built-in RegExp execution is synchronous; if it gets stuck, it blocks the event loop. There isn’t a standard per-regex timeout knob you can pass to RegExp.prototype.exec/test. (This is the core issue discussed in “timing out regex” questions.) [3]
  1. Isolate regex work so you can enforce a timeout
  • Run the match in a Worker thread / separate process, and kill it if it exceeds a deadline. This doesn’t make the regex faster, but it prevents one request from freezing the whole server (a common operational mitigation). (This is the typical solution when people ask for “regex timeouts” in JS.) [3]
  1. Prevent vulnerable patterns from reaching production
  • Static checks: safe-regex can flag potentially catastrophic regexes (heuristic-based). [4]
  • Linters/analyzers: security tooling and articles commonly recommend rules/tools that detect “vulnerable regular expressions,” and to refactor patterns to remove ambiguous nested quantifiers, etc. [5]
  1. Input hardening (still useful even with other mitigations)
  • Put strict length limits on user-controlled strings before regexing them; catastrophic cases often trigger on long near-misses. (Common mitigation in ReDoS guidance.) [5]

Rule of thumb: if the pattern or the input is attacker-controlled, either use RE2 [1] or run the match off-thread with a kill switch [3], and add static detection (safe-regex) as a guardrail. [4]

Sources

[1] re2 (node-re2) npm package description (RE2 bindings; protects against ReDoS) — https://www.npmjs.com/package/re2
[2] V8 blog: “An additional non-backtracking RegExp engine” (flags, fallback threshold, linear engine) — https://v8.dev/blog/non-backtracking-regexp
[3] Stack Overflow: “Timing out regex to avoid catastrophic backtracking” (JS regex is synchronous; timeout requires isolation) — https://stackoverflow.com/questions/50749499/timing-out-regex-to-avoid-catastrophic-backtracking
[4] safe-regex npm package (detect potentially catastrophic regexes) — https://www.npmjs.com/package/safe-regex
[5] SonarSource: “Vulnerable regular expressions in JavaScript” (ReDoS causes, mitigations, mentions RE2 bindings/tools) — https://www.sonarsource.com/blog/vulnerable-regular-expressions-javascript/


ReDoS vulnerability: regex constructed from untrusted input.

User-supplied filter patterns (e.g., /pattern/) are compiled directly into RegExp without validation (line 163). Malicious patterns with catastrophic backtracking (e.g., /(a+)+$/) can cause denial-of-service by consuming excessive CPU time. The existing catch block only handles syntax errors, not slow execution.

To mitigate:

  • Preferred: Replace with RE2 bindings (re2 npm package) for linear-time matching on untrusted patterns
  • Practical: Add strict length limits on valueSpec before regex compilation
  • Detection: Use safe-regex as a heuristic check during development (note: requires static pattern validation, not runtime)
  • Isolation: If using standard RegExp, consider moving regex execution to a Worker thread with a timeout to prevent blocking the event loop
🧰 Tools
🪛 ast-grep (0.41.0)

[warning] 162-162: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(pattern)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🪛 ESLint

[error] 164-164: Expected blank line before this statement.

(padding-line-between-statements)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test-workflow/taxonomy-filter-service.ts`
around lines 160 - 169, The RegExp construction in the taxonomy filter block
(the trimmed.startsWith('/') && trimmed.endsWith('/') branch) uses untrusted
input and is vulnerable to ReDoS; replace unsafe RegExp usage by either: 1)
switching to a linear-time regex engine such as the re2 package (create an RE2
instance instead of new RegExp for the pattern), or 2) if re2 cannot be
introduced, enforce strict runtime protections by (a) validating and bounding
the pattern length (reject patterns above a safe threshold), and (b) executing
the regex test off the main thread with a timeout (e.g., a Worker/worker_threads
task that returns false on timeout) to avoid blocking the event loop; update the
catch behavior to treat rejected/timeout cases as literal matches and surface a
debug log message for rejected patterns.

Comment on lines +278 to +282
lines.push(` <failure message="${TestResultReporter.escapeXml(failure.message)}">`);
if (failure.stackTrace) {
lines.push(` <![CDATA[${failure.stackTrace}]]>`);
}
lines.push(' </failure>');
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

CDATA section can be broken by ]]> in stack trace.

If failure.stackTrace contains the literal string ]]>, it will prematurely close the CDATA section and produce malformed XML. CDATA cannot contain ]]> as-is.

🐛 Proposed fix: escape or split CDATA terminator
+  /**
+   * Sanitize content for CDATA by splitting any ]]> sequences.
+   */
+  private static sanitizeCData(content: string): string {
+    return content.replace(/\]\]>/g, ']]]]><![CDATA[>');
+  }
+
   private static toJUnitXml(results: TestResult[]): string {

Then in the CDATA output:

         if (failure.stackTrace) {
-          lines.push(`        <![CDATA[${failure.stackTrace}]]>`);
+          lines.push(`        <![CDATA[${TestResultReporter.sanitizeCData(failure.stackTrace)}]]>`);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
lines.push(` <failure message="${TestResultReporter.escapeXml(failure.message)}">`);
if (failure.stackTrace) {
lines.push(` <![CDATA[${failure.stackTrace}]]>`);
}
lines.push(' </failure>');
lines.push(` <failure message="${TestResultReporter.escapeXml(failure.message)}">`);
if (failure.stackTrace) {
lines.push(` <![CDATA[${TestResultReporter.sanitizeCData(failure.stackTrace)}]]>`);
}
lines.push(' </failure>');
Suggested change
lines.push(` <failure message="${TestResultReporter.escapeXml(failure.message)}">`);
if (failure.stackTrace) {
lines.push(` <![CDATA[${failure.stackTrace}]]>`);
}
lines.push(' </failure>');
/**
* Sanitize content for CDATA by splitting any ]]> sequences.
*/
private static sanitizeCData(content: string): string {
return content.replace(/\]\]>/g, ']]]]><![CDATA[>');
}
🧰 Tools
🪛 ESLint

[error] 278-278: Do not call Array#push() multiple times.

(unicorn/no-array-push-push)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test-workflow/test-result-reporter.ts` around
lines 278 - 282, The CDATA block written in TestResultReporter (where lines.push
is used to output failure.stackTrace) can be broken if failure.stackTrace
contains "]]>"; update the reporter to sanitize/split the stack trace before
wrapping in <![CDATA[...]]> by replacing every occurrence of "]]>" with
"]]]]><![CDATA[>" (or otherwise escape/split the terminator) so the CDATA
section remains valid; ensure this transformation is applied to the value used
in the lines.push call that currently writes `<![CDATA[${failure.stackTrace}]]>`
and keep using TestResultReporter.escapeXml for other XML content as
appropriate.

Comment on lines +64 to +69
if (raw.needs !== undefined) {
if (!Array.isArray(raw.needs)) {
throw new Error(`Run '${raw.name}': 'needs' must be an array of strings`);
}
run.needs = raw.needs;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate that each element in needs is a string.

The array is assigned directly without validating that each element is actually a string. If the YAML contains non-string values like needs: [1, null], they would be accepted silently.

🛡️ Proposed fix
     if (raw.needs !== undefined) {
       if (!Array.isArray(raw.needs)) {
         throw new TypeError(`Run '${raw.name}': 'needs' must be an array of strings`);
       }
-      run.needs = raw.needs;
+      if (!raw.needs.every((n: unknown) => typeof n === 'string')) {
+        throw new TypeError(`Run '${raw.name}': 'needs' must contain only strings`);
+      }
+      run.needs = raw.needs as string[];
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (raw.needs !== undefined) {
if (!Array.isArray(raw.needs)) {
throw new Error(`Run '${raw.name}': 'needs' must be an array of strings`);
}
run.needs = raw.needs;
}
if (raw.needs !== undefined) {
if (!Array.isArray(raw.needs)) {
throw new TypeError(`Run '${raw.name}': 'needs' must be an array of strings`);
}
if (!raw.needs.every((n: unknown) => typeof n === 'string')) {
throw new TypeError(`Run '${raw.name}': 'needs' must contain only strings`);
}
run.needs = raw.needs as string[];
}
🧰 Tools
🪛 ESLint

[error] 66-66: new Error() is too unspecific for a type check. Use new TypeError() instead.

(unicorn/prefer-type-error)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test-workflow/test-suite-parser.ts` around
lines 64 - 69, The needs array is assigned without element type checks; update
the validation in the block that checks raw.needs (the code referencing raw.name
and assigning run.needs) to ensure every element is a string: iterate over
raw.needs, confirm typeof item === 'string' for each entry, and if any element
fails, throw a descriptive Error including the run name and the offending
index/value (e.g., "Run '...': 'needs' element at index X must be a string").
Only assign run.needs = raw.needs after all elements pass validation.

Comment on lines +63 to +65
const resultFormat = params.testResultFormat;
if (resultPath) {
TestResultReporter.writeResults(allResults, resultPath, resultFormat as 'junit' | 'json' | 'both');
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unsafe type assertion; validate testResultFormat before use.

The cast resultFormat as 'junit' | 'json' | 'both' bypasses type checking. If params.testResultFormat contains an unexpected value, writeResults may behave unexpectedly or fail silently.

🛡️ Proposed fix: validate and default the format
     const resultPath = params.testResultPath;
-    const resultFormat = params.testResultFormat;
+    const validFormats = ['junit', 'json', 'both'] as const;
+    const resultFormat = validFormats.includes(params.testResultFormat as any)
+      ? (params.testResultFormat as 'junit' | 'json' | 'both')
+      : 'both';
     if (resultPath) {
-      TestResultReporter.writeResults(allResults, resultPath, resultFormat as 'junit' | 'json' | 'both');
+      TestResultReporter.writeResults(allResults, resultPath, resultFormat);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test-workflow/test-workflow-service.ts`
around lines 63 - 65, The code unsafely asserts params.testResultFormat when
calling TestResultReporter.writeResults; instead validate
params.testResultFormat against allowed values ('junit','json','both') and
map/normalize it to a safe union before calling writeResults. In the block
around resultFormat/resultPath, replace the assertion with a small validation:
read params.testResultFormat into resultFormat, if it matches one of the allowed
strings use it, otherwise set a sensible default (e.g., 'both'), then pass that
validated value to TestResultReporter.writeResults.

Comment on lines +92 to +103
// Build the full Unity command
const unityPath = TestWorkflowService.resolveUnityPath(params);
const command = `"${unityPath}" ${args} -testResults "${resultFile}"`;

core.info(`[TestWorkflow] Executing: ${command}`);

execSync(command, {
timeout: timeoutMs,
stdio: 'pipe',
encoding: 'utf8',
cwd: params.projectPath || process.cwd(),
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential command injection via shell metacharacters in paths.

The command string (line 94) is built by concatenating unityPath and resultFile into a shell command. Since execSync uses shell by default, special characters in params.testResultPath or run.name could allow command injection.

Consider using execFileSync (or async equivalent) which bypasses the shell, passing arguments as an array.

🔒️ Proposed fix: use execFile with argument array
-import { execSync } from 'node:child_process';
+import { execFile } from 'node:child_process';
+import { promisify } from 'node:util';
+
+const execFileAsync = promisify(execFile);
-      const command = `"${unityPath}" ${args} -testResults "${resultFile}"`;
-      core.info(`[TestWorkflow] Executing: ${command}`);
-
-      execSync(command, {
-        timeout: timeoutMs,
-        stdio: 'pipe',
-        encoding: 'utf8',
-        cwd: params.projectPath || process.cwd(),
-      });
+      const argList = [...args.split(' ').filter(Boolean), '-testResults', resultFile];
+      core.info(`[TestWorkflow] Executing: ${unityPath} ${argList.join(' ')}`);
+
+      await execFileAsync(unityPath, argList, {
+        timeout: timeoutMs,
+        cwd: params.projectPath || process.cwd(),
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test-workflow/test-workflow-service.ts`
around lines 92 - 103, The command string built in TestWorkflowService (using
unityPath, args and resultFile) is vulnerable to shell injection because
execSync runs via the shell; instead, replace the execSync(command, ...) call
with execFileSync (or execFile) and pass unityPath as the file and the
individual arguments (including the test result path derived from
params.testResultPath/run.name) as an array so special characters are not
interpreted by a shell; keep the same options (timeout, cwd, stdio, encoding)
when invoking execFileSync and continue using
TestWorkflowService.resolveUnityPath to resolve the binary path.

Comment on lines +188 to +199
} else if (run.editMode && run.playMode) {
// Both modes: run EditMode first, then PlayMode will require a separate invocation
// For combined mode, use EditMode (the service handles sequencing)
args.push('-runTests');
args.push('-testPlatform EditMode');
} else if (run.playMode) {
args.push('-runTests');
args.push('-testPlatform PlayMode');
} else if (run.editMode) {
args.push('-runTests');
args.push('-testPlatform EditMode');
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Combined editMode+playMode only runs EditMode; PlayMode tests are silently skipped.

When both run.editMode and run.playMode are true, only EditMode is configured (lines 191-192). The comment states "the service handles sequencing," but no such sequencing logic exists in executeTestRun or executeTestSuite.

Either implement dual-mode execution (two Unity invocations) or document that users should create separate runs for each mode.

Do you want me to propose an implementation that handles both modes with separate invocations?

🧰 Tools
🪛 ESLint

[error] 192-192: Do not call Array#push() multiple times.

(unicorn/no-array-push-push)


[error] 195-195: Do not call Array#push() multiple times.

(unicorn/no-array-push-push)


[error] 198-198: Do not call Array#push() multiple times.

(unicorn/no-array-push-push)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test-workflow/test-workflow-service.ts`
around lines 188 - 199, When both run.editMode and run.playMode are true, the
current branch only configures EditMode and silently skips PlayMode; update
test-workflow-service to perform two sequential Unity invocations instead: build
args for EditMode (as currently done) and call executeTestRun/executeTestSuite
for that run, then build args for PlayMode (with '-runTests' and '-testPlatform
PlayMode') and call executeTestRun/executeTestSuite again; ensure you
propagate/aggregate results or errors from both invocations (e.g., fail the
overall run if either invocation fails) and reference the existing run object
and functions executeTestRun and executeTestSuite when locating where to add the
second invocation and result handling.

Comment on lines +227 to +237
// Default paths by platform
if (process.platform === 'win32') {
return `C:/Program Files/Unity/Hub/Editor/${params.editorVersion}/Editor/Unity.exe`;
}
if (process.platform === 'darwin') {
return `/Applications/Unity/Hub/Editor/${params.editorVersion}/Unity.app/Contents/MacOS/Unity`;
}

// Linux default (Docker container path)
return '/opt/unity/Editor/Unity';
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle undefined editorVersion to avoid invalid paths.

If params.editorVersion is undefined, the constructed paths will contain the literal string "undefined" (e.g., /Applications/Unity/Hub/Editor/undefined/Unity.app/...). Consider adding validation or a fallback.

🛡️ Proposed fix: validate editorVersion
   private static resolveUnityPath(params: BuildParameters): string {
     const envUnityPath = process.env.UNITY_PATH ?? process.env.UNITY_EDITOR;
     if (envUnityPath) {
       return envUnityPath;
     }

+    if (!params.editorVersion) {
+      throw new Error('Unity editor version is required but not specified. Set UNITY_PATH environment variable or provide editorVersion.');
+    }
+
     // Default paths by platform
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Default paths by platform
if (process.platform === 'win32') {
return `C:/Program Files/Unity/Hub/Editor/${params.editorVersion}/Editor/Unity.exe`;
}
if (process.platform === 'darwin') {
return `/Applications/Unity/Hub/Editor/${params.editorVersion}/Unity.app/Contents/MacOS/Unity`;
}
// Linux default (Docker container path)
return '/opt/unity/Editor/Unity';
}
private static resolveUnityPath(params: BuildParameters): string {
const envUnityPath = process.env.UNITY_PATH ?? process.env.UNITY_EDITOR;
if (envUnityPath) {
return envUnityPath;
}
if (!params.editorVersion) {
throw new Error('Unity editor version is required but not specified. Set UNITY_PATH environment variable or provide editorVersion.');
}
// Default paths by platform
if (process.platform === 'win32') {
return `C:/Program Files/Unity/Hub/Editor/${params.editorVersion}/Editor/Unity.exe`;
}
if (process.platform === 'darwin') {
return `/Applications/Unity/Hub/Editor/${params.editorVersion}/Unity.app/Contents/MacOS/Unity`;
}
// Linux default (Docker container path)
return '/opt/unity/Editor/Unity';
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/services/test-workflow/test-workflow-service.ts`
around lines 227 - 237, The path builder uses params.editorVersion directly and
can produce "undefined" in returned paths; validate params.editorVersion at the
start of the routine (the function that returns platform-specific Unity paths)
and either throw a clear error or substitute a sensible fallback before
constructing the paths. Specifically, check params.editorVersion for
undefined/empty and handle it prior to the win32/darwin/Linux returns so
Unity.exe/Unity.app/Unity path strings are never built with "undefined" — update
the function that contains the platform branch (references to
params.editorVersion and the three return statements for Windows, macOS, and
Linux) to perform this validation and return/raise accordingly.

Replace execSync with promisified exec so Promise.all actually runs
test groups in parallel. Add native timeout support via exec options.
Add 50MB maxBuffer for large Unity output. Fix ESLint violations
(variable naming, padding lines, array push consolidation).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov
Copy link

codecov bot commented Mar 5, 2026

Codecov Report

❌ Patch coverage is 75.36842% with 117 lines in your changes missing coverage. Please review.
✅ Project coverage is 35.41%. Comparing base (9d47543) to head (c5355c5).

Files with missing lines Patch % Lines
...or/services/test-workflow/test-workflow-service.ts 25.47% 79 Missing ⚠️
...tor/services/test-workflow/test-result-reporter.ts 84.21% 24 Missing ⚠️
...trator/services/test-workflow/test-suite-parser.ts 93.89% 8 Missing ⚠️
.../services/test-workflow/taxonomy-filter-service.ts 92.10% 6 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #790      +/-   ##
==========================================
+ Coverage   31.25%   35.41%   +4.15%     
==========================================
  Files          84       88       +4     
  Lines        4563     5038     +475     
  Branches     1103     1238     +135     
==========================================
+ Hits         1426     1784     +358     
- Misses       3137     3254     +117     
Files with missing lines Coverage Δ
src/model/build-parameters.ts 90.00% <ø> (ø)
src/model/input.ts 89.63% <100.00%> (+0.67%) ⬆️
src/model/orchestrator/workflows/async-workflow.ts 27.77% <ø> (ø)
...rchestrator/workflows/build-automation-workflow.ts 10.44% <ø> (ø)
.../services/test-workflow/taxonomy-filter-service.ts 92.10% <92.10%> (ø)
...trator/services/test-workflow/test-suite-parser.ts 93.89% <93.89%> (ø)
...tor/services/test-workflow/test-result-reporter.ts 84.21% <84.21%> (ø)
...or/services/test-workflow/test-workflow-service.ts 25.47% <25.47%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

frostebite and others added 2 commits March 5, 2026 23:33
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The orchestrator-develop branch no longer exists. Update all fallback
clone commands and test fixtures to use main instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/model/orchestrator/workflows/build-automation-workflow.ts (1)

101-103: Extract this clone-fallback block into a shared helper.

Lines 101-103 now mirror the same branch-resolution shell snippet in src/model/orchestrator/workflows/async-workflow.ts. Keeping this policy duplicated in two template strings makes future fallback changes easy to drift again.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/model/orchestrator/workflows/build-automation-workflow.ts` around lines
101 - 103, Extract the duplicated clone-fallback shell snippet into a shared
helper function (e.g., renderCloneFallbackSnippet or getCloneFallbackSnippet)
and replace the inline template string in both build-automation-workflow.ts and
async-workflow.ts with calls to that helper; the helper should return the exact
snippet currently used (the echo + git clone -b main ... || git clone ...) so
future changes are made in one place, and update imports/exports accordingly so
build-automation-workflow.ts and async-workflow.ts use the new helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/orchestrator-async-checks.yml:
- Line 57: Replace the hardcoded "git clone -b main ..." invocation so the
workflow checks out the dispatched branch/commit instead of always bootstrapping
from main; specifically, stop using the literal "git clone -b main" and either
use actions/checkout@v4 with ref: ${{ github.ref }} (or ref: ${{ github.sha }})
or adjust the git clone to use the dispatched ref variable (github.ref or
github.sha) so the workflow validates the exact revision of the dispatch.

---

Nitpick comments:
In `@src/model/orchestrator/workflows/build-automation-workflow.ts`:
- Around line 101-103: Extract the duplicated clone-fallback shell snippet into
a shared helper function (e.g., renderCloneFallbackSnippet or
getCloneFallbackSnippet) and replace the inline template string in both
build-automation-workflow.ts and async-workflow.ts with calls to that helper;
the helper should return the exact snippet currently used (the echo + git clone
-b main ... || git clone ...) so future changes are made in one place, and
update imports/exports accordingly so build-automation-workflow.ts and
async-workflow.ts use the new helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2efb14ba-4f25-4eda-a403-28f20e737d6b

📥 Commits

Reviewing files that changed from the base of the PR and between 5e54bcd and c5355c5.

⛔ Files ignored due to path filters (2)
  • dist/index.js is excluded by !**/dist/**
  • dist/index.js.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (5)
  • .github/workflows/build-tests-mac.yml
  • .github/workflows/orchestrator-async-checks.yml
  • src/model/orchestrator/tests/e2e/orchestrator-end2end-caching.test.ts
  • src/model/orchestrator/workflows/async-workflow.ts
  • src/model/orchestrator/workflows/build-automation-workflow.ts
✅ Files skipped from review due to trivial changes (1)
  • src/model/orchestrator/tests/e2e/orchestrator-end2end-caching.test.ts

CHECKS_UPDATE: ${{ github.event.inputs.checksObject }}
run: |
git clone -b orchestrator-develop https://github.com/game-ci/unity-builder
git clone -b main https://github.com/game-ci/unity-builder
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clone the dispatched ref instead of always bootstrapping from main.

Line 57 now forces checks-update to run from the default-branch code even when this workflow is dispatched from a feature branch. That makes async checks validate the wrong revision and can hide regressions in the branch under review.

Suggested fix
-          git clone -b main https://github.com/game-ci/unity-builder
+          BRANCH="${ORCHESTRATOR_BRANCH#refs/heads/}"
+          git clone -b "$BRANCH" https://github.com/game-ci/unity-builder \
+            || git clone -b main https://github.com/game-ci/unity-builder
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
git clone -b main https://github.com/game-ci/unity-builder
BRANCH="${ORCHESTRATOR_BRANCH#refs/heads/}"
git clone -b "$BRANCH" https://github.com/game-ci/unity-builder \
|| git clone -b main https://github.com/game-ci/unity-builder
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/orchestrator-async-checks.yml at line 57, Replace the
hardcoded "git clone -b main ..." invocation so the workflow checks out the
dispatched branch/commit instead of always bootstrapping from main;
specifically, stop using the literal "git clone -b main" and either use
actions/checkout@v4 with ref: ${{ github.ref }} (or ref: ${{ github.sha }}) or
adjust the git clone to use the dispatched ref variable (github.ref or
github.sha) so the workflow validates the exact revision of the dispatch.

@frostebite
Copy link
Member Author

Closing — all orchestrator code has been extracted to the standalone game-ci/orchestrator repository.

Content from this PR (test workflow engine — suite definitions, taxonomy filters, structured results) is fully present in the orchestrator repo. See PR #819 for the extraction.

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

Labels

enhancement New feature or request Next-Gen Orchestrator Next-Gen experimental features orchestrator Orchestrator module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Test Workflow Engine — suite definitions, taxonomy filters, structured results

1 participant