Skip to content

Conversation

@AlexSkrypnyk
Copy link
Member

@AlexSkrypnyk AlexSkrypnyk commented Nov 21, 2025

Related #1192

Summary by CodeRabbit

  • New Features

    • Added a standalone PHP tooling package with notification scripts (email, GitHub, Jira, New Relic, Slack, webhook), helper utilities, request/messaging tools, and interactive playground demos.
  • Documentation

    • Added comprehensive tooling development guide, conversion plan, and updated testing architecture and usage docs.
  • Tests

    • Extensive unit/self/integration test suites and fixtures covering helpers, notifications, mocks, and test infrastructure.
  • Chores

    • New CI workflow to lint, run tests across PHP versions, collect coverage, and upload reports.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 21, 2025

Walkthrough

Adds a standalone PHP tooling package under .vortex/tooling/ (helpers, notify channel scripts, tests, configs, playground) and a new GitHub Actions workflow .github/workflows/vortex-test-tooling.yml to run linting and tests across PHP 8.3–8.5 with coverage reporting.

Changes

Cohort / File(s) Summary
GitHub Actions workflow
/.github/workflows/vortex-test-tooling.yml
New CI workflow to run tooling tests (PHP 8.3–8.5): checkout, setup PHP/Node, install deps in .vortex/tooling, lint, run tests with coverage, upload artifacts, and publish coverage to Codecov.
Tooling package manifest & configs
.vortex/tooling
/.vortex/tooling/composer.json, /.vortex/tooling/phpcs.xml, /.vortex/tooling/phpstan.neon, /.vortex/tooling/phpunit.xml, /.vortex/tooling/rector.php, /.vortex/tooling/.gitattributes, /.vortex/tooling/.gitignore
Adds Composer manifest and static analysis/linting/rector/phpunit/config files plus gitattributes/gitignore for the tooling package.
Core helpers
/.vortex/tooling/src/helpers.php
New helper library (namespace DrevOps\VortexTooling) providing dotenv loading, getenv helpers, formatted output primitives, command_exists, token replacement, curl-based request helpers (request_get/post/request), execute_override, and a test-friendly quit wrapper.
Notification router & channels
/.vortex/tooling/src/notify, /.vortex/tooling/src/notify-*
Adds router (notify) and channel scripts: notify-email, notify-github, notify-jira, notify-newrelic, notify-slack, notify-webhook — each driven by env vars, builds payloads, calls external APIs or mail/webhook, and uses helpers for logging and error handling.
Lint/check tool & fixtures
/.vortex/tooling/check-no-exit.php, /.vortex/tooling/tests/Fixtures/check-exit/*
Adds a script to detect exit() usage (advocates quit()), plus fixtures exercising detection, comment-ignores, and line reporting.
Playground & demos
/.vortex/tooling/playground/{env.php,formatters.php,request.php,.env,.env.local}
Demo scripts and example .env files to exercise dotenv loading, formatters, and HTTP helpers.
Test infra: traits, base classes & exceptions
/.vortex/tooling/tests/Traits/MockTrait.php, /.vortex/tooling/tests/Unit/UnitTestCase.php, /.vortex/tooling/tests/Exceptions/*
Adds MockTrait (stateful mocks for passthru/mail/request/quit), UnitTestCase base with runScript helpers, and QuitSuccess/QuitError exceptions.
Tests — Self & Unit
/.vortex/tooling/tests/Self/*, /.vortex/tooling/tests/Unit/*
Large test suite covering mock infrastructure, helpers (dotenv, getenv, formatters, command_exists, requests, replace_tokens), override handling, and comprehensive router/channel tests (email, GitHub, Jira, New Relic, Slack, webhook).
Fixtures for tests
/.vortex/tooling/tests/Fixtures/*
Many fixture scripts used by self-tests to validate passthru, quit, request, mail behavior, overrides, and exit-check tool.
Documentation & conversion plan
/.vortex/CLAUDE.md, /.vortex/tooling/CLAUDE.md, /.vortex/tooling/scripts_conversion_plan.md
Adds tooling package documentation, development guide, and a plan for converting Bash scripts to the PHP package; updates Vortex docs to include the tooling subsystem.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Dev as Developer
    participant CI as GitHub Actions
    participant Repo as Repo
    participant Router as notify (router)
    participant Channel as notify-{channel}
    participant Ext as External Service

    Dev->>Repo: push / open PR (develop / feature/** / bugfix/**)
    Repo->>CI: workflow trigger (.github/workflows/vortex-test-tooling.yml)
    CI->>CI: setup PHP matrix (8.3,8.4,8.5), Node.js, install deps
    CI->>CI: lint (phpcs/phpstan), run phpunit with coverage, upload artifacts
    CI->>Ext: upload cobertura.xml to Codecov

    Note over Router,Channel: Runtime flow for notifications
    Dev->>Router: run notify (env: VORTEX_NOTIFY_CHANNELS,LABEL,EVENT)
    Router->>Router: load env, validate, parse channels
    loop per channel
        Router->>Channel: execute notify-{channel} (passthru)
        Channel->>Channel: build payload (replace tokens)
        Channel->>Ext: send API/mail/webhook
        Ext-->>Channel: response (status)
        Channel-->>Router: exit code & logs
    end
    Router->>Dev: aggregated exit (0 if all succeed, else non-zero)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas to focus review on:

  • .vortex/tooling/src/helpers.php — curl usage, timeouts, error parsing, and quit/override behavior.
  • Notification channels (.vortex/tooling/src/notify-*) — auth, payload construction, response handling, and validation.
  • MockTrait (tests/Traits/MockTrait.php) and UnitTestCase — correctness of mock consumption lifecycle and script-execution helpers.
  • CI workflow secrets/artifact handling and Codecov upload steps.

Possibly related PRs

Suggested labels

PR: DO NOT MERGE, PR: Do not review

Poem

🐇 I hopped through helpers, neat and bright,

Tokens snug and tests in flight.
Routers call and channels sing,
Mocks keep order, CI will bring —
A toolbox ready, day or night.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.76% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding the drevops/vortex-tooling codebase. It is concise, directly related to the changeset, and includes an issue reference.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/1192-tooling-package

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

@github-actions github-actions bot temporarily deployed to commit November 21, 2025 01:08 Inactive
@AlexSkrypnyk AlexSkrypnyk force-pushed the feature/1192-tooling-package branch 2 times, most recently from 95b0b22 to 310b5e8 Compare November 21, 2025 01:14
@github-actions github-actions bot temporarily deployed to commit November 21, 2025 01:17 Inactive
@AlexSkrypnyk AlexSkrypnyk force-pushed the feature/1192-tooling-package branch from 310b5e8 to 9a69aa1 Compare November 21, 2025 01:21
@github-actions github-actions bot temporarily deployed to commit November 21, 2025 01:23 Inactive
@AlexSkrypnyk AlexSkrypnyk force-pushed the feature/1192-tooling-package branch from 9a69aa1 to 482ed6a Compare November 21, 2025 01:23
@github-actions github-actions bot temporarily deployed to commit November 21, 2025 01:26 Inactive
@codecov
Copy link

codecov bot commented Nov 21, 2025

Codecov Report

❌ Patch coverage is 98.95561% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.86%. Comparing base (e265715) to head (fddfa28).

Files with missing lines Patch % Lines
.vortex/tooling/src/helpers.php 96.07% 4 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #2115      +/-   ##
===========================================
+ Coverage    70.89%   72.86%   +1.96%     
===========================================
  Files           99      105       +6     
  Lines         5075     5458     +383     
  Branches        44       44              
===========================================
+ Hits          3598     3977     +379     
- Misses        1477     1481       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 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.

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: 25

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6cac67a and 482ed6a.

📒 Files selected for processing (37)
  • .github/workflows/vortex-test-tooling.yml (1 hunks)
  • .vortex/CLAUDE.md (9 hunks)
  • .vortex/installer/composer.json (1 hunks)
  • .vortex/tooling/.gitignore (1 hunks)
  • .vortex/tooling/CLAUDE.md (1 hunks)
  • .vortex/tooling/composer.json (1 hunks)
  • .vortex/tooling/phpcs.xml (1 hunks)
  • .vortex/tooling/phpstan.neon (1 hunks)
  • .vortex/tooling/phpunit.xml (1 hunks)
  • .vortex/tooling/rector.php (1 hunks)
  • .vortex/tooling/scripts_conversion_plan.md (1 hunks)
  • .vortex/tooling/src/helpers.php (1 hunks)
  • .vortex/tooling/src/notify (1 hunks)
  • .vortex/tooling/src/notify-email (1 hunks)
  • .vortex/tooling/src/notify-github (1 hunks)
  • .vortex/tooling/src/notify-jira (1 hunks)
  • .vortex/tooling/src/notify-newrelic (1 hunks)
  • .vortex/tooling/src/notify-slack (1 hunks)
  • .vortex/tooling/src/notify-webhook (1 hunks)
  • .vortex/tooling/test_plan.md (1 hunks)
  • .vortex/tooling/tests/Exceptions/QuitErrorException.php (1 hunks)
  • .vortex/tooling/tests/Exceptions/QuitSuccessException.php (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-passthru-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-passthru-multiple (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-passthru-passing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-quit-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-quit-passing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-get-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-get-passing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-multiple (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-post-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-post-passing (1 hunks)
  • .vortex/tooling/tests/Self/MockPassthruSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockQuitSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Traits/MockTrait.php (1 hunks)
  • .vortex/tooling/tests/Unit/UnitTestCase.php (1 hunks)
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-09-13T04:14:41.765Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 2011
File: .vortex/tests/phpunit/Traits/Steps/StepAhoyTrait.php:20-23
Timestamp: 2025-09-13T04:14:41.765Z
Learning: In .vortex/tests/phpunit PHPUnit tests using cmd/cmdFail helpers, patterns in the expected output arrays support '*' markers for positive assertions (text must be present) and '!' markers for negative assertions (text must NOT be present). The '*' markers are NOT literal text matches but rather indicate expected presence.

Applied to files:

  • .vortex/tooling/tests/Fixtures/test-passthru-failing
  • .vortex/tooling/tests/Fixtures/test-request-post-passing
  • .vortex/tooling/phpunit.xml
  • .vortex/tooling/tests/Fixtures/test-request-multiple
  • .vortex/tooling/tests/Unit/UnitTestCase.php
  • .vortex/tooling/tests/Exceptions/QuitSuccessException.php
  • .vortex/tooling/tests/Exceptions/QuitErrorException.php
  • .vortex/tooling/tests/Fixtures/test-quit-failing
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
  • .vortex/tooling/CLAUDE.md
  • .vortex/tooling/tests/Fixtures/test-request-get-passing
  • .vortex/tooling/tests/Fixtures/test-passthru-multiple
  • .vortex/tooling/tests/Traits/MockTrait.php
  • .vortex/tooling/tests/Self/MockQuitSelfTest.php
  • .vortex/tooling/tests/Self/MockPassthruSelfTest.php
  • .vortex/tooling/tests/Fixtures/test-passthru-passing
  • .vortex/tooling/test_plan.md
  • .vortex/tooling/tests/Fixtures/test-request-post-failing
  • .vortex/CLAUDE.md
  • .vortex/tooling/tests/Fixtures/test-quit-passing
  • .vortex/tooling/tests/Fixtures/test-request-get-failing
📚 Learning: 2025-08-08T12:02:24.652Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1896
File: .vortex/tests/bats/unit/download-db-lagoon.bats:24-25
Timestamp: 2025-08-08T12:02:24.652Z
Learning: In .vortex/tests/bats/unit Bats tests using ../_helper.bash (run_steps), prefixing a STEPS entry with "- " denotes a negative assertion (the substring must NOT appear in output). Unprefixed entries are positive assertions. Example: "- Database dump refresh requested. Will create a new dump." asserts absence; "Database dump refresh requested. Will create a new dump." asserts presence.

Applied to files:

  • .vortex/tooling/tests/Fixtures/test-passthru-failing
  • .vortex/tooling/tests/Fixtures/test-passthru-passing
  • .vortex/CLAUDE.md
📚 Learning: 2025-05-29T12:15:32.188Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 0
File: :0-0
Timestamp: 2025-05-29T12:15:32.188Z
Learning: Do not review files in `.vortex/installer/tests/Fixtures/install` directory as they are test fixtures.

Applied to files:

  • .vortex/tooling/tests/Fixtures/test-passthru-failing
  • .vortex/tooling/.gitignore
  • .vortex/CLAUDE.md
📚 Learning: 2025-06-02T00:41:36.321Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1697
File: .vortex/installer/tests/Fixtures/install/provision_database_lagoon/docs/onboarding.md:16-17
Timestamp: 2025-06-02T00:41:36.321Z
Learning: DO NOT EVER REVIEW FILES IN .vortex/installer/tests/Fixtures/ directory - these are test fixtures and should not be reviewed under any circumstances.

Applied to files:

  • .vortex/tooling/tests/Fixtures/test-passthru-failing
  • .vortex/CLAUDE.md
📚 Learning: 2025-06-01T08:08:59.071Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1693
File: .vortex/installer/tests/Fixtures/install/deploy_type_none_gha/.github/workflows/build-test-deploy.yml:1-4
Timestamp: 2025-06-01T08:08:59.071Z
Learning: Test fixture files (typically located in directories containing "fixture" or "test") should not be reviewed for code quality issues as they may intentionally contain incomplete, invalid, or malformed data for testing purposes.

Applied to files:

  • .vortex/tooling/tests/Fixtures/test-request-post-failing
📚 Learning: 2025-06-03T03:15:29.849Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1714
File: scripts/vortex/doctor.sh:69-70
Timestamp: 2025-06-03T03:15:29.849Z
Learning: In the Vortex project, logging functions (task(), info(), note(), pass(), fail(), warn()) are intentionally duplicated across all script files to make each script self-contained and independent, rather than sharing them through a common library. This design choice prioritizes script independence over reducing code duplication.

Applied to files:

  • .vortex/CLAUDE.md
🧬 Code graph analysis (8)
.vortex/tooling/tests/Unit/UnitTestCase.php (1)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockTearDown (63-81)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • __construct (20-26)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
  • __construct (20-26)
.vortex/tooling/tests/Self/MockRequestSelfTest.php (3)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (14-57)
  • runScript (27-55)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockRequest (350-352)
  • mockRequestAssertAllMocksConsumed (360-371)
.vortex/tooling/src/helpers.php (3)
  • request_get (237-243)
  • request_post (265-272)
  • request (294-347)
.vortex/tooling/tests/Traits/MockTrait.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-28)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
  • QuitSuccessException (12-28)
.vortex/tooling/src/helpers.php (2)
  • fail (149-152)
  • quit (367-369)
.vortex/tooling/tests/Self/MockQuitSelfTest.php (5)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-28)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
  • QuitSuccessException (12-28)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (14-57)
  • runScript (27-55)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/src/helpers.php (1)
  • quit (367-369)
.vortex/tooling/tests/Self/MockPassthruSelfTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (14-57)
  • runScript (27-55)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockPassthru (161-163)
  • mockPassthruAssertAllMocksConsumed (171-182)
.vortex/tooling/src/helpers.php (1)
.vortex/installer/src/Utils/Tui.php (3)
  • line (57-59)
  • output (38-43)
  • error (53-55)
🪛 GitHub Actions: Vortex - Test tooling
.vortex/tooling/tests/Self/MockRequestSelfTest.php

[error] 37-37: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 57-57: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 75-75: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 93-93: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 110-110: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 128-128: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 146-146: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 163-163: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 184-184: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 218-218: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 222-222: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 226-226: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 242-242: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 243-243: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 248-248: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 267-267: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 268-268: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 270-270: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 285-285: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 297-297: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 480-480: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)

🪛 LanguageTool
.vortex/tooling/CLAUDE.md

[grammar] ~140-~140: Ensure spelling is correct
Context: ...ssertAllMocksConsumed(): void #### Passthru Mocking **Response Structure**:php...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
.vortex/tooling/CLAUDE.md

53-53: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


83-83: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


128-128: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


143-143: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


153-153: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


171-171: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


183-183: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


198-198: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


252-252: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


259-259: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


264-264: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


332-332: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


364-364: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


379-379: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


380-380: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


389-389: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


390-390: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


397-397: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


425-425: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


426-426: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


439-439: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


444-444: Ordered list item prefix
Expected: 1; Actual: 4; Style: 1/1/1

(MD029, ol-prefix)


592-592: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


675-675: Bare URL used

(MD034, no-bare-urls)


676-676: Bare URL used

(MD034, no-bare-urls)


677-677: Bare URL used

(MD034, no-bare-urls)

.vortex/tooling/test_plan.md

11-11: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


86-86: Bare URL used

(MD034, no-bare-urls)


87-87: Bare URL used

(MD034, no-bare-urls)


92-92: Ordered list item prefix
Expected: 1; Actual: 15; Style: 1/2/3

(MD029, ol-prefix)


93-93: Ordered list item prefix
Expected: 2; Actual: 16; Style: 1/2/3

(MD029, ol-prefix)


94-94: Ordered list item prefix
Expected: 3; Actual: 17; Style: 1/2/3

(MD029, ol-prefix)


95-95: Ordered list item prefix
Expected: 4; Actual: 18; Style: 1/2/3

(MD029, ol-prefix)


96-96: Ordered list item prefix
Expected: 5; Actual: 19; Style: 1/2/3

(MD029, ol-prefix)


97-97: Ordered list item prefix
Expected: 6; Actual: 20; Style: 1/2/3

(MD029, ol-prefix)


98-98: Ordered list item prefix
Expected: 7; Actual: 21; Style: 1/2/3

(MD029, ol-prefix)


99-99: Ordered list item prefix
Expected: 8; Actual: 22; Style: 1/2/3

(MD029, ol-prefix)


100-100: Ordered list item prefix
Expected: 9; Actual: 23; Style: 1/2/3

(MD029, ol-prefix)


101-101: Ordered list item prefix
Expected: 10; Actual: 24; Style: 1/2/3

(MD029, ol-prefix)


102-102: Ordered list item prefix
Expected: 11; Actual: 25; Style: 1/2/3

(MD029, ol-prefix)


103-103: Ordered list item prefix
Expected: 12; Actual: 26; Style: 1/2/3

(MD029, ol-prefix)


113-113: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


134-134: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


155-155: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


176-176: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


197-197: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

.vortex/tooling/scripts_conversion_plan.md

9-9: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


16-16: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


17-17: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


17-17: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


43-43: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


48-48: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


59-59: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


68-68: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


92-92: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


93-93: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


101-101: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


103-103: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


112-112: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


125-125: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


126-126: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


139-139: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


140-140: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


146-146: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


147-147: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


166-166: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


167-167: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


182-182: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


278-278: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


283-283: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


386-386: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


393-393: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


394-394: Ordered list item prefix
Expected: 1; Actual: 6; Style: 1/2/3

(MD029, ol-prefix)


395-395: Ordered list item prefix
Expected: 2; Actual: 7; Style: 1/2/3

(MD029, ol-prefix)


396-396: Ordered list item prefix
Expected: 3; Actual: 8; Style: 1/2/3

(MD029, ol-prefix)


397-397: Ordered list item prefix
Expected: 4; Actual: 9; Style: 1/2/3

(MD029, ol-prefix)


398-398: Ordered list item prefix
Expected: 5; Actual: 10; Style: 1/2/3

(MD029, ol-prefix)


399-399: Ordered list item prefix
Expected: 6; Actual: 11; Style: 1/2/3

(MD029, ol-prefix)


401-401: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


402-402: Ordered list item prefix
Expected: 1; Actual: 12; Style: 1/2/3

(MD029, ol-prefix)


403-403: Ordered list item prefix
Expected: 2; Actual: 13; Style: 1/2/3

(MD029, ol-prefix)


404-404: Ordered list item prefix
Expected: 3; Actual: 14; Style: 1/2/3

(MD029, ol-prefix)


405-405: Ordered list item prefix
Expected: 4; Actual: 15; Style: 1/2/3

(MD029, ol-prefix)


406-406: Ordered list item prefix
Expected: 5; Actual: 16; Style: 1/2/3

(MD029, ol-prefix)


431-431: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


432-432: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


440-440: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


441-441: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


453-453: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


454-454: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


466-466: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


467-467: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


475-475: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


476-476: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


489-489: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


496-496: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


503-503: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: build (0)
  • GitHub Check: vortex-test-workflow (4)
  • GitHub Check: vortex-test-workflow (3)
  • GitHub Check: vortex-test-workflow (2)
  • GitHub Check: vortex-test-workflow (1)
  • GitHub Check: vortex-test-workflow (0)
  • GitHub Check: vortex-test-common
  • GitHub Check: vortex-test-installer (8.3)
  • GitHub Check: vortex-test-installer (8.4)
🔇 Additional comments (25)
.vortex/tooling/phpstan.neon (1)

5-28: PHPStan configuration is strict and well-scoped

Level 9 with paths limited to src/tests and targeted ignoreErrors against test-only patterns looks solid and conservative; I don’t see issues here.

.vortex/tooling/tests/Fixtures/test-quit-failing (1)

1-20: Quit fixture matches conventions and behavior

Fixture structure (shebang, strict types, namespace, helpers include) and behavior (announce, quit(1), unreachable error line) are consistent and suitable for testing non-zero exit paths.

.vortex/installer/composer.json (1)

14-18: Installer metadata correctly points to main vortex repository

Updating homepage and support URLs to drevops/vortex is consistent with the project consolidation and leaves runtime/dev requirements untouched.

.vortex/tooling/.gitignore (1)

1-3: Tooling .gitignore entries are appropriate

Ignoring vendor, composer.lock, and .logs for the tooling subtree matches its role as an internal dev/test package and keeps generated artifacts out of VCS.

.vortex/tooling/tests/Fixtures/test-quit-passing (1)

1-20: Fixture looks correct; no further review needed

Script shape (namespace, helpers include, message, quit(0) and unreachable line) matches a “success” quit fixture and is syntactically fine. Not reviewing deeper code‑quality aspects since this is a test fixture.
Based on learnings

.vortex/tooling/tests/Fixtures/test-request-get-failing (1)

1-24: Failure GET fixture is consistent with its intent

Flow and messaging align with a “request_get expecting failure” scenario; no syntax or obvious runtime issues. Skipping deeper code‑quality review as this is a test fixture.
Based on learnings

.vortex/tooling/tests/Fixtures/test-request-post-failing (1)

1-24: POST failure fixture matches described behavior

Script clearly exercises a failing request_post scenario and logs expected diagnostics; no obvious syntax/runtime issues. Treating this strictly as a fixture and not applying general code‑quality rules.
Based on learnings

.vortex/tooling/tests/Fixtures/test-passthru-passing (1)

1-19: Passthru “success” fixture is straightforward and valid

Command, messaging, and exit‑code logging align with a passing passthru scenario; no syntax or obvious runtime problems. Not applying broader quality checks since this is a test fixture.
Based on learnings

.vortex/tooling/tests/Fixtures/test-request-get-passing (1)

1-24: GET success fixture aligns with helper contract

The script cleanly exercises a successful request_get case and logs status, ok flag, and body; nothing stands out as problematic for a fixture. Skipping deeper code‑quality review per fixture guidance.
Based on learnings

.vortex/tooling/tests/Fixtures/test-passthru-failing (1)

4-19: Confirm intended exit‑code behavior for the “failing” passthru fixture

The fixture says “Script should return code 1” but currently runs passthru('echo "success"', $exit_code), which normally exits with code 0. If your tests rely on the real shell exit code, you may want a command that actually fails (e.g., false or exit 1). If MockPassthru/MockTrait fully controls $exit_code for this script, the current command is fine—just confirm it matches the test harness assumptions.

.vortex/tooling/tests/Fixtures/test-request-post-passing (1)

1-24: POST success fixture is consistent and minimal

The script correctly represents a “POST success” scenario using request_post and simple logging; no obvious syntax/runtime issues. Treating this as a fixture and not applying broader code‑quality review.
Based on learnings

.vortex/tooling/tests/Fixtures/test-request-multiple (1)

1-26: LGTM! Test fixture structure is correct.

The fixture correctly exercises multiple request helper functions (request_get, request_post, request) in sequence, with appropriate output for test verification. The structure aligns with the tooling test harness pattern.

.vortex/tooling/tests/Fixtures/test-passthru-multiple (1)

1-23: LGTM! Test fixture correctly exercises multiple passthru calls.

The fixture properly demonstrates sequential passthru operations with exit code capture, following the established test fixture pattern for the tooling subsystem.

.vortex/tooling/phpcs.xml (2)

1-16: LGTM! PHPCS configuration with appropriate base and custom rules.

The configuration correctly extends the Drupal standard and adds strict types enforcement plus snake_case naming conventions, which align with the tooling subsystem requirements.


27-46: LGTM! Test file exclusions are appropriately configured.

The exclusions for long array declarations and missing comments in test files follow best practices, allowing tests to focus on functionality rather than documentation requirements.

.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)

1-28: LGTM! Exception class correctly enforces non-zero exit code constraint.

The implementation is symmetrical with QuitSuccessException and properly validates that only non-zero exit codes are represented. The exception message clearly indicates the exit code value for test verification.

.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)

1-28: LGTM! Exception class correctly enforces zero exit code constraint.

The implementation properly validates that only exit code 0 is represented, maintaining symmetry with QuitErrorException. This allows tests to distinguish between successful and failed quit() scenarios.

.vortex/tooling/src/notify-newrelic (4)

1-76: LGTM! Script structure and environment variable loading follow established patterns.

The script correctly includes the shebang, strict types, helper requirements, and override execution. Environment variable loading with fallbacks is properly implemented using the ternary operator pattern.


78-98: LGTM! Validation and pre-deployment skip logic are correct.

Required variables are validated, revision auto-generation follows a clear format, and the pre-deployment event is correctly skipped (New Relic only supports post-deployment notifications).


139-143: LGTM! App ID validation correctly handles non-configured environments.

The validation appropriately checks for numeric app IDs and gracefully exits with an informative message for non-configured environments, avoiding false failures.


157-176: LGTM! Payload construction and dispatch correctly implement New Relic API requirements.

The deployment payload is properly structured, JSON-encoded with appropriate flags, and the response status is validated (expecting 201 for successful creation). Error messages are descriptive.

.github/workflows/vortex-test-tooling.yml (3)

22-51: LGTM! Workflow setup steps are properly configured.

Actions are pinned with commit SHAs for security, PHP is configured with coverage support, and all Composer operations correctly specify the .vortex/tooling working directory.


59-72: LGTM! Coverage upload configuration is appropriately strict.

Both artifact and Codecov uploads are configured to fail if files are missing or if upload errors occur, ensuring test coverage is properly tracked.


18-21: No issues found — PHP 8.5 is officially released and supported.

PHP 8.5.0 reached general availability on November 20, 2025, and shivammathur/setup-php@v2 supports PHP 8.5. The matrix configuration will work correctly without failures.

.vortex/tooling/scripts_conversion_plan.md (1)

1-574: LGTM! Documentation accurately describes the tooling conversion plan.

The conversion plan thoroughly documents the bash-to-PHP migration strategy, coding patterns, and project structure. The examples and guidelines align with the implemented tooling subsystem introduced in this PR.

Note: The static analysis hints flag markdown formatting issues (missing blank lines, language specifications for code blocks), but these are cosmetic and don't affect the content quality. Consider addressing them for consistency if markdown linting is enforced in CI.

Comment on lines +53 to +58
- name: Run tests
run: composer test-coverage
working-directory: .vortex/tooling
env:
GITHUB_TOKEN: ${{ secrets.PACKAGE_TOKEN }}

Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Clarify GITHUB_TOKEN environment variable naming.

Line 57 sets the environment variable GITHUB_TOKEN but uses the secret PACKAGE_TOKEN. If the tooling tests expect GITHUB_TOKEN specifically, ensure the secret name matches. If PACKAGE_TOKEN is intentional (e.g., for higher privileges), the env var name may be confusing.

Consider renaming for clarity:

 env:
-  GITHUB_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
+  PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}

Or document why GITHUB_TOKEN uses PACKAGE_TOKEN.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
.github/workflows/vortex-test-tooling.yml lines 53-58: the workflow sets env
GITHUB_TOKEN from secrets.PACKAGE_TOKEN which is confusing or potentially
incorrect; either (A) change the env key to PACKAGE_TOKEN (env: PACKAGE_TOKEN:
${{ secrets.PACKAGE_TOKEN }}) if the tooling expects that secret name, or (B)
change the secret reference to secrets.GITHUB_TOKEN (env: GITHUB_TOKEN: ${{
secrets.GITHUB_TOKEN }}) if tooling needs the standard token, or (C) leave as-is
but add a comment above these lines explaining why PACKAGE_TOKEN is mapped to
GITHUB_TOKEN (e.g., requires elevated privileges), so the intent is clear to
maintainers.

Comment on lines 17 to 26
<file>src</file>
<file>src/notify</file>
<file>src/notify-email</file>
<file>src/notify-github</file>
<file>src/notify-jira</file>
<file>src/notify-newrelic</file>
<file>src/notify-slack</file>
<file>src/notify-webhook</file>
<file>tests</file>
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider simplifying file specification.

Currently, each src/notify-* directory is listed individually. Since line 17 already includes src, lines 18-24 may be redundant unless PHPCS doesn't recursively scan subdirectories. If recursive scanning is enabled by default, you can remove lines 18-24.

Apply this diff if recursive scanning is sufficient:

 <file>src</file>
-<file>src/notify</file>
-<file>src/notify-email</file>
-<file>src/notify-github</file>
-<file>src/notify-jira</file>
-<file>src/notify-newrelic</file>
-<file>src/notify-slack</file>
-<file>src/notify-webhook</file>
 <file>tests</file>
🤖 Prompt for AI Agents
.vortex/tooling/phpcs.xml around lines 17 to 25: the file list redundantly
enumerates src/notify-* subdirectories even though src is already included; if
PHPCS is configured to scan directories recursively, remove the explicit
src/notify, src/notify-email, src/notify-github, src/notify-jira,
src/notify-newrelic, src/notify-slack, and src/notify-webhook entries (lines
18–24) leaving only <file>src</file> (and <file>tests</file>), otherwise confirm
PHPCS non-recursive behavior and add a comment explaining why the individual
entries must remain.

Comment on lines 10 to 217
**Mock response structure:**
```php
[
'url' => 'https://example.com/api', // Expected URL
'method' => 'GET', // Optional: expected method
'response' => [
'ok' => true,
'status' => 200,
'body' => 'response body',
'error' => null,
'info' => ['http_code' => 200]
]
]
```

**Implementation approach:** Mock the low-level curl_* functions (curl_init, curl_exec, curl_getinfo, etc.) used by the HTTP request functions to intercept all request operations.

## 2. Test Class Structure (tests/Self/MockRequestSelfTest.php)

**Direct Tests (14 tests):**

1. **testMockRequestGetSuccess** - Basic GET with 200
- Mock: url, status 200, body "success"
- Assert: ok=true, status=200, body contains "success"

2. **testMockRequestGetCustomSuccess** - GET with custom values
- Mock: status 201, custom body, custom info
- Assert: all custom values returned correctly

3. **testMockRequestGetDefaultsSuccess** - GET with minimal mock
- Mock: only url and status
- Assert: defaults filled in correctly

4. **testMockRequestGetFailure404** - GET with 404 error
- Mock: status 404, ok=false
- Assert: ok=false, status=404

5. **testMockRequestGetFailure500** - GET with 500 error
- Mock: status 500, ok=false
- Assert: ok=false, status=500

6. **testMockRequestGetNetworkError** - GET with network error
- Mock: error="Could not resolve host"
- Assert: error message present, ok=false

7. **testMockRequestPostSuccess** - POST with body
- Mock: POST method, body present, status 201
- Assert: ok=true, status=201

8. **testMockRequestPostJsonSuccess** - POST with JSON body
- Mock: POST with JSON content-type header
- Assert: request handled correctly

9. **testMockRequestCustomMethodSuccess** - PUT/DELETE
- Mock: method=PUT, status 200
- Assert: custom method works

10. **testMockRequestMultipleSuccess** - Sequential calls
- Mock: 3 different responses
- Assert: each returns correct response in order

11. **testMockRequestMultipleMoreCallsFailure** - Too many calls
- Mock: 2 responses
- Make: 3 calls
- Assert: RuntimeException on 3rd call

12. **testMockRequestMultipleLessCallsFailure** - Too few calls
- Mock: 3 responses
- Make: 2 calls
- Assert: AssertionFailedError in tearDown

13. **testMockRequestFailureArgumentExceptionUrl** - Missing URL validation
- Mock: response without url key
- Assert: InvalidArgumentException

14. **testMockRequestFailureAssertUnexpectedUrl** - URL mismatch
- Mock: url="https://example.com"
- Call: url="https://different.com"
- Assert: RuntimeException with clear message

**Script Tests (12 tests):**

15. **testMockRequestGetScriptPassingSuccess** - GET through script (uses test-request-get-passing)
16. **testMockRequestGetScriptFailingSuccess** - GET 404 through script (uses test-request-get-failing)
17. **testMockRequestGetScriptCustomSuccess** - Custom values through script (uses test-request-get-passing)
18. **testMockRequestGetScriptDefaultsSuccess** - Defaults through script (uses test-request-get-passing)
19. **testMockRequestGetScriptNetworkErrorSuccess** - Network error through script (uses test-request-get-failing)
20. **testMockRequestPostScriptPassingSuccess** - POST success through script (uses test-request-post-passing)
21. **testMockRequestPostScriptFailingSuccess** - POST failure through script (uses test-request-post-failing)
22. **testMockRequestMultipleScriptSuccess** - Multiple calls through script
23. **testMockRequestMultipleScriptMoreCallsFailure** - Too many calls through script
24. **testMockRequestMultipleScriptLessCallsFailure** - Too few calls through script
25. **testMockRequestGetScriptFailureArgumentExceptionUrl** - Missing URL through script
26. **testMockRequestGetScriptFailureAssertUnexpectedUrl** - URL mismatch through script

**Naming Convention:**
- **Test method names**: Use `Success`/`Failure` to indicate the test pathway (what behavior is being tested)
- **Fixture names**: Use `passing`/`failing` to describe the fixture behavior (whether it passes or fails)
- **Request script test naming**: For request tests with multiple methods (get, post, request), the pattern is `testMockRequest[Method]Script[Description][Outcome]` (e.g., `testMockRequestGetScriptPassingSuccess`, `testMockRequestPostScriptFailingSuccess`)

## 3. Test Fixtures (tests/Fixtures/)

**test-request-get-passing:**
```php
#!/usr/bin/env php
<?php
declare(strict_types=1);
namespace DrevOps\VortexTooling;

require_once __DIR__ . '/../../src/helpers.php';

echo 'Script will call request_get' . PHP_EOL;

$result = request_get('https://example.com/api');

echo 'Response status: ' . $result['status'] . PHP_EOL;
echo 'Response ok: ' . ($result['ok'] ? 'true' : 'false') . PHP_EOL;

if ($result['ok']) {
echo 'Response body: ' . $result['body'] . PHP_EOL;
}
```

**test-request-get-failing:**
```php
#!/usr/bin/env php
<?php
declare(strict_types=1);
namespace DrevOps\VortexTooling;

require_once __DIR__ . '/../../src/helpers.php';

echo 'Script will call request_get expecting failure' . PHP_EOL;

$result = request_get('https://example.com/not-found');

echo 'Response status: ' . $result['status'] . PHP_EOL;
echo 'Response ok: ' . ($result['ok'] ? 'true' : 'false') . PHP_EOL;

if (!$result['ok']) {
echo 'Request failed as expected' . PHP_EOL;
}
```

**test-request-post-passing:**
```php
#!/usr/bin/env php
<?php
declare(strict_types=1);
namespace DrevOps\VortexTooling;

require_once __DIR__ . '/../../src/helpers.php';

echo 'Script will call request_post' . PHP_EOL;

$result = request_post('https://example.com/api', json_encode(['key' => 'value']), ['Content-Type: application/json']);

echo 'Response status: ' . $result['status'] . PHP_EOL;
echo 'Response ok: ' . ($result['ok'] ? 'true' : 'false') . PHP_EOL;

if ($result['ok']) {
echo 'POST succeeded' . PHP_EOL;
}
```

**test-request-post-failing:**
```php
#!/usr/bin/env php
<?php
declare(strict_types=1);
namespace DrevOps\VortexTooling;

require_once __DIR__ . '/../../src/helpers.php';

echo 'Script will call request_post expecting failure' . PHP_EOL;

$result = request_post('https://example.com/error', json_encode(['key' => 'value']), ['Content-Type: application/json']);

echo 'Response status: ' . $result['status'] . PHP_EOL;
echo 'Response ok: ' . ($result['ok'] ? 'true' : 'false') . PHP_EOL;

if (!$result['ok']) {
echo 'POST failed as expected' . PHP_EOL;
}
```

**test-request-multiple:**
```php
#!/usr/bin/env php
<?php
declare(strict_types=1);
namespace DrevOps\VortexTooling;

require_once __DIR__ . '/../../src/helpers.php';

echo 'Script will call request functions multiple times' . PHP_EOL;

$result1 = request_get('https://example.com/first');
echo 'First call status: ' . $result1['status'] . PHP_EOL;

$result2 = request_post('https://example.com/second', 'data');
echo 'Second call status: ' . $result2['status'] . PHP_EOL;

$result3 = request('https://example.com/third', ['method' => 'PUT']);
echo 'Third call status: ' . $result3['status'] . PHP_EOL;

echo 'Script completed' . PHP_EOL;
```
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Align markdown with markdownlint (MD029, MD031, MD034)

The plan content looks good; the remaining issues are purely formatting and are causing markdownlint failures:

  • Surround fenced code blocks with blank lines (MD031).
  • Wrap bare URLs in angle brackets (e.g., https://example.com<https://example.com>) (MD034).
  • For the “Script Tests (12 tests)” section, restart numbering at 1–12 rather than 15–26 so it matches the 1/2/3 style (MD029), relying on the section title to distinguish from the direct tests.

Example adjustments:

-**Mock response structure:**
-```php
+**Mock response structure:**
+
+```php
@@
-```
+```
@@
-15. **testMockRequestGetScriptPassingSuccess** - GET through script (uses test-request-get-passing)
-16. **testMockRequestGetScriptFailingSuccess** - GET 404 through script (uses test-request-get-failing)
+1. **testMockRequestGetScriptPassingSuccess** - GET through script (uses test-request-get-passing)
+2. **testMockRequestGetScriptFailingSuccess** - GET 404 through script (uses test-request-get-failing)
@@
--   - Mock: url="https://example.com"
--   - Call: url="https://different.com"
+-   - Mock: url="<https://example.com>"
+-   - Call: url="<https://different.com>"

Apply the same blank‑line and URL tweaks around the other fenced blocks and bullet descriptions to clear the remaining lint errors.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

11-11: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


86-86: Bare URL used

(MD034, no-bare-urls)


87-87: Bare URL used

(MD034, no-bare-urls)


92-92: Ordered list item prefix
Expected: 1; Actual: 15; Style: 1/2/3

(MD029, ol-prefix)


93-93: Ordered list item prefix
Expected: 2; Actual: 16; Style: 1/2/3

(MD029, ol-prefix)


94-94: Ordered list item prefix
Expected: 3; Actual: 17; Style: 1/2/3

(MD029, ol-prefix)


95-95: Ordered list item prefix
Expected: 4; Actual: 18; Style: 1/2/3

(MD029, ol-prefix)


96-96: Ordered list item prefix
Expected: 5; Actual: 19; Style: 1/2/3

(MD029, ol-prefix)


97-97: Ordered list item prefix
Expected: 6; Actual: 20; Style: 1/2/3

(MD029, ol-prefix)


98-98: Ordered list item prefix
Expected: 7; Actual: 21; Style: 1/2/3

(MD029, ol-prefix)


99-99: Ordered list item prefix
Expected: 8; Actual: 22; Style: 1/2/3

(MD029, ol-prefix)


100-100: Ordered list item prefix
Expected: 9; Actual: 23; Style: 1/2/3

(MD029, ol-prefix)


101-101: Ordered list item prefix
Expected: 10; Actual: 24; Style: 1/2/3

(MD029, ol-prefix)


102-102: Ordered list item prefix
Expected: 11; Actual: 25; Style: 1/2/3

(MD029, ol-prefix)


103-103: Ordered list item prefix
Expected: 12; Actual: 26; Style: 1/2/3

(MD029, ol-prefix)


113-113: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


134-134: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


155-155: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


176-176: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


197-197: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

Comment on lines 7 to 512
use DrevOps\VortexTooling\Tests\Unit\CoversClass;
use DrevOps\VortexTooling\Tests\Unit\UnitTestCase;

/**
* Self-tests for mocking of HTTP request functions.
*
* We test mockRequest() to ensure it returns the correct responses
* and validates URL/method expectations.
*/
#[CoversClass(UnitTestCase::class)]
class MockRequestSelfTest extends UnitTestCase {

/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Load helpers.php to make request_get/request_post/request available.
require_once __DIR__ . '/../../src/helpers.php';
}

public function testMockRequestGetSuccess(): void {
$this->mockRequest([
'url' => 'https://example.com/api',
'response' => [
'status' => 200,
'body' => 'success response',
],
]);

$result = \DrevOps\VortexTooling\request_get('https://example.com/api');

$this->assertTrue($result['ok']);
$this->assertEquals(200, $result['status']);
$this->assertEquals('success response', $result['body']);
$this->assertNull($result['error']);
}

public function testMockRequestGetCustomSuccess(): void {
$this->mockRequest([
'url' => 'https://example.com/api',
'response' => [
'ok' => TRUE,
'status' => 201,
'body' => 'custom body content',
'error' => NULL,
'info' => ['http_code' => 201, 'total_time' => 1.5],
],
]);

$result = \DrevOps\VortexTooling\request_get('https://example.com/api');

$this->assertTrue($result['ok']);
$this->assertEquals(201, $result['status']);
$this->assertEquals('custom body content', $result['body']);
$this->assertNull($result['error']);
$this->assertEquals(201, $result['info']['http_code']);
$this->assertEquals(1.5, $result['info']['total_time']);
}

public function testMockRequestGetDefaultsSuccess(): void {
$this->mockRequest([
'url' => 'https://example.com/api',
'response' => [
'status' => 200,
],
]);

$result = \DrevOps\VortexTooling\request_get('https://example.com/api');

$this->assertTrue($result['ok']);
$this->assertEquals(200, $result['status']);
$this->assertEquals('', $result['body']);
$this->assertNull($result['error']);
}

public function testMockRequestGetFailure404(): void {
$this->mockRequest([
'url' => 'https://example.com/not-found',
'response' => [
'ok' => FALSE,
'status' => 404,
'body' => 'Not Found',
],
]);

$result = \DrevOps\VortexTooling\request_get('https://example.com/not-found');

$this->assertFalse($result['ok']);
$this->assertEquals(404, $result['status']);
$this->assertEquals('Not Found', $result['body']);
}

public function testMockRequestGetFailure500(): void {
$this->mockRequest([
'url' => 'https://example.com/error',
'response' => [
'ok' => FALSE,
'status' => 500,
'body' => 'Internal Server Error',
],
]);

$result = \DrevOps\VortexTooling\request_get('https://example.com/error');

$this->assertFalse($result['ok']);
$this->assertEquals(500, $result['status']);
$this->assertEquals('Internal Server Error', $result['body']);
}

public function testMockRequestGetNetworkError(): void {
$this->mockRequest([
'url' => 'https://example.com/timeout',
'response' => [
'ok' => FALSE,
'status' => 0,
'body' => FALSE,
'error' => 'Could not resolve host',
],
]);

$result = \DrevOps\VortexTooling\request_get('https://example.com/timeout');

$this->assertFalse($result['ok']);
$this->assertEquals(0, $result['status']);
$this->assertFalse($result['body']);
$this->assertEquals('Could not resolve host', $result['error']);
}

public function testMockRequestPostSuccess(): void {
$this->mockRequest([
'url' => 'https://example.com/api',
'method' => 'POST',
'response' => [
'status' => 201,
'body' => 'created',
],
]);

$result = \DrevOps\VortexTooling\request_post('https://example.com/api', '{"key":"value"}');

$this->assertTrue($result['ok']);
$this->assertEquals(201, $result['status']);
$this->assertEquals('created', $result['body']);
}

public function testMockRequestPostJsonSuccess(): void {
$this->mockRequest([
'url' => 'https://example.com/api/json',
'method' => 'POST',
'response' => [
'status' => 200,
'body' => '{"success":true}',
],
]);

$result = \DrevOps\VortexTooling\request_post(
'https://example.com/api/json',
json_encode(['data' => 'value']),
['Content-Type: application/json']
);

$this->assertTrue($result['ok']);
$this->assertEquals(200, $result['status']);
$this->assertEquals('{"success":true}', $result['body']);
}

public function testMockRequestCustomMethodSuccess(): void {
$this->mockRequest([
'url' => 'https://example.com/resource',
'method' => 'PUT',
'response' => [
'status' => 200,
'body' => 'updated',
],
]);

$result = \DrevOps\VortexTooling\request('https://example.com/resource', ['method' => 'PUT']);

$this->assertTrue($result['ok']);
$this->assertEquals(200, $result['status']);
$this->assertEquals('updated', $result['body']);
}

public function testMockRequestMultipleSuccess(): void {
$this->mockRequest([
'url' => 'https://example.com/first',
'response' => [
'status' => 200,
'body' => 'first response',
],
]);

$this->mockRequest([
'url' => 'https://example.com/second',
'method' => 'POST',
'response' => [
'status' => 201,
'body' => 'second response',
],
]);

$this->mockRequest([
'url' => 'https://example.com/third',
'method' => 'PUT',
'response' => [
'status' => 200,
'body' => 'third response',
],
]);

$result1 = \DrevOps\VortexTooling\request_get('https://example.com/first');
$this->assertEquals(200, $result1['status']);
$this->assertEquals('first response', $result1['body']);

$result2 = \DrevOps\VortexTooling\request_post('https://example.com/second', 'data');
$this->assertEquals(201, $result2['status']);
$this->assertEquals('second response', $result2['body']);

$result3 = \DrevOps\VortexTooling\request('https://example.com/third', ['method' => 'PUT']);
$this->assertEquals(200, $result3['status']);
$this->assertEquals('third response', $result3['body']);
}

public function testMockRequestMultipleMoreCallsFailure(): void {
$this->mockRequest([
'url' => 'https://example.com/first',
'response' => ['status' => 200, 'body' => 'first'],
]);

$this->mockRequest([
'url' => 'https://example.com/second',
'response' => ['status' => 200, 'body' => 'second'],
]);

\DrevOps\VortexTooling\request_get('https://example.com/first');
\DrevOps\VortexTooling\request_get('https://example.com/second');

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('curl_init() called more times than mocked responses. Expected 2 request(s), but attempting request #3');

\DrevOps\VortexTooling\request_get('https://example.com/third');
}

public function testMockRequestMultipleLessCallsFailure(): void {
$this->mockRequest([
'url' => 'https://example.com/first',
'response' => ['status' => 200, 'body' => 'first'],
]);

$this->mockRequest([
'url' => 'https://example.com/second',
'response' => ['status' => 200, 'body' => 'second'],
]);

$this->mockRequest([
'url' => 'https://example.com/third',
'response' => ['status' => 200, 'body' => 'third'],
]);

\DrevOps\VortexTooling\request_get('https://example.com/first');
\DrevOps\VortexTooling\request_get('https://example.com/second');

$this->expectException(\PHPUnit\Framework\AssertionFailedError::class);
$this->expectExceptionMessage('Not all mocked request responses were consumed. Expected 3 request(s), but only 2 request(s) were made.');

// Manually trigger the check that normally happens in tearDown().
$this->mockRequestAssertAllMocksConsumed();
}

public function testMockRequestFailureArgumentExceptionUrl(): void {
$this->mockRequest([
'response' => ['status' => 200],
]);

$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Mocked request response must include "url" key to specify expected URL.');

\DrevOps\VortexTooling\request_get('https://example.com/api');
}

public function testMockRequestFailureAssertUnexpectedUrl(): void {
$this->mockRequest([
'url' => 'https://example.com/expected',
'response' => ['status' => 200],
]);

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('request made to unexpected URL. Expected "https://example.com/expected", got "https://example.com/actual".');

\DrevOps\VortexTooling\request_get('https://example.com/actual');
}

public function testMockRequestGetScriptPassingSuccess(): void {
$this->mockRequest([
'url' => 'https://example.com/api',
'response' => [
'status' => 200,
'body' => 'success response',
],
]);

$output = $this->runScript('test-request-get-passing', 'tests/Fixtures');

$this->assertStringContainsString('Script will call request_get', $output);
$this->assertStringContainsString('Response status: 200', $output);
$this->assertStringContainsString('Response ok: true', $output);
$this->assertStringContainsString('Response body: success response', $output);
}

public function testMockRequestGetScriptFailingSuccess(): void {
$this->mockRequest([
'url' => 'https://example.com/not-found',
'response' => [
'ok' => FALSE,
'status' => 404,
'body' => 'Not Found',
],
]);

$output = $this->runScript('test-request-get-failing', 'tests/Fixtures');

$this->assertStringContainsString('Script will call request_get expecting failure', $output);
$this->assertStringContainsString('Response status: 404', $output);
$this->assertStringContainsString('Response ok: false', $output);
$this->assertStringContainsString('Request failed as expected', $output);
}

public function testMockRequestGetScriptCustomSuccess(): void {
$this->mockRequest([
'url' => 'https://example.com/api',
'response' => [
'status' => 201,
'body' => 'custom response',
],
]);

$output = $this->runScript('test-request-get-passing', 'tests/Fixtures');

$this->assertStringContainsString('Response status: 201', $output);
$this->assertStringContainsString('Response body: custom response', $output);
}

public function testMockRequestGetScriptDefaultsSuccess(): void {
$this->mockRequest([
'url' => 'https://example.com/api',
'response' => [
'status' => 200,
],
]);

$output = $this->runScript('test-request-get-passing', 'tests/Fixtures');

$this->assertStringContainsString('Response status: 200', $output);
$this->assertStringContainsString('Response ok: true', $output);
}

public function testMockRequestGetScriptNetworkErrorSuccess(): void {
$this->mockRequest([
'url' => 'https://example.com/not-found',
'response' => [
'ok' => FALSE,
'status' => 0,
'body' => FALSE,
'error' => 'Could not resolve host',
],
]);

$output = $this->runScript('test-request-get-failing', 'tests/Fixtures');

$this->assertStringContainsString('Response status: 0', $output);
$this->assertStringContainsString('Response ok: false', $output);
}

public function testMockRequestPostScriptPassingSuccess(): void {
$this->mockRequest([
'url' => 'https://example.com/api',
'method' => 'POST',
'response' => [
'status' => 201,
'body' => 'created',
],
]);

$output = $this->runScript('test-request-post-passing', 'tests/Fixtures');

$this->assertStringContainsString('Script will call request_post', $output);
$this->assertStringContainsString('Response status: 201', $output);
$this->assertStringContainsString('Response ok: true', $output);
$this->assertStringContainsString('POST succeeded', $output);
}

public function testMockRequestPostScriptFailingSuccess(): void {
$this->mockRequest([
'url' => 'https://example.com/error',
'method' => 'POST',
'response' => [
'ok' => FALSE,
'status' => 500,
'body' => 'Internal Server Error',
],
]);

$output = $this->runScript('test-request-post-failing', 'tests/Fixtures');

$this->assertStringContainsString('Script will call request_post expecting failure', $output);
$this->assertStringContainsString('Response status: 500', $output);
$this->assertStringContainsString('Response ok: false', $output);
$this->assertStringContainsString('POST failed as expected', $output);
}

public function testMockRequestMultipleScriptSuccess(): void {
$this->mockRequest([
'url' => 'https://example.com/first',
'response' => ['status' => 200, 'body' => 'first'],
]);

$this->mockRequest([
'url' => 'https://example.com/second',
'method' => 'POST',
'response' => ['status' => 201, 'body' => 'second'],
]);

$this->mockRequest([
'url' => 'https://example.com/third',
'method' => 'PUT',
'response' => ['status' => 200, 'body' => 'third'],
]);

$output = $this->runScript('test-request-multiple', 'tests/Fixtures');

$this->assertStringContainsString('Script will call request functions multiple times', $output);
$this->assertStringContainsString('First call status: 200', $output);
$this->assertStringContainsString('Second call status: 201', $output);
$this->assertStringContainsString('Third call status: 200', $output);
$this->assertStringContainsString('Script completed', $output);
}

public function testMockRequestMultipleScriptMoreCallsFailure(): void {
$this->mockRequest([
'url' => 'https://example.com/first',
'response' => ['status' => 200, 'body' => 'first'],
]);

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('curl_init() called more times than mocked responses. Expected 1 request(s), but attempting request #2');

$this->runScript('test-request-multiple', 'tests/Fixtures');
}

public function testMockRequestMultipleScriptLessCallsFailure(): void {
$this->mockRequest([
'url' => 'https://example.com/first',
'response' => ['status' => 200, 'body' => 'first'],
]);

$this->mockRequest([
'url' => 'https://example.com/second',
'method' => 'POST',
'response' => ['status' => 201, 'body' => 'second'],
]);

$this->mockRequest([
'url' => 'https://example.com/third',
'method' => 'PUT',
'response' => ['status' => 200, 'body' => 'third'],
]);

$this->mockRequest([
'url' => 'https://example.com/fourth',
'response' => ['status' => 200, 'body' => 'fourth'],
]);

$this->expectException(\PHPUnit\Framework\AssertionFailedError::class);
$this->expectExceptionMessage('Not all mocked request responses were consumed. Expected 4 request(s), but only 3 request(s) were made.');

$this->runScript('test-request-multiple', 'tests/Fixtures');

// Manually trigger the check that normally happens in tearDown().
$this->mockRequestAssertAllMocksConsumed();
}

public function testMockRequestGetScriptFailureArgumentExceptionUrl(): void {
$this->mockRequest([
'response' => ['status' => 200],
]);

$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Mocked request response must include "url" key to specify expected URL.');

$this->runScript('test-request-get-passing', 'tests/Fixtures');
}

public function testMockRequestGetScriptFailureAssertUnexpectedUrl(): void {
$this->mockRequest([
'url' => 'https://wrong.com/api',
'response' => ['status' => 200],
]);

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('request made to unexpected URL. Expected "https://wrong.com/api", got "https://example.com/api".');

$this->runScript('test-request-get-passing', 'tests/Fixtures');
}

}
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

Import functions/classes instead of using fully‑qualified names in the body

PHPCS is flagging all \DrevOps\VortexTooling\request_* and \RuntimeException, \InvalidArgumentException, \PHPUnit\Framework\AssertionFailedError usages. Bringing these in via use/use function will fix the pipeline and keep the tests cleaner.

Suggested patch:

@@
-namespace DrevOps\VortexTooling\Tests\Self;
-
-use DrevOps\VortexTooling\Tests\Unit\CoversClass;
-use DrevOps\VortexTooling\Tests\Unit\UnitTestCase;
+namespace DrevOps\VortexTooling\Tests\Self;
+
+use DrevOps\VortexTooling\Tests\Unit\CoversClass;
+use DrevOps\VortexTooling\Tests\Unit\UnitTestCase;
+use InvalidArgumentException;
+use PHPUnit\Framework\AssertionFailedError;
+use RuntimeException;
+use function DrevOps\VortexTooling\request;
+use function DrevOps\VortexTooling\request_get;
+use function DrevOps\VortexTooling\request_post;
@@
-    $result = \DrevOps\VortexTooling\request_get('https://example.com/api');
+    $result = request_get('https://example.com/api');
@@
-    $result = \DrevOps\VortexTooling\request_get('https://example.com/api');
+    $result = request_get('https://example.com/api');
@@
-    $result = \DrevOps\VortexTooling\request_get('https://example.com/api');
+    $result = request_get('https://example.com/api');
@@
-    $result = \DrevOps\VortexTooling\request_get('https://example.com/not-found');
+    $result = request_get('https://example.com/not-found');
@@
-    $result = \DrevOps\VortexTooling\request_get('https://example.com/error');
+    $result = request_get('https://example.com/error');
@@
-    $result = \DrevOps\VortexTooling\request_get('https://example.com/timeout');
+    $result = request_get('https://example.com/timeout');
@@
-    $result = \DrevOps\VortexTooling\request_post('https://example.com/api', '{"key":"value"}');
+    $result = request_post('https://example.com/api', '{"key":"value"}');
@@
-    $result = \DrevOps\VortexTooling\request_post(
+    $result = request_post(
@@
-    $result = \DrevOps\VortexTooling\request('https://example.com/resource', ['method' => 'PUT']);
+    $result = request('https://example.com/resource', ['method' => 'PUT']);
@@
-    $result1 = \DrevOps\VortexTooling\request_get('https://example.com/first');
+    $result1 = request_get('https://example.com/first');
@@
-    $result2 = \DrevOps\VortexTooling\request_post('https://example.com/second', 'data');
+    $result2 = request_post('https://example.com/second', 'data');
@@
-    $result3 = \DrevOps\VortexTooling\request('https://example.com/third', ['method' => 'PUT']);
+    $result3 = request('https://example.com/third', ['method' => 'PUT']);
@@
-    \DrevOps\VortexTooling\request_get('https://example.com/first');
-    \DrevOps\VortexTooling\request_get('https://example.com/second');
+    request_get('https://example.com/first');
+    request_get('https://example.com/second');
@@
-    \DrevOps\VortexTooling\request_get('https://example.com/third');
+    request_get('https://example.com/third');
@@
-    \DrevOps\VortexTooling\request_get('https://example.com/first');
-    \DrevOps\VortexTooling\request_get('https://example.com/second');
+    request_get('https://example.com/first');
+    request_get('https://example.com/second');
@@
-    \DrevOps\VortexTooling\request_get('https://example.com/api');
+    request_get('https://example.com/api');
@@
-    \DrevOps\VortexTooling\request_get('https://example.com/actual');
+    request_get('https://example.com/actual');
@@
-    $this->expectException(\RuntimeException::class);
+    $this->expectException(RuntimeException::class);
@@
-    $this->expectException(\PHPUnit\Framework\AssertionFailedError::class);
+    $this->expectException(AssertionFailedError::class);
@@
-    $this->expectException(\InvalidArgumentException::class);
+    $this->expectException(InvalidArgumentException::class);
@@
-    $this->expectException(\RuntimeException::class);
+    $this->expectException(RuntimeException::class);

(You can similarly replace the remaining \DrevOps\VortexTooling\request_* invocations, if any, with the imported functions.)

The rest of the test logic and coverage look solid once standards are satisfied.

🧰 Tools
🪛 GitHub Actions: Vortex - Test tooling

[error] 37-37: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 57-57: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 75-75: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 93-93: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 110-110: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 128-128: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 146-146: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 163-163: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 184-184: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 218-218: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 222-222: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 226-226: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 242-242: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 243-243: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 248-248: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 267-267: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 268-268: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 270-270: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 285-285: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 297-297: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)


[error] 480-480: PHPCS: Namespaced classes/interfaces/traits should be referenced with use statements (Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing)

🤖 Prompt for AI Agents
.vortex/tooling/tests/Self/MockRequestSelfTest.php lines 7-512: PHPCS flags
full-qualified names for the request helpers and exception classes; import them
at the top and use short names in the test body. Add appropriate use statements
(e.g. use function DrevOps\VortexTooling\request_get; use function
DrevOps\VortexTooling\request_post; use function DrevOps\VortexTooling\request;
and use RuntimeException; use InvalidArgumentException; use
PHPUnit\Framework\AssertionFailedError;) and then replace all occurrences of the
leading-backslash qualified names in the file with the imported short names
(request_get, request_post, request, RuntimeException, InvalidArgumentException,
AssertionFailedError).

@AlexSkrypnyk AlexSkrypnyk force-pushed the feature/1192-tooling-package branch 2 times, most recently from 9b2f100 to daaa5df Compare November 21, 2025 01:53
@github-actions github-actions bot temporarily deployed to commit November 21, 2025 01:55 Inactive
@AlexSkrypnyk AlexSkrypnyk force-pushed the feature/1192-tooling-package branch from daaa5df to ad38200 Compare November 21, 2025 01:59
@github-actions github-actions bot temporarily deployed to commit November 21, 2025 02:01 Inactive
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.vortex/CLAUDE.md (1)

759-766: Update “three systems” summary to include tooling

This section still describes .vortex/ as having “three independent systems” (docs, installer, tests), but earlier sections now document four subsystems (docs, installer, tooling, tests) and a separate Tooling Tests subsystem.

Consider updating this summary block to stay aligned, for example:

-### Test Harness (.vortex/) - Three Separate Systems
+### Test Harness (.vortex/) - Four Independent Systems
@@
-**Critical Understanding**: The `.vortex/` directory contains three **completely independent subsystems**:
+**Critical Understanding**: The `.vortex/` directory contains four **completely independent subsystems**:
@@
-1. **`.vortex/docs/`** - Docusaurus website (Node.js/React)
-2. **`.vortex/installer/`** - PHP installer application (Symfony Console)
-3. **`.vortex/tests/`** - Template testing harness (PHPUnit + BATS)
+1. **`.vortex/docs/`**     - Docusaurus website (Node.js/React)
+2. **`.vortex/installer/`** - PHP installer application (Symfony Console)
+3. **`.vortex/tooling/`**   - PHP helpers and notification tooling (with its own tests)
+4. **`.vortex/tests/`**     - Template testing harness (PHPUnit + BATS + manual)

This keeps the high-level count consistent with the rest of the document.

♻️ Duplicate comments (20)
.vortex/tooling/rector.php (2)

38-48: Include src directory to ensure helpers.php is processed by Rector.

The current paths list specific src/notify* subdirectories but exclude src/helpers.php, which exists and should be modernized by Rector. Update to include the entire src directory.

Apply this diff:

 ->withPaths([
-  __DIR__ . '/src/notify',
-  __DIR__ . '/src/notify-email',
-  __DIR__ . '/src/notify-github',
-  __DIR__ . '/src/notify-jira',
-  __DIR__ . '/src/notify-newrelic',
-  __DIR__ . '/src/notify-slack',
-  __DIR__ . '/src/notify-webhook',
+  __DIR__ . '/src',
   __DIR__ . '/tests',
 ])

75-77: Align PHP version comments with configuration.

The comments state "modernizes syntax to PHP 8.3" and "rules from PHP 5.3 through 8.3", but the configuration uses php82: TRUE. Either update the comments to reflect PHP 8.2 or change the configuration to php83: TRUE to match the comments.

Apply this diff to match comments (if PHP 8.3 is intended):

-  ->withPhpSets(php82: TRUE)
+  ->withPhpSets(php83: TRUE)

Or update comments to reflect PHP 8.2:

-  // PHP version upgrade sets - modernizes syntax to PHP 8.3.
-  // Includes all rules from PHP 5.3 through 8.3.
+  // PHP version upgrade sets - modernizes syntax to PHP 8.2.
+  // Includes all rules from PHP 5.3 through 8.2.
   ->withPhpSets(php82: TRUE)
.github/workflows/vortex-test-tooling.yml (1)

53-57: Clarify GITHUB_TOKEN vs PACKAGE_TOKEN mapping

This step exposes GITHUB_TOKEN while sourcing it from secrets.PACKAGE_TOKEN. That’s easy to misread as the standard GitHub token rather than a higher-privilege PAT.

Either rename the env var or add a short comment explaining why PACKAGE_TOKEN is mapped here.

       - name: Run tests
         run: composer test-coverage
         working-directory: .vortex/tooling
         env:
-          GITHUB_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
+          PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
+          # If tooling requires a PAT, use PACKAGE_TOKEN instead of the default GITHUB_TOKEN.

Or, if it really should be the default token, switch to secrets.GITHUB_TOKEN.

.vortex/tooling/test_plan.md (1)

10-26: Fix remaining markdownlint issues (fences, URLs, numbering)

markdownlint is still flagging this file for:

  • Fenced blocks not surrounded by blank lines (MD031).
  • Bare URLs (MD034).
  • Script Tests list numbered 15–26 instead of 1–12 (MD029).

You can address these with small structural tweaks, e.g.:

-**Mock response structure:**
-```php
+**Mock response structure:**
+
+```php
@@
-```
+```
+
@@
-15. **testMockRequestGetScriptPassingSuccess** - GET through script (uses test-request-get-passing)
-16. **testMockRequestGetScriptFailingSuccess** - GET 404 through script (uses test-request-get-failing)
+1. **testMockRequestGetScriptPassingSuccess** - GET through script (uses test-request-get-passing)
+2. **testMockRequestGetScriptFailingSuccess** - GET 404 through script (uses test-request-get-failing)
@@
--   - Mock: url="https://example.com"
--   - Call: url="https://different.com"
+-   - Mock: url="<https://example.com>"
+-   - Call: url="<https://different.com>"

Apply the same “blank line around fences” and “wrap URLs in <...>” pattern to the other sections to clear the remaining lint errors.

Also applies to: 90-103, 110-217

.vortex/tooling/tests/Self/MockRequestSelfTest.php (1)

1-499: Test coverage is comprehensive and well-structured.

The self-test suite thoroughly validates HTTP request mocking across success, failure, custom values, defaults, network errors, multiple sequential calls, and script-based validations. The test design follows established patterns from MockPassthruSelfTest and MockQuitSelfTest.

.vortex/tooling/src/notify-newrelic (1)

1-176: Script structure and notification flow are well-designed.

The New Relic notification script properly handles environment variables, auto-generates revision when needed, discovers app IDs by name, validates configuration, and posts deployment notifications with appropriate error handling for the API response.

.vortex/tooling/src/notify-github (2)

61-71: Relax deployment ID length upper bound to future‑proof validation

validate_deployment_id() currently requires 9–11 digits:

// Check length (9-11 digits) and numeric format.
return $length >= 9 && $length <= 11 && ctype_digit($id_str);

This hard upper bound risks rejecting valid deployment IDs if GitHub increases their size. You can keep the numeric and minimum‑length checks while dropping the upper bound, e.g.:

-  // Check length (9-11 digits) and numeric format.
-  return $length >= 9 && $length <= 11 && ctype_digit($id_str);
+  // Check length (at least 9 digits) and numeric format.
+  return $length >= 9 && ctype_digit($id_str);

88-92: Explicitly mark JSON POSTs with Content-Type: application/json

Both deployment creation and status update send JSON bodies but don’t set a JSON content type. Making this explicit avoids relying on any request_post() defaults and lines up with GitHub’s expectations for JSON requests.

Suggested change:

   $response = request_post($url, json_encode($payload_data), [
-    'Authorization: token ' . $token,
-    'Accept: application/vnd.github.v3+json',
+    'Authorization: token ' . $token,
+    'Accept: application/vnd.github.v3+json',
+    'Content-Type: application/json',
   ]);
@@
   $response = request_post($url, json_encode($status_data), [
-    'Accept: application/vnd.github.v3+json',
-    'Authorization: token ' . $token,
+    'Accept: application/vnd.github.v3+json',
+    'Authorization: token ' . $token,
+    'Content-Type: application/json',
   ]);

Also applies to: 146-150

.vortex/tooling/src/notify-email (1)

117-151: Filter out empty recipients after explode/trim

$recipients is currently:

$recipients = array_map(trim(...), explode(',', $recipients));

If the env var contains extra commas or whitespace, this yields empty strings, which then produce blank To: headers and unnecessary sendmail / mail calls.

Filter out empty entries before looping:

-// Parse and send to each recipient.
-$sent = [];
-$recipients = array_map(trim(...), explode(',', $recipients));
+// Parse and send to each recipient.
+$sent = [];
+$recipients = array_filter(
+  array_map(trim(...), explode(',', $recipients)),
+  static fn(string $value): bool => $value !== ''
+);
.vortex/tooling/src/notify-slack (1)

137-144: Also validate Slack webhook response body, not just HTTP 200

Currently you only fail when status !== 200:

$response = request_post($webhook, $payload, [
  'Content-Type: application/json',
]);

if ($response['status'] !== 200) {
  fail('Unable to send notification to Slack. HTTP status: %s', $response['status']);
}

Slack can respond with HTTP 200 but an error string in the body. To catch misconfigurations and invalid payloads, consider also checking the body, e.g.:

 $response = request_post($webhook, $payload, [
   'Content-Type: application/json',
 ]);
 
-$if ($response['status'] !== 200) {
-  fail('Unable to send notification to Slack. HTTP status: %s', $response['status']);
-}
+$body = trim((string) ($response['body'] ?? ''));
+if ($response['status'] !== 200 || ($body !== '' && $body !== 'ok')) {
+  fail(
+    'Unable to send notification to Slack. HTTP status: %s, body: %s',
+    $response['status'],
+    $body !== '' ? $body : '<empty>'
+  );
+}
.vortex/tooling/src/notify-jira (3)

91-112: Fix issue extraction: use extract_issue() and exit when label has no issue

extract_issue() is defined but not used, and the inline preg_match block doesn’t exit when no issue is found:

task('Extracting issue');
if (!preg_match('/([^\/]+\/)?([A-Za-z0-9]+\-\d+)/', $label, $matches)) {
  pass('Deployment label %s does not contain issue number.', $label);
}
$issue = $matches[2];
note('Found issue %s.', $issue);

If the label lacks an issue key, $matches and $issue are undefined yet the script continues, leading to runtime notices and broken requests later.

Leverage the helper and bail out cleanly:

 task('Extracting issue');
-if (!preg_match('/([^\/]+\/)?([A-Za-z0-9]+\-\d+)/', $label, $matches)) {
-  pass('Deployment label %s does not contain issue number.', $label);
-}
-$issue = $matches[2];
-note('Found issue %s.', $issue);
+$issue = extract_issue($label);
+if ($issue === NULL) {
+  pass('Deployment label %s does not contain issue number.', $label);
+  exit(0);
+}
+note('Found issue %s.', $issue);

This removes duplication, uses the dedicated helper, and prevents the notifier from running with an invalid/undefined issue key.


83-90: Clean up default message template and unused $message_content

Two related issues around the message template:

  1. The default heredoc has an extra double quote:
$message = $message ?: <<<EOT
## This is an automated message ##"

This will render an unintended " at the end of the first line. It should be:

-$message = $message ?: <<<EOT
-## This is an automated message ##"
+$message = $message ?: <<<EOT
+## This is an automated message ##
  1. $message_content is built with replace_tokens() but never used; the ADF payload below is hard-coded, so VORTEX_NOTIFY_JIRA_MESSAGE currently has no effect:
// Build message by replacing tokens.
$message_content = replace_tokens($message, [
  'project' => $project,
  'label' => $label,
  'timestamp' => $timestamp,
  'environment_url' => $env_url,
  'login_url' => $login_url,
]);

Either wire $message_content into the ADF structure (so custom templates are honored) or remove this block and the related env var from the docs to avoid dead code and misleading configuration.

Also applies to: 140-151


130-135: Replace raw echo calls with standard logging helpers

There are several echo statements mixed into a script that otherwise uses task(), note(), pass(), fail():

echo "success\n";
echo '       Discovering transition ID for ' . $transition . '...';
echo "success\n";
echo '       Discovering user ID for ' . $assignee_email . '...';
echo "success\n";

For consistent output formatting and easier parsing, prefer the existing helpers, e.g.:

-if (strlen((string) $account_id) < 24) {
-  fail('Unable to authenticate');
-}
-echo "success\n";
+if (strlen((string) $account_id) < 24) {
+  fail('Unable to authenticate');
+}
+note('success');
@@
-  echo '       Discovering transition ID for ' . $transition . '...';
+  note('Discovering transition ID for ' . $transition . '...');
@@
-  echo "success\n";
+  note('success');
@@
-  echo '       Discovering user ID for ' . $assignee_email . '...';
+  note('Discovering user ID for ' . $assignee_email . '...');
@@
-  echo "success\n";
+  note('success');

Also applies to: 212-213, 235-235, 250-251, 269-269

.vortex/tooling/src/notify (1)

68-88: Use DIRECTORY_SEPARATOR when building script paths for portability

The per‑channel script path is currently built with a hardcoded /:

$script = __DIR__ . ('/notify-' . $channel);

For better cross‑platform compatibility (e.g., Windows paths), prefer DIRECTORY_SEPARATOR:

-  $script = __DIR__ . ('/notify-' . $channel);
+  $script = __DIR__ . DIRECTORY_SEPARATOR . 'notify-' . $channel;
.vortex/tooling/src/notify-webhook (2)

79-92: Handle json_encode() failure in json_escape_value() to avoid TypeError

json_escape_value() calls json_encode() and immediately substr()s the result. If json_encode() returns false (e.g., malformed UTF‑8), substr(false, ...) will throw a TypeError. Add a strict failure check before calling substr().

 function json_escape_value(string $value): string {
-  $escaped = json_encode($value);
-  // Remove surrounding quotes.
-  return substr($escaped, 1, -1);
+  $escaped = json_encode($value);
+  if ($escaped === FALSE) {
+    fail('Failed to JSON-encode value for webhook payload: %s (error: %s)', $value, json_last_error_msg());
+  }
+  // Remove surrounding quotes.
+  return substr($escaped, 1, -1);
 }

112-138: Avoid logging full webhook URL; use masked domain in error message

On failure you log the full $webhook_url, which may embed secrets (tokens in path/query). You already compute $webhook_domain for masking; reuse it in the error.

 $webhook_domain = preg_replace('|(https?://[^/]+).*|', '$1', $webhook_url);
@@
-if ($response['status'] !== (int) $expected_status) {
-  fail(sprintf('Unable to send notification to webhook %s.', $webhook_url));
-}
+if ($response['status'] !== (int) $expected_status) {
+  fail(sprintf('Unable to send notification to webhook %s/***.', $webhook_domain));
+}
.vortex/tooling/tests/Traits/MockTrait.php (1)

83-96: Align passthru mock docblock with actual response keys

The docblock still references exit_code, but the structure and implementation use result_code plus cmd/return. This can confuse consumers of mockPassthruMultiple().

-   * @param array<int, array{cmd:string, output?: string, result_code?: int, return?: NULL|FALSE}> $responses
-   *   Array of responses to return for each passthru call.
-   *   Each response should have:
-   *   - output: The output to echo
-   *   - exit_code: The exit code to set (0 for success).
+   * @param array<int, array{cmd:string, output?: string, result_code?: int, return?: NULL|FALSE}> $responses
+   *   Array of responses to return for each passthru call.
+   *   Each response should have:
+   *   - cmd: Expected command (required)
+   *   - output: Output to echo (optional)
+   *   - result_code: Exit code to set (optional, default 0)
+   *   - return: Return value (optional, NULL or FALSE)
.vortex/tooling/src/helpers.php (3)

22-31: Harden execute_override() against path traversal in $name

$name is concatenated directly into $dir . DIRECTORY_SEPARATOR . $name. While current callers pass basename(__FILE__), future usage with untrusted input could enable path traversal. Validate that $name has no directory separators before building the path.

 function execute_override(string $name): void {
+  // Prevent path traversal by ensuring name doesn't contain directory separators.
+  if (str_contains($name, DIRECTORY_SEPARATOR) || str_contains($name, '/') || str_contains($name, '\\')) {
+    fail('Invalid script name for override: %s', $name);
+  }
+
   $dir = getenv('VORTEX_TOOLING_CUSTOM_DIR');
   if ($dir) {
     $path = $dir . DIRECTORY_SEPARATOR . $name;

294-298: Remove unreachable exit(1) after fail() in request()

fail() already prints the error and calls exit(1), so the subsequent exit(1) is dead code.

 function request(string $url, array $options = []): array {
   if (!function_exists('curl_init')) {
     fail('curl extension is not available.');
-    exit(1);
   }

300-301: Check curl_init() return value before using the handle

curl_init($url) can return false on failure. Passing false into curl_setopt_array() will cause a TypeError. Add a strict check and fail early.

-  $ch = curl_init($url);
+  $ch = curl_init($url);
+  if ($ch === FALSE) {
+    fail('Failed to initialize curl for URL: %s', $url);
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 482ed6a and ad38200.

📒 Files selected for processing (36)
  • .github/workflows/vortex-test-tooling.yml (1 hunks)
  • .vortex/CLAUDE.md (9 hunks)
  • .vortex/tooling/.gitignore (1 hunks)
  • .vortex/tooling/CLAUDE.md (1 hunks)
  • .vortex/tooling/composer.json (1 hunks)
  • .vortex/tooling/phpcs.xml (1 hunks)
  • .vortex/tooling/phpstan.neon (1 hunks)
  • .vortex/tooling/phpunit.xml (1 hunks)
  • .vortex/tooling/rector.php (1 hunks)
  • .vortex/tooling/scripts_conversion_plan.md (1 hunks)
  • .vortex/tooling/src/helpers.php (1 hunks)
  • .vortex/tooling/src/notify (1 hunks)
  • .vortex/tooling/src/notify-email (1 hunks)
  • .vortex/tooling/src/notify-github (1 hunks)
  • .vortex/tooling/src/notify-jira (1 hunks)
  • .vortex/tooling/src/notify-newrelic (1 hunks)
  • .vortex/tooling/src/notify-slack (1 hunks)
  • .vortex/tooling/src/notify-webhook (1 hunks)
  • .vortex/tooling/test_plan.md (1 hunks)
  • .vortex/tooling/tests/Exceptions/QuitErrorException.php (1 hunks)
  • .vortex/tooling/tests/Exceptions/QuitSuccessException.php (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-passthru-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-passthru-multiple (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-passthru-passing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-quit-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-quit-passing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-get-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-get-passing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-multiple (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-post-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-post-passing (1 hunks)
  • .vortex/tooling/tests/Self/MockPassthruSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockQuitSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Traits/MockTrait.php (1 hunks)
  • .vortex/tooling/tests/Unit/UnitTestCase.php (1 hunks)
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-09-13T04:14:41.765Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 2011
File: .vortex/tests/phpunit/Traits/Steps/StepAhoyTrait.php:20-23
Timestamp: 2025-09-13T04:14:41.765Z
Learning: In .vortex/tests/phpunit PHPUnit tests using cmd/cmdFail helpers, patterns in the expected output arrays support '*' markers for positive assertions (text must be present) and '!' markers for negative assertions (text must NOT be present). The '*' markers are NOT literal text matches but rather indicate expected presence.

Applied to files:

  • .vortex/tooling/tests/Fixtures/test-quit-passing
  • .vortex/tooling/tests/Fixtures/test-request-get-failing
  • .vortex/tooling/CLAUDE.md
  • .vortex/tooling/tests/Fixtures/test-passthru-failing
  • .vortex/tooling/tests/Fixtures/test-request-post-failing
  • .vortex/tooling/tests/Fixtures/test-request-post-passing
  • .vortex/tooling/tests/Self/MockQuitSelfTest.php
  • .vortex/tooling/phpunit.xml
  • .vortex/tooling/tests/Fixtures/test-request-get-passing
  • .vortex/tooling/test_plan.md
  • .vortex/tooling/tests/Fixtures/test-passthru-multiple
  • .vortex/CLAUDE.md
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
  • .vortex/tooling/tests/Exceptions/QuitSuccessException.php
  • .vortex/tooling/tests/Exceptions/QuitErrorException.php
  • .vortex/tooling/tests/Fixtures/test-passthru-passing
  • .vortex/tooling/tests/Self/MockPassthruSelfTest.php
  • .vortex/tooling/tests/Fixtures/test-quit-failing
  • .vortex/tooling/tests/Fixtures/test-request-multiple
📚 Learning: 2025-06-01T08:08:59.071Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1693
File: .vortex/installer/tests/Fixtures/install/deploy_type_none_gha/.github/workflows/build-test-deploy.yml:1-4
Timestamp: 2025-06-01T08:08:59.071Z
Learning: Test fixture files (typically located in directories containing "fixture" or "test") should not be reviewed for code quality issues as they may intentionally contain incomplete, invalid, or malformed data for testing purposes.

Applied to files:

  • .vortex/tooling/tests/Fixtures/test-request-get-failing
  • .vortex/tooling/tests/Fixtures/test-request-post-failing
  • .vortex/tooling/tests/Fixtures/test-quit-failing
📚 Learning: 2025-06-02T00:41:36.321Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1697
File: .vortex/installer/tests/Fixtures/install/provision_database_lagoon/docs/onboarding.md:16-17
Timestamp: 2025-06-02T00:41:36.321Z
Learning: DO NOT EVER REVIEW FILES IN .vortex/installer/tests/Fixtures/ directory - these are test fixtures and should not be reviewed under any circumstances.

Applied to files:

  • .vortex/tooling/tests/Fixtures/test-request-get-failing
  • .vortex/tooling/tests/Fixtures/test-passthru-failing
  • .vortex/CLAUDE.md
📚 Learning: 2025-05-29T12:15:32.188Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 0
File: :0-0
Timestamp: 2025-05-29T12:15:32.188Z
Learning: Do not review files in `.vortex/installer/tests/Fixtures/install` directory as they are test fixtures.

Applied to files:

  • .vortex/tooling/tests/Fixtures/test-request-get-failing
  • .vortex/tooling/tests/Fixtures/test-passthru-failing
  • .vortex/tooling/.gitignore
  • .vortex/CLAUDE.md
📚 Learning: 2025-08-08T12:02:24.652Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1896
File: .vortex/tests/bats/unit/download-db-lagoon.bats:24-25
Timestamp: 2025-08-08T12:02:24.652Z
Learning: In .vortex/tests/bats/unit Bats tests using ../_helper.bash (run_steps), prefixing a STEPS entry with "- " denotes a negative assertion (the substring must NOT appear in output). Unprefixed entries are positive assertions. Example: "- Database dump refresh requested. Will create a new dump." asserts absence; "Database dump refresh requested. Will create a new dump." asserts presence.

Applied to files:

  • .vortex/tooling/tests/Fixtures/test-passthru-failing
  • .vortex/tooling/tests/Fixtures/test-passthru-multiple
  • .vortex/CLAUDE.md
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
  • .vortex/tooling/tests/Fixtures/test-passthru-passing
📚 Learning: 2025-07-23T01:16:30.963Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1816
File: .docker/cli.dockerfile:72-76
Timestamp: 2025-07-23T01:16:30.963Z
Learning: In the Vortex project, unauthenticated Composer installs should be allowed, so GitHub token secrets should not be marked as `required=true` in Docker build mounts. The conditional check `if [ -s /run/secrets/github_token ]` is the preferred approach to allow builds to proceed without a token when needed.

Applied to files:

  • .github/workflows/vortex-test-tooling.yml
📚 Learning: 2025-06-03T03:15:29.849Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1714
File: scripts/vortex/doctor.sh:69-70
Timestamp: 2025-06-03T03:15:29.849Z
Learning: In the Vortex project, logging functions (task(), info(), note(), pass(), fail(), warn()) are intentionally duplicated across all script files to make each script self-contained and independent, rather than sharing them through a common library. This design choice prioritizes script independence over reducing code duplication.

Applied to files:

  • .vortex/CLAUDE.md
🧬 Code graph analysis (8)
.vortex/tooling/tests/Self/MockQuitSelfTest.php (5)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-28)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
  • QuitSuccessException (12-28)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (14-57)
  • runScript (27-55)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/src/helpers.php (1)
  • quit (369-371)
.vortex/tooling/tests/Unit/UnitTestCase.php (1)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockTearDown (63-81)
.vortex/tooling/tests/Self/MockRequestSelfTest.php (3)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (14-57)
  • runScript (27-55)
.vortex/tooling/tests/Traits/MockTrait.php (4)
  • mockRequestGet (401-414)
  • mockRequestPost (432-446)
  • mockRequest (369-385)
  • mockRequestAssertAllMocksConsumed (454-465)
.vortex/tooling/src/helpers.php (3)
  • request_get (237-243)
  • request_post (265-272)
  • request (294-349)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • __construct (20-26)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
  • __construct (20-26)
.vortex/tooling/tests/Self/MockPassthruSelfTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (14-57)
  • runScript (27-55)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockPassthru (161-163)
  • mockPassthruAssertAllMocksConsumed (171-182)
.vortex/tooling/src/helpers.php (1)
.vortex/installer/src/Utils/Tui.php (3)
  • line (57-59)
  • output (38-43)
  • error (53-55)
.vortex/tooling/tests/Traits/MockTrait.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-28)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
  • QuitSuccessException (12-28)
.vortex/tooling/src/helpers.php (2)
  • fail (149-152)
  • quit (369-371)
🪛 LanguageTool
.vortex/tooling/CLAUDE.md

[grammar] ~140-~140: Ensure spelling is correct
Context: ...ssertAllMocksConsumed(): void #### Passthru Mocking **Response Structure**:php...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
.vortex/tooling/scripts_conversion_plan.md

9-9: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


16-16: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


17-17: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


17-17: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


43-43: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


48-48: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


59-59: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


68-68: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


92-92: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


93-93: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


101-101: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


103-103: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


112-112: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


125-125: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


126-126: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


139-139: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


140-140: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


146-146: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


147-147: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


166-166: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


167-167: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


182-182: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


278-278: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


283-283: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


386-386: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


393-393: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


394-394: Ordered list item prefix
Expected: 1; Actual: 6; Style: 1/2/3

(MD029, ol-prefix)


395-395: Ordered list item prefix
Expected: 2; Actual: 7; Style: 1/2/3

(MD029, ol-prefix)


396-396: Ordered list item prefix
Expected: 3; Actual: 8; Style: 1/2/3

(MD029, ol-prefix)


397-397: Ordered list item prefix
Expected: 4; Actual: 9; Style: 1/2/3

(MD029, ol-prefix)


398-398: Ordered list item prefix
Expected: 5; Actual: 10; Style: 1/2/3

(MD029, ol-prefix)


399-399: Ordered list item prefix
Expected: 6; Actual: 11; Style: 1/2/3

(MD029, ol-prefix)


401-401: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


402-402: Ordered list item prefix
Expected: 1; Actual: 12; Style: 1/2/3

(MD029, ol-prefix)


403-403: Ordered list item prefix
Expected: 2; Actual: 13; Style: 1/2/3

(MD029, ol-prefix)


404-404: Ordered list item prefix
Expected: 3; Actual: 14; Style: 1/2/3

(MD029, ol-prefix)


405-405: Ordered list item prefix
Expected: 4; Actual: 15; Style: 1/2/3

(MD029, ol-prefix)


406-406: Ordered list item prefix
Expected: 5; Actual: 16; Style: 1/2/3

(MD029, ol-prefix)


431-431: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


432-432: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


440-440: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


441-441: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


453-453: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


454-454: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


466-466: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


467-467: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


475-475: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


476-476: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


489-489: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


496-496: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


503-503: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

.vortex/tooling/CLAUDE.md

53-53: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


83-83: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


128-128: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


143-143: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


153-153: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


171-171: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


216-216: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


310-310: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


317-317: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


322-322: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


390-390: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


422-422: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


437-437: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


438-438: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


447-447: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


448-448: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


455-455: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


483-483: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


484-484: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


497-497: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


502-502: Ordered list item prefix
Expected: 1; Actual: 4; Style: 1/1/1

(MD029, ol-prefix)


646-646: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


727-727: Bare URL used

(MD034, no-bare-urls)


728-728: Bare URL used

(MD034, no-bare-urls)


729-729: Bare URL used

(MD034, no-bare-urls)

.vortex/tooling/test_plan.md

11-11: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


86-86: Bare URL used

(MD034, no-bare-urls)


87-87: Bare URL used

(MD034, no-bare-urls)


92-92: Ordered list item prefix
Expected: 1; Actual: 15; Style: 1/2/3

(MD029, ol-prefix)


93-93: Ordered list item prefix
Expected: 2; Actual: 16; Style: 1/2/3

(MD029, ol-prefix)


94-94: Ordered list item prefix
Expected: 3; Actual: 17; Style: 1/2/3

(MD029, ol-prefix)


95-95: Ordered list item prefix
Expected: 4; Actual: 18; Style: 1/2/3

(MD029, ol-prefix)


96-96: Ordered list item prefix
Expected: 5; Actual: 19; Style: 1/2/3

(MD029, ol-prefix)


97-97: Ordered list item prefix
Expected: 6; Actual: 20; Style: 1/2/3

(MD029, ol-prefix)


98-98: Ordered list item prefix
Expected: 7; Actual: 21; Style: 1/2/3

(MD029, ol-prefix)


99-99: Ordered list item prefix
Expected: 8; Actual: 22; Style: 1/2/3

(MD029, ol-prefix)


100-100: Ordered list item prefix
Expected: 9; Actual: 23; Style: 1/2/3

(MD029, ol-prefix)


101-101: Ordered list item prefix
Expected: 10; Actual: 24; Style: 1/2/3

(MD029, ol-prefix)


102-102: Ordered list item prefix
Expected: 11; Actual: 25; Style: 1/2/3

(MD029, ol-prefix)


103-103: Ordered list item prefix
Expected: 12; Actual: 26; Style: 1/2/3

(MD029, ol-prefix)


113-113: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


134-134: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


155-155: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


176-176: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


197-197: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: build (1)
  • GitHub Check: build (0)
  • GitHub Check: vortex-test-workflow (0)
  • GitHub Check: vortex-test-workflow (3)
  • GitHub Check: vortex-test-workflow (1)
  • GitHub Check: vortex-test-workflow (4)
  • GitHub Check: vortex-test-workflow (2)
  • GitHub Check: vortex-test-common
  • GitHub Check: vortex-test-installer (8.4)
  • GitHub Check: vortex-test-installer (8.3)
🔇 Additional comments (24)
.vortex/tooling/.gitignore (1)

1-3: LGTM!

The gitignore entries are appropriate for a PHP library package. Ignoring composer.lock is correct for libraries (it should only be committed for applications), and the .logs directory aligns with the test output configuration in phpunit.xml.

.vortex/tooling/tests/Fixtures/test-passthru-passing (1)

1-19: LGTM!

The test fixture follows all documented conventions: shebang, strict types, proper namespace, loads helpers, and produces testable output. The logic correctly demonstrates passthru behavior for testing purposes.

.vortex/tooling/phpstan.neon (1)

1-28: LGTM!

The PHPStan configuration is appropriately strict (level 9) and the ignore rules are justified:

  • The iterable type ignore for tests is reasonable for data providers
  • The function-not-found ignore for tests/Self/* is appropriate since those tests exercise mocked functions that won't exist during static analysis
.vortex/tooling/phpunit.xml (1)

1-42: LGTM!

The PHPUnit configuration is comprehensive and correct. The schema version (12.4) now matches the PHPUnit version constraint in composer.json (^12.4.2), addressing the previous concern. The strict settings for warnings, errors, and notices ensure high test quality.

.vortex/tooling/phpcs.xml (1)

1-47: LGTM!

The PHPCS configuration appropriately bases on Drupal standards with custom additions and provides sensible test-file exclusions. The explicit file listings (while potentially redundant if PHPCS scans recursively) are clear and functional. The previous nitpick about simplification remains valid but non-blocking.

.vortex/tooling/composer.json (1)

1-65: LGTM!

The Composer manifest is well-structured with appropriate metadata, autoloading, and tooling scripts. The PHP constraint placement in require-dev was previously reviewed and correctly retained for an internal tooling package.

.vortex/tooling/CLAUDE.md (1)

1-733: LGTM!

The documentation is comprehensive and well-structured, covering package overview, helper functions, testing architecture, naming conventions, and best practices. The previous API inconsistency concern was addressed. The static analysis formatting hints (blank lines, list prefixes, bare URLs) are minor style suggestions that don't affect documentation quality.

.github/workflows/vortex-test-tooling.yml (1)

18-21: Confirm setup-php supports all matrix PHP versions

The matrix includes '8.5'; please confirm that shivammathur/setup-php supports this version alias in your GitHub-hosted runners so the workflow doesn’t start failing when that job is selected.

You can verify against the action docs / release notes for the pinned SHA.

.vortex/CLAUDE.md (1)

18-22: Tooling subsystem and testing docs look consistent

The additions for .vortex/tooling/ and “Tooling Tests” integrate cleanly with the existing docs: four subsystems under .vortex/, five testing systems, and clear delegation of all tooling-specific details to .vortex/tooling/CLAUDE.md. This keeps the main guide high-level while still discoverable.

Also applies to: 30-31, 92-107, 141-143, 181-185, 569-576, 584-585, 591-592

.vortex/tooling/tests/Fixtures/test-quit-passing (1)

1-20: Treating this as a behavior fixture, not for style review

This fixture is syntactically valid and matches the intended “quit with code 0” behavior; given project conventions around test fixtures, I’m not reviewing it for broader code-quality concerns.

Based on learnings

.vortex/tooling/tests/Fixtures/test-request-get-failing (1)

1-24: Fixture accepted without style review

This negative request_get fixture is structurally consistent and syntactically fine; per the project’s guidance on fixtures, I’m not applying additional code-quality review here.

Based on learnings

.vortex/tooling/tests/Fixtures/test-request-post-failing (1)

1-24: POST failure fixture looks fine; skipping style review

The script correctly exercises a failing request_post scenario and is syntactically valid. No further style or structural review applied, consistent with fixture conventions.

Based on learnings

.vortex/tooling/tests/Fixtures/test-request-get-passing (1)

1-24: GET success fixture accepted as-is

This request_get success fixture matches the documented pattern and is syntactically correct; no additional code-quality review applied due to its role as a test fixture.

Based on learnings

.vortex/tooling/tests/Fixtures/test-passthru-failing (1)

1-19: Passthru failure fixture OK; no further review

The passthru-based failure fixture is syntactically correct and aligned with the tooling test plan; not applying style/structure review per fixture best practices.

Based on learnings

Comment on lines 175 to 179
function validate_command(string $command): void {
exec(sprintf('command -v %s 2>/dev/null', $command), $output, $code);
if ($code !== 0) {
fail(sprintf("Command '%s' is not available", $command));
}
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

Shell-escape $command in validate_command() to avoid injection risk

validate_command() interpolates $command directly into a shell string. If $command ever comes from untrusted input, this becomes a command-injection vector. Even if you currently pass only constants, escaping is cheap and safer.

 function validate_command(string $command): void {
-  exec(sprintf('command -v %s 2>/dev/null', $command), $output, $code);
+  exec(sprintf('command -v %s 2>/dev/null', escapeshellarg($command)), $output, $code);
📝 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
function validate_command(string $command): void {
exec(sprintf('command -v %s 2>/dev/null', $command), $output, $code);
if ($code !== 0) {
fail(sprintf("Command '%s' is not available", $command));
}
function validate_command(string $command): void {
exec(sprintf('command -v %s 2>/dev/null', escapeshellarg($command)), $output, $code);
if ($code !== 0) {
fail(sprintf("Command '%s' is not available", $command));
}
🤖 Prompt for AI Agents
.vortex/tooling/src/helpers.php around lines 175 to 179: the function
interpolates $command directly into a shell invocation which can allow command
injection; change the sprintf to quote/escape the argument before passing it to
the shell (e.g. wrap the command in escapeshellarg() or otherwise
sanitize/validate it) so the executed string becomes safe (also consider
rejecting empty/null commands); keep the same exit-code check and fail behavior.

Comment on lines 229 to 245
protected function mockRequestMultiple(array $responses, string $namespace = 'DrevOps\\VortexTooling'): void {
// Normalize responses by applying defaults before storing.
$normalized_responses = [];
foreach ($responses as $response) {
// @phpstan-ignore-next-line nullCoalesce.offset
$response['response'] = ($response['response'] ?? []) + [
'ok' => TRUE,
'status' => 200,
'body' => '',
'error' => NULL,
'info' => [],
];
$normalized_responses[] = $response;
}

// Add normalized responses to the class property.
$this->mockRequestResponses = array_merge($this->mockRequestResponses, $normalized_responses);
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Validate response shape in mockRequestMultiple() before applying defaults

mockRequestMultiple() assumes $response['response'] is an array when doing ($response['response'] ?? []) + [...]. If a caller accidentally passes a non-array (e.g., string), this will fatally error with unsupported operand types. A small upfront validation would give a clearer failure.

   protected function mockRequestMultiple(array $responses, string $namespace = 'DrevOps\\VortexTooling'): void {
     // Normalize responses by applying defaults before storing.
     $normalized_responses = [];
     foreach ($responses as $response) {
-      // @phpstan-ignore-next-line nullCoalesce.offset
-      $response['response'] = ($response['response'] ?? []) + [
+      if (isset($response['response']) && !is_array($response['response'])) {
+        throw new \InvalidArgumentException('Mocked request response "response" key must be an array.');
+      }
+
+      // @phpstan-ignore-next-line nullCoalesce.offset
+      $response['response'] = ($response['response'] ?? []) + [
         'ok' => TRUE,
         'status' => 200,
         'body' => '',
📝 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
protected function mockRequestMultiple(array $responses, string $namespace = 'DrevOps\\VortexTooling'): void {
// Normalize responses by applying defaults before storing.
$normalized_responses = [];
foreach ($responses as $response) {
// @phpstan-ignore-next-line nullCoalesce.offset
$response['response'] = ($response['response'] ?? []) + [
'ok' => TRUE,
'status' => 200,
'body' => '',
'error' => NULL,
'info' => [],
];
$normalized_responses[] = $response;
}
// Add normalized responses to the class property.
$this->mockRequestResponses = array_merge($this->mockRequestResponses, $normalized_responses);
protected function mockRequestMultiple(array $responses, string $namespace = 'DrevOps\\VortexTooling'): void {
// Normalize responses by applying defaults before storing.
$normalized_responses = [];
foreach ($responses as $response) {
if (isset($response['response']) && !is_array($response['response'])) {
throw new \InvalidArgumentException('Mocked request response "response" key must be an array.');
}
// @phpstan-ignore-next-line nullCoalesce.offset
$response['response'] = ($response['response'] ?? []) + [
'ok' => TRUE,
'status' => 200,
'body' => '',
'error' => NULL,
'info' => [],
];
$normalized_responses[] = $response;
}
// Add normalized responses to the class property.
$this->mockRequestResponses = array_merge($this->mockRequestResponses, $normalized_responses);
🤖 Prompt for AI Agents
.vortex/tooling/tests/Traits/MockTrait.php around lines 229 to 245: the code
assumes $response['response'] is an array when merging defaults which will fatal
if a caller passes a non-array; validate that isset($response['response'])
implies is_array($response['response']) (or coerce only when it's null) and if
it is present but not an array throw a clear InvalidArgumentException (include
the offending index/key in the message) so callers get a descriptive failure;
after validation, proceed to merge the defaults into the array and append to
$this->mockRequestResponses as before.

@github-actions github-actions bot temporarily deployed to commit November 21, 2025 02:20 Inactive
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: 3

♻️ Duplicate comments (4)
.vortex/tooling/rector.php (2)

38-49: Expand withPaths() to cover core tooling sources (e.g. helpers)

Right now Rector only runs on the notify* subdirectories plus playground and tests. Any shared tooling files directly under src/ (for example src/helpers.php) will never be processed.

If those core files are meant to be modernized and kept in sync with the notifier code, consider simplifying the paths to include the whole tooling src tree:

-  ->withPaths([
-    __DIR__ . '/src/notify',
-    __DIR__ . '/src/notify-email',
-    __DIR__ . '/src/notify-github',
-    __DIR__ . '/src/notify-jira',
-    __DIR__ . '/src/notify-newrelic',
-    __DIR__ . '/src/notify-slack',
-    __DIR__ . '/src/notify-webhook',
-    __DIR__ . '/playground',
-    __DIR__ . '/tests',
-  ])
+  ->withPaths([
+    __DIR__ . '/src',
+    __DIR__ . '/playground',
+    __DIR__ . '/tests',
+  ])

This keeps the config simpler and ensures shared helpers are also refactored.


76-78: Align PHP target comment with withPhpSets() configuration

The comment states this config “modernizes syntax to PHP 8.3” and includes rules “through 8.3”, but the actual config uses the PHP‑8.2 set:

// PHP version upgrade sets - modernizes syntax to PHP 8.3.
// Includes all rules from PHP 5.3 through 8.3.
->withPhpSets(php82: TRUE)

To avoid confusion, either:

  • If 8.3 is the intended baseline, switch to the 8.3 set:
-  ->withPhpSets(php82: TRUE)
+  ->withPhpSets(php83: TRUE)
  • Or, if 8.2 is the real target for now, update the comment to reference PHP 8.2 (and 5.3–8.2) accordingly.
.vortex/tooling/phpcs.xml (1)

17-26: Consider whether explicit src/notify-* entries are needed.

If PHPCS scans src recursively (the usual default), the additional src/notify* <file> entries are likely redundant; you can keep just <file>src</file> and <file>tests</file> unless you’ve observed non-recursive behaviour.

.github/workflows/vortex-test-tooling.yml (1)

53-57: Clarify mapping between GITHUB_TOKEN env and PACKAGE_TOKEN secret.

The step exposes GITHUB_TOKEN but sources it from secrets.PACKAGE_TOKEN. If the tooling expects a package token distinct from the standard GITHUB_TOKEN, consider renaming the env var (e.g., PACKAGE_TOKEN) or, if it truly needs the default token, switch the secret reference to secrets.GITHUB_TOKEN. Otherwise, add a brief comment explaining this mapping.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad38200 and f3740d1.

📒 Files selected for processing (9)
  • .github/workflows/vortex-test-tooling.yml (1 hunks)
  • .vortex/tooling/phpcs.xml (1 hunks)
  • .vortex/tooling/phpstan.neon (1 hunks)
  • .vortex/tooling/playground/.env (1 hunks)
  • .vortex/tooling/playground/.env.local (1 hunks)
  • .vortex/tooling/playground/env.php (1 hunks)
  • .vortex/tooling/playground/formatters.php (1 hunks)
  • .vortex/tooling/playground/request.php (1 hunks)
  • .vortex/tooling/rector.php (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-23T01:16:30.963Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1816
File: .docker/cli.dockerfile:72-76
Timestamp: 2025-07-23T01:16:30.963Z
Learning: In the Vortex project, unauthenticated Composer installs should be allowed, so GitHub token secrets should not be marked as `required=true` in Docker build mounts. The conditional check `if [ -s /run/secrets/github_token ]` is the preferred approach to allow builds to proceed without a token when needed.

Applied to files:

  • .github/workflows/vortex-test-tooling.yml
🧬 Code graph analysis (3)
.vortex/tooling/playground/formatters.php (1)
.vortex/tooling/src/helpers.php (1)
  • fail_no_exit (135-139)
.vortex/tooling/playground/env.php (1)
.vortex/tooling/src/helpers.php (1)
  • load_dotenv (39-71)
.vortex/tooling/playground/request.php (1)
.vortex/tooling/src/helpers.php (3)
  • request_get (237-243)
  • request_post (265-272)
  • request (294-349)
🪛 dotenv-linter (4.0.0)
.vortex/tooling/playground/.env

[warning] 25-25: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 29-29: [UnorderedKey] The PLAYGROUND_FLOAT key should go before the PLAYGROUND_NUMBER key

(UnorderedKey)


[warning] 33-33: [UnorderedKey] The PLAYGROUND_BOOL_FALSE key should go before the PLAYGROUND_BOOL_TRUE key

(UnorderedKey)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: build (1)
  • GitHub Check: build (0)
  • GitHub Check: vortex-test-workflow (4)
  • GitHub Check: vortex-test-workflow (3)
  • GitHub Check: vortex-test-workflow (2)
  • GitHub Check: vortex-test-workflow (0)
  • GitHub Check: vortex-test-workflow (1)
  • GitHub Check: vortex-test-common
  • GitHub Check: vortex-test-installer (8.3)
  • GitHub Check: vortex-test-installer (8.4)
  • GitHub Check: vortex-test-docs
🔇 Additional comments (7)
.vortex/tooling/playground/formatters.php (1)

1-15: LGTM! Clean file structure and setup.

The shebang, strict types declaration, namespace, and documentation are all well-structured. The relative path for requiring helpers.php is correct for a playground script.

.vortex/tooling/rector.php (1)

13-37: Base Rector configuration structure looks sound

declare(strict_types=1);, the Rector use imports, and returning RectorConfig::configure() with chained methods follow Rector 2.x patterns and are internally consistent.

.vortex/tooling/phpstan.neon (1)

5-29: PHPStan configuration is strict and narrowly exempted.

Level 9 with paths restricted to src, playground, and tests, plus tightly scoped ignoreErrors rules for test-only patterns, is a solid setup with no obvious gaps.

.vortex/tooling/playground/.env (1)

4-39: Playground .env cases cover the loader semantics well.

Sample variables exercise quoting, = in values, overrides, comments, multi-word, and various primitive-like values; this aligns nicely with what load_dotenv() supports.

.github/workflows/vortex-test-tooling.yml (1)

14-88: Workflow structure for tooling CI looks solid.

Matrixed PHP setup, composer install/normalize, linting, coverage-aware tests, playground runs, and artifact plus Codecov uploads form a coherent CI loop for the tooling package.

.vortex/tooling/playground/.env.local (1)

4-16: Local override sample aligns with the env-loading demo.

Overrides for PLAYGROUND_OVERRIDE and PLAYGROUND_QUOTED plus local-only and debug/env flags are consistent with the behaviour verified in playground/env.php.

.vortex/tooling/playground/request.php (1)

19-123: HTTP request demo gives comprehensive coverage of helper behaviour.

The script exercises GET/POST, custom methods, error statuses, and timeouts against httpbin, using the helpers as intended and keeping failures non-fatal for CI via printed diagnostics.

Comment on lines +28 to +52
<!-- Allow long array lines in tests. -->
<rule ref="Drupal.Arrays.Array.LongLineDeclaration">
<exclude-pattern>*.Test\.php</exclude-pattern>
<exclude-pattern>*.TestCase\.php</exclude-pattern>
<exclude-pattern>*.test</exclude-pattern>
</rule>

<!-- Allow missing class description in tests. -->
<rule ref="Drupal.Commenting.ClassComment.Missing">
<exclude-pattern>*.Test\.php</exclude-pattern>
<exclude-pattern>*.TestCase\.php</exclude-pattern>
<exclude-pattern>*.test</exclude-pattern>
</rule>

<!-- Allow missing function names in tests. -->
<rule ref="Drupal.Commenting.FunctionComment.Missing">
<exclude-pattern>*.Test\.php</exclude-pattern>
<exclude-pattern>*.TestCase\.php</exclude-pattern>
<exclude-pattern>*.test</exclude-pattern>
</rule>
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Verify that exclude-pattern globs match the intended test files.

Patterns like *.Test\.php, *.TestCase\.php, and *.test are meant to target test files, but PHPCS’ exclude-pattern handling (glob vs regex, escaping) can be subtle. Please confirm these patterns actually exclude your test files for the referenced sniffs and adjust to the recommended PHPCS style if needed.

🤖 Prompt for AI Agents
In .vortex/tooling/phpcs.xml around lines 28-47, the exclude-patterns use
backslash-escaped dots and possibly incorrect wildcards (e.g. "*.Test\.php",
"*.TestCase\.php", "*.test") which PHPCS treats as shell-style globs not regex;
update them to proper glob forms such as "*Test.php" and "*TestCase.php" (or
"**/*Test.php" / "**/*TestCase.php" to match subdirectories) and remove the
unnecessary backslash escapes; also verify whether "*.test" is intended (replace
with "*/*.test" or remove if not a real filename extension) so the rules
actually exclude your test files.

Comment on lines +20 to +151
echo "=== Environment Loading Demo ===\n\n";

// Show current directory.
echo "Current directory: " . __DIR__ . "\n\n";

// Check if .env files exist.
$env_file = __DIR__ . '/.env';
$env_local_file = __DIR__ . '/.env.local';

echo "Checking for .env files:\n";
if (file_exists($env_file)) {
echo " ✓ .env file found\n";
echo " Content preview:\n";
$file_lines = file($env_file, FILE_IGNORE_NEW_LINES);
if ($file_lines !== FALSE) {
$lines = array_slice($file_lines, 0, 5);
foreach ($lines as $line) {
echo " " . $line . "\n";
}
}
}
else {
echo " ✗ .env file not found\n";
echo " Run from playground directory where .env file exists\n";
}

if (file_exists($env_local_file)) {
echo " ✓ .env.local file found\n";
echo " Content preview:\n";
$file_lines = file($env_local_file, FILE_IGNORE_NEW_LINES);
if ($file_lines !== FALSE) {
$lines = array_slice($file_lines, 0, 5);
foreach ($lines as $line) {
echo " " . $line . "\n";
}
}
}
else {
echo " ✗ .env.local file not found\n";
echo " Create .env.local to test override behavior\n";
}

echo "\n";

// Load environment files.
echo "Loading environment files...\n";
load_dotenv([$env_file, $env_local_file]);
echo " ✓ Environment files loaded\n";

echo "\n";

// Display loaded variables.
echo "Environment variables loaded:\n";

$test_vars = [
'PLAYGROUND_VAR_1',
'PLAYGROUND_VAR_2',
'PLAYGROUND_VAR_3',
'PLAYGROUND_QUOTED',
'PLAYGROUND_WITH_EQUALS',
'PLAYGROUND_OVERRIDE',
];

foreach ($test_vars as $test_var) {
$value = getenv($test_var);
if ($value !== FALSE) {
echo " " . $test_var . " = " . $value . "\n";
}
else {
echo " " . $test_var . " = (not set)\n";
}
}

echo "\n";

// Verify specific values.
echo "Verification tests:\n";

if (getenv('PLAYGROUND_VAR_1') === 'value1') {
echo " ✓ PLAYGROUND_VAR_1 loaded correctly\n";
}
else {
echo " ✗ PLAYGROUND_VAR_1 not loaded correctly\n";
}

$quoted_value = getenv('PLAYGROUND_QUOTED');
if ($quoted_value === 'quoted value with spaces' || $quoted_value === 'locally overridden with single quotes') {
echo " ✓ PLAYGROUND_QUOTED loaded correctly\n";
echo " Value: " . $quoted_value . "\n";
}
else {
echo " ✗ PLAYGROUND_QUOTED not loaded correctly\n";
echo " Expected: 'quoted value with spaces' or 'locally overridden with single quotes'\n";
echo " Got: " . $quoted_value . "\n";
}

if (getenv('PLAYGROUND_WITH_EQUALS') === 'value=with=equals') {
echo " ✓ PLAYGROUND_WITH_EQUALS loaded correctly (handles = in value)\n";
}
else {
echo " ✗ PLAYGROUND_WITH_EQUALS not loaded correctly\n";
}

// Check override behavior.
if (file_exists($env_local_file)) {
$override_value = getenv('PLAYGROUND_OVERRIDE');
if ($override_value === 'overridden_by_local') {
echo " ✓ PLAYGROUND_OVERRIDE correctly overridden by .env.local\n";
echo " Original value in .env: original_value\n";
echo " Overridden value from .env.local: " . $override_value . "\n";
}
elseif ($override_value === 'original_value') {
echo " ✗ PLAYGROUND_OVERRIDE not overridden (still has .env value)\n";
echo " Check that .env.local is loaded after .env\n";
}
else {
echo " ✗ PLAYGROUND_OVERRIDE has unexpected value: " . $override_value . "\n";
}
}

echo "\n";

// Test comment handling.
echo "Comment handling:\n";
if (getenv('PLAYGROUND_COMMENT') === FALSE) {
echo " ✓ Comment lines correctly ignored\n";
}
else {
echo " ✗ Comment line was parsed as variable\n";
}

echo "\n=== Demo Complete ===\n";
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Env-loading demo correctly exercises quoting, overrides, and comment handling.

The flow—previewing files, calling load_dotenv([$env_file, $env_local_file]), then verifying key variables and override/comment semantics—matches the helper implementation and the playground .env/.env.local contents. If you want even stronger coverage, consider adding brief checks for PLAYGROUND_MULTIWORD, numeric, and boolean-like variables, but the current script is already effective as a manual demo.

🤖 Prompt for AI Agents
In .vortex/tooling/playground/env.php around lines 20 to 151, the demo approves
current tests but suggests stronger coverage; add brief checks that verify
PLAYGROUND_MULTIWORD (a multi-word unquoted value), PLAYGROUND_NUMERIC (numeric
value), and PLAYGROUND_BOOL_LIKE (true/false-like strings) are loaded and
reported correctly: fetch each with getenv(), print their values (or "(not
set)"), and include simple assertions that compare them to expected strings
(e.g., 'multi word', '12345', 'true' or 'false') with success/failure messages
matching the existing style so the script exercises quoting, numeric and
boolean-like parsing without changing the current flow.

Comment on lines +17 to +51
echo "=== Output Formatter Demo ===\n\n";

echo "1. note() - Plain note output:\n";
note("This is a plain note message");
note("Note with formatting: %s %d", "text", 123);

echo "\n2. info() - [INFO] Cyan output:\n";
info("This is an info message");
info("Info with formatting: %s", "formatted text");

echo "\n3. task() - [TASK] Blue output:\n";
task("This is a task message");
task("Task with formatting: %s", "running task");

echo "\n4. pass() - [ OK ] Green output:\n";
pass("This is a success message");
pass("Success with formatting: %s completed", "operation");

echo "\n5. fail_no_exit() - [FAIL] Red output (no exit):\n";
fail_no_exit("This is a failure message (no exit)");
fail_no_exit("Failure with formatting: %s failed", "operation");

echo "\n6. Testing color detection:\n";
note("Colors should appear in terminal with color support");
note("If TERM=dumb or no tput, colors are stripped");
info("Current TERM: %s", getenv('TERM') ?: 'not set');

echo "\n7. Multiple formatters in sequence:\n";
task("Starting multi-step process");
note("Step 1: Initializing");
note("Step 2: Processing data");
note("Step 3: Validating results");
pass("Multi-step process completed successfully");

echo "\n=== Demo Complete ===\n";
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Excellent demonstration of formatter functions.

The script provides a clear, progressive demonstration of all the formatter helpers (note, info, task, pass, fail_no_exit). The examples cover both plain and formatted messages, and the multi-step sequence illustrates practical usage patterns effectively.

Optional: For robustness, consider adding a file existence check before the require_once on line 15:

+if (!file_exists(__DIR__ . '/../src/helpers.php')) {
+  fwrite(STDERR, "Error: helpers.php not found. Run from playground directory.\n");
+  exit(1);
+}
 require_once __DIR__ . '/../src/helpers.php';

However, this is entirely optional for a playground script where the path relationship is fixed.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In .vortex/tooling/playground/formatters.php around lines 15–16, add a guard
before the require_once so the script checks that the file exists and is
readable (e.g., file_exists/is_readable) and only calls require_once when
present; if the file is missing, emit a clear error message and exit gracefully
to avoid a fatal require_once failure.

@AlexSkrypnyk AlexSkrypnyk force-pushed the feature/1192-tooling-package branch from 3086d40 to b9aeaad Compare November 21, 2025 02:35
@github-actions github-actions bot temporarily deployed to commit November 21, 2025 02:38 Inactive
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.vortex/CLAUDE.md (1)

1-1613: Markdown formatting issues across the document

Similar to the scripts_conversion_plan.md file, markdownlint has flagged several formatting inconsistencies (ordered list prefixes). These are minor style issues.

Consider running the markdown auto-formatter:

#!/bin/bash
cd .vortex
ahoy lint-markdown-fix
♻️ Duplicate comments (23)
.vortex/tooling/rector.php (1)

38-49: Past review comments remain valid.

The previous review identified two issues that should be addressed:

  1. Lines 38-49: The paths list specific src/notify-* subdirectories but may exclude src/helpers.php. Consider whether __DIR__ . '/src' should be included to ensure all core helpers are modernized.

  2. Lines 76-78: The comment states "modernizes syntax to PHP 8.3" and "rules from PHP 5.3 through 8.3," but the configuration uses withPhpSets(php82: TRUE). Either update the comment to reflect PHP 8.2 as the target, or change to php83: TRUE if PHP 8.3 is intended.

Also applies to: 76-78

.vortex/tooling/src/notify-slack (1)

137-151: Past review comment remains valid.

The previous review noted that the current implementation only validates HTTP status 200 but doesn't inspect the Slack webhook response body. Slack webhooks can return HTTP 200 with error messages (e.g., "invalid_payload", "no_text") in the body, which would currently be treated as success.

The past review suggested also checking that the response body equals "ok" for stricter validation. This remains an optional improvement to make misconfiguration or invalid payload failures more visible.

.vortex/tooling/phpcs.xml (2)

17-26: Past review comment remains valid.

The previous review noted that lines 18-24 individually list src/notify-* subdirectories, which may be redundant since line 17 already includes <file>src</file> and PHPCS scans directories recursively by default. Consider removing the individual subdirectory entries unless non-recursive behavior is specifically required.


28-47: Past review comment remains valid.

The previous review identified that the exclude-patterns use backslash-escaped dots (e.g., *.Test\.php, *.TestCase\.php) which may not work as expected in PHPCS glob patterns. These should be updated to proper glob forms like *Test.php and *TestCase.php (or **/*Test.php / **/*TestCase.php for subdirectories) without the unnecessary backslash escapes.

.vortex/tooling/src/notify-github (2)

61-71: Consider removing the upper bound on deployment ID length.

The 11-digit upper limit could reject future larger GitHub deployment IDs. While the numeric validation is useful, consider relaxing the constraint to only enforce a minimum length and the ctype_digit() check.

Apply this diff:

-  // Check length (9-11 digits) and numeric format.
-  return $length >= 9 && $length <= 11 && ctype_digit($id_str);
+  // Check minimum length (9+ digits) and numeric format.
+  return $length >= 9 && ctype_digit($id_str);

89-92: Add explicit Content-Type header for JSON POST requests.

Both deployment creation and status update POSTs send JSON bodies but don't specify Content-Type: application/json. While GitHub's API may handle this, setting the header explicitly prevents potential issues and follows best practices.

Apply this diff to both POST calls:

   $response = request_post($url, json_encode($payload_data), [
     'Authorization: token ' . $token,
     'Accept: application/vnd.github.v3+json',
+    'Content-Type: application/json',
   ]);

Also apply to line 147-150.

.vortex/tooling/src/notify-email (1)

117-121: Filter out empty recipient entries after parsing.

If VORTEX_NOTIFY_EMAIL_RECIPIENTS contains extra commas or whitespace, the current explode/trim logic can produce empty strings, leading to blank To: headers and unnecessary mail command invocations.

Apply this diff:

 // Parse and send to each recipient.
 $sent = [];
-$recipients = array_map(trim(...), explode(',', $recipients));
+$recipients = array_filter(
+  array_map(trim(...), explode(',', $recipients)),
+  static fn(string $value): bool => $value !== ''
+);
.vortex/tooling/test_plan.md (1)

10-217: Fix markdownlint violations for consistency.

The document has several formatting issues that are causing markdownlint failures:

  1. Surround all fenced code blocks with blank lines (MD031)
  2. Wrap bare URLs in angle brackets, e.g., <https://example.com> (MD034)
  3. Restart ordered list numbering at 1 for the "Script Tests" section instead of continuing from 15 (MD029)

Example fixes:

 **Mock response structure:**
+
 ```php
@@

@@
-15. testMockRequestGetScriptPassingSuccess
+1. testMockRequestGetScriptPassingSuccess
@@
-- - Mock: url="https://example.com"
+- - Mock: url="https://example.com"


Apply similar fixes throughout the document.

</blockquote></details>
<details>
<summary>.github/workflows/vortex-test-tooling.yml (1)</summary><blockquote>

`53-58`: **Clarify environment variable naming.**

Setting `GITHUB_TOKEN` from `secrets.PACKAGE_TOKEN` is potentially confusing. If the tooling expects `GITHUB_TOKEN`, consider using `secrets.GITHUB_TOKEN`. If elevated privileges from `PACKAGE_TOKEN` are required, add a comment explaining why.




Example clarification:

```diff
       - name: Run tests
         run: composer test-coverage
         working-directory: .vortex/tooling
         env:
+          # Uses PACKAGE_TOKEN for elevated privileges needed by tooling tests
           GITHUB_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
.vortex/tooling/tests/Self/MockRequestSelfTest.php (1)

16-16: Consider importing functions and classes instead of using phpcs:disable.

The @phpcs:disable directive suppresses warnings about fully qualified names, but the cleaner approach would be to import the functions and exception classes at the top of the file and use short names throughout.

Add imports:

 namespace DrevOps\VortexTooling\Tests\Self;
 
 use DrevOps\VortexTooling\Tests\Unit\UnitTestCase;
+use InvalidArgumentException;
 use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\AssertionFailedError;
+use RuntimeException;
+use function DrevOps\VortexTooling\request;
+use function DrevOps\VortexTooling\request_get;
+use function DrevOps\VortexTooling\request_post;
 
 /**
  * Self-tests for mocking of HTTP request functions.
@@
- * @phpcs:disable Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing
  */

Then replace all \DrevOps\VortexTooling\request_* with request_* and exception FQNs with short names.

.vortex/tooling/src/notify (1)

73-73: Use DIRECTORY_SEPARATOR for cross-platform path construction.

The hardcoded forward slash will break on Windows systems. Use PHP's DIRECTORY_SEPARATOR constant for cross-platform compatibility.

Apply this diff:

-  $script = __DIR__ . ('/notify-' . $channel);
+  $script = __DIR__ . DIRECTORY_SEPARATOR . 'notify-' . $channel;
.vortex/tooling/src/notify-jira (1)

107-112: Critical: Script continues after skipping issue extraction, causing undefined variable errors.

The code still contains the issues previously flagged:

  1. Unused helper function: The extract_issue() function (lines 100-105) is defined but not used. The same regex is duplicated inline at lines 108-109.

  2. Missing exit: When no issue is found (line 109), pass() is called but execution continues. This causes $issue to be undefined when referenced later (e.g., line 182), leading to runtime errors.

These issues were marked as addressed in commit 9b2f100, but they appear to still be present in the code.

Apply this fix to use the helper and exit properly:

 task('Extracting issue');
-if (!preg_match('/([^\/]+\/)?([A-Za-z0-9]+\-\d+)/', $label, $matches)) {
+$issue = extract_issue($label);
+if ($issue === NULL) {
   pass('Deployment label %s does not contain issue number.', $label);
+  exit(0);
 }
-$issue = $matches[2];
 note('Found issue %s.', $issue);
.vortex/tooling/src/notify-webhook (2)

88-92: Add error handling for json_encode() failure.

The json_escape_value() function doesn't handle the case where json_encode() returns false on failure (e.g., malformed UTF-8 input). This will cause a type error in substr() when it receives false instead of a string.

Apply this diff:

 function json_escape_value(string $value): string {
   $escaped = json_encode($value);
+  if ($escaped === FALSE) {
+    fail('Failed to JSON-encode value: ' . $value);
+  }
   // Remove surrounding quotes.
   return substr($escaped, 1, -1);
 }

136-138: Consider masking webhook URL in error message.

The error message includes the full $webhook_url, which may contain sensitive tokens or credentials in the URL path or query string. Since $webhook_domain is already sanitized (line 113), use it instead to avoid potential credential leakage in logs.

Apply this diff:

 if ($response['status'] !== (int) $expected_status) {
-  fail(sprintf('Unable to send notification to webhook %s.', $webhook_url));
+  fail(sprintf('Unable to send notification to webhook %s/***', $webhook_domain));
 }
.vortex/tooling/src/notify-newrelic (1)

122-135: Add validation for json_decode() result.

The code decodes the API response without verifying that json_decode() succeeded. If the response contains invalid JSON, $data will be null, leading to potential "undefined array key" errors when accessing $data['applications'][0]['id'].

Apply this diff:

 if ($response['ok'] && $response['body']) {
   $data = json_decode((string) $response['body'], TRUE);
-  if (isset($data['applications'][0]['id'])) {
+  if ($data && is_array($data) && isset($data['applications'][0]['id'])) {
     $appid = $data['applications'][0]['id'];
   }
 }
.vortex/tooling/CLAUDE.md (2)

128-138: Update documentation to use consistent mockRequest* method names.

Lines 135-138 still reference the old mockCurl* naming (mockCurl, mockCurlMultiple, mockCurlAssertAllMocksConsumed), but the actual MockTrait API (and the reference section at lines 714-720) uses mockRequest(), mockRequestGet(), mockRequestPost(), mockRequestMultiple(), and mockRequestAssertAllMocksConsumed().

This inconsistency was previously flagged and marked as addressed in commit 9b2f100, but appears to remain in lines 135-138.

Apply this diff:

 **Pattern**:
 ```php
 protected function mockPassthru(array $response): void
 protected function mockPassthruMultiple(array $responses): void
 protected function mockPassthruAssertAllMocksConsumed(): void
 
 protected function mockQuit(int $code = 0): void
 
-protected function mockCurl(array $response): void
-protected function mockCurlMultiple(array $responses): void
-protected function mockCurlAssertAllMocksConsumed(): void
+protected function mockRequest(string $url, array $options = [], array $response = []): void
+protected function mockRequestGet(string $url, array $headers = [], int $timeout = 10, array $response = []): void
+protected function mockRequestPost(string $url, $body = NULL, array $headers = [], int $timeout = 10, array $response = []): void
+protected function mockRequestMultiple(array $responses): void
+protected function mockRequestAssertAllMocksConsumed(): void

---

`1-733`: **Consider addressing markdown linting issues for better consistency.**

Static analysis has flagged several markdown formatting issues:
- Fenced code blocks should be surrounded by blank lines (MD031)
- Ordered list prefixes should consistently use `1.` style (MD029)
- Bare URLs should be wrapped in `<...>` or converted to markdown links (MD034)

These are minor formatting issues that don't affect functionality but would improve markdown consistency and pass linting checks.



If you'd like to address these, the main changes would be:
- Add blank lines before and after ` ```php ` and ` ``` ` blocks
- Change numbered lists from `2.`, `3.`, etc. to all use `1.` (markdown auto-numbers)
- Wrap URLs like `https://phpunit.de/documentation.html` in angle brackets or markdown link syntax

</blockquote></details>
<details>
<summary>.vortex/tooling/tests/Traits/MockTrait.php (2)</summary><blockquote>

`83-96`: **Update passthru docblock to match actual array keys.**

The docblock at line 90 references `exit_code`, but the actual response array uses `result_code` (as seen in lines 119-123 and the property annotation at line 25). This inconsistency can confuse users of the trait.

This was previously flagged and marked as addressed, but the docblock still contains the old naming.




Apply this diff:

```diff
  * @param array<int, array{cmd:string, output?: string, result_code?: int, return?: NULL|FALSE}> $responses
  *   Array of responses to return for each passthru call.
  *   Each response should have:
+ *   - cmd: Expected command (required)
  *   - output: The output to echo
- *   - exit_code: The exit code to set (0 for success).
+ *   - result_code: The exit code to set (optional, default: 0)
+ *   - return: Return value (optional, NULL or FALSE)

229-245: Validate response structure before applying defaults.

At line 234, the code assumes $response['response'] is an array when merging with defaults using the + operator. If a caller accidentally passes a non-array value (e.g., a string), this will cause a fatal error with "unsupported operand types."

This validation was previously suggested and marked as addressed, but no validation is present in the code.

Apply this diff:

   protected function mockRequestMultiple(array $responses, string $namespace = 'DrevOps\\VortexTooling'): void {
     // Normalize responses by applying defaults before storing.
     $normalized_responses = [];
     foreach ($responses as $response) {
+      if (isset($response['response']) && !is_array($response['response'])) {
+        throw new \InvalidArgumentException('Mocked request response "response" key must be an array.');
+      }
+
       // @phpstan-ignore-next-line nullCoalesce.offset
       $response['response'] = ($response['response'] ?? []) + [
.vortex/tooling/src/helpers.php (4)

22-31: Add path traversal protection to execute_override().

The $name parameter is concatenated directly into a file path (line 25) without validation. While current usage passes basename(__FILE__), the function signature accepts any string, creating a potential path traversal vulnerability if future usage passes untrusted input.

Apply this diff to add validation:

 function execute_override(string $name): void {
+  // Prevent path traversal by ensuring name doesn't contain separators.
+  if (str_contains($name, DIRECTORY_SEPARATOR) || str_contains($name, '/') || str_contains($name, '\\')) {
+    fail('Invalid script name for override: %s', $name);
+  }
+
   $dir = getenv('VORTEX_TOOLING_CUSTOM_DIR');
   if ($dir) {
     $path = $dir . DIRECTORY_SEPARATOR . $name;

295-298: Remove unreachable exit(1) after fail().

The exit(1) call on line 297 is unreachable dead code because fail() already calls exit(1) internally (line 151).

Apply this diff:

 function request(string $url, array $options = []): array {
   if (!function_exists('curl_init')) {
     fail('curl extension is not available.');
-    exit(1);
   }

300-302: Check curl_init() return value to prevent TypeError.

curl_init() can return false on failure (e.g., invalid URL). Passing false to curl_setopt_array() on line 322 will cause a TypeError in PHP 8.0+.

Apply this diff:

   $ch = curl_init($url);
+  if ($ch === FALSE) {
+    fail('Failed to initialize curl for URL: %s', $url);
+  }
 
   try {

175-179: Shell-escape $command in validate_command() to prevent injection.

The $command parameter is interpolated directly into a shell command (line 176) without escaping. If $command ever comes from untrusted input, this becomes a command-injection vector.

Apply this diff:

 function validate_command(string $command): void {
-  exec(sprintf('command -v %s 2>/dev/null', $command), $output, $code);
+  exec(sprintf('command -v %s 2>/dev/null', escapeshellarg($command)), $output, $code);
   if ($code !== 0) {
     fail(sprintf("Command '%s' is not available", $command));
   }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3086d40 and b9aeaad.

📒 Files selected for processing (41)
  • .github/workflows/vortex-test-tooling.yml (1 hunks)
  • .vortex/CLAUDE.md (9 hunks)
  • .vortex/tooling/.gitignore (1 hunks)
  • .vortex/tooling/CLAUDE.md (1 hunks)
  • .vortex/tooling/composer.json (1 hunks)
  • .vortex/tooling/phpcs.xml (1 hunks)
  • .vortex/tooling/phpstan.neon (1 hunks)
  • .vortex/tooling/phpunit.xml (1 hunks)
  • .vortex/tooling/playground/.env (1 hunks)
  • .vortex/tooling/playground/.env.local (1 hunks)
  • .vortex/tooling/playground/env.php (1 hunks)
  • .vortex/tooling/playground/formatters.php (1 hunks)
  • .vortex/tooling/playground/request.php (1 hunks)
  • .vortex/tooling/rector.php (1 hunks)
  • .vortex/tooling/scripts_conversion_plan.md (1 hunks)
  • .vortex/tooling/src/helpers.php (1 hunks)
  • .vortex/tooling/src/notify (1 hunks)
  • .vortex/tooling/src/notify-email (1 hunks)
  • .vortex/tooling/src/notify-github (1 hunks)
  • .vortex/tooling/src/notify-jira (1 hunks)
  • .vortex/tooling/src/notify-newrelic (1 hunks)
  • .vortex/tooling/src/notify-slack (1 hunks)
  • .vortex/tooling/src/notify-webhook (1 hunks)
  • .vortex/tooling/test_plan.md (1 hunks)
  • .vortex/tooling/tests/Exceptions/QuitErrorException.php (1 hunks)
  • .vortex/tooling/tests/Exceptions/QuitSuccessException.php (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-passthru-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-passthru-multiple (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-passthru-passing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-quit-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-quit-passing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-get-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-get-passing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-multiple (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-post-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-post-passing (1 hunks)
  • .vortex/tooling/tests/Self/MockPassthruSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockQuitSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Traits/MockTrait.php (1 hunks)
  • .vortex/tooling/tests/Unit/UnitTestCase.php (1 hunks)
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-09-13T04:14:41.765Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 2011
File: .vortex/tests/phpunit/Traits/Steps/StepAhoyTrait.php:20-23
Timestamp: 2025-09-13T04:14:41.765Z
Learning: In .vortex/tests/phpunit PHPUnit tests using cmd/cmdFail helpers, patterns in the expected output arrays support '*' markers for positive assertions (text must be present) and '!' markers for negative assertions (text must NOT be present). The '*' markers are NOT literal text matches but rather indicate expected presence.

Applied to files:

  • .vortex/tooling/phpunit.xml
  • .vortex/tooling/phpcs.xml
  • .vortex/tooling/tests/Unit/UnitTestCase.php
  • .vortex/tooling/test_plan.md
  • .vortex/tooling/tests/Fixtures/test-request-multiple
  • .vortex/CLAUDE.md
  • .vortex/tooling/tests/Fixtures/test-passthru-failing
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
  • .vortex/tooling/tests/Exceptions/QuitSuccessException.php
  • .vortex/tooling/tests/Self/MockQuitSelfTest.php
  • .vortex/tooling/tests/Fixtures/test-quit-passing
  • .vortex/tooling/tests/Fixtures/test-request-get-failing
  • .vortex/tooling/tests/Fixtures/test-passthru-multiple
  • .vortex/tooling/tests/Fixtures/test-request-post-failing
  • .vortex/tooling/CLAUDE.md
  • .vortex/tooling/tests/Fixtures/test-quit-failing
  • .vortex/tooling/tests/Fixtures/test-passthru-passing
  • .vortex/tooling/tests/Self/MockPassthruSelfTest.php
  • .vortex/tooling/tests/Exceptions/QuitErrorException.php
  • .vortex/tooling/tests/Fixtures/test-request-get-passing
  • .vortex/tooling/tests/Fixtures/test-request-post-passing
📚 Learning: 2025-06-01T08:09:18.993Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1693
File: .vortex/installer/tests/Fixtures/install/hosting_acquia/docs/deployment.md:6-8
Timestamp: 2025-06-01T08:09:18.993Z
Learning: Do not review files in test fixture directories (e.g., files under paths containing `/tests/Fixtures/` or similar test fixture patterns). These files are used for testing purposes and should remain as-is.

Applied to files:

  • .vortex/tooling/phpcs.xml
📚 Learning: 2025-06-01T08:08:59.071Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1693
File: .vortex/installer/tests/Fixtures/install/deploy_type_none_gha/.github/workflows/build-test-deploy.yml:1-4
Timestamp: 2025-06-01T08:08:59.071Z
Learning: Test fixture files (typically located in directories containing "fixture" or "test") should not be reviewed for code quality issues as they may intentionally contain incomplete, invalid, or malformed data for testing purposes.

Applied to files:

  • .vortex/tooling/phpcs.xml
  • .vortex/tooling/tests/Fixtures/test-request-post-failing
  • .vortex/tooling/tests/Fixtures/test-quit-failing
📚 Learning: 2025-06-02T00:41:36.321Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1697
File: .vortex/installer/tests/Fixtures/install/provision_database_lagoon/docs/onboarding.md:16-17
Timestamp: 2025-06-02T00:41:36.321Z
Learning: DO NOT EVER REVIEW FILES IN .vortex/installer/tests/Fixtures/ directory - these are test fixtures and should not be reviewed under any circumstances.

Applied to files:

  • .vortex/CLAUDE.md
  • .vortex/tooling/tests/Fixtures/test-passthru-failing
📚 Learning: 2025-05-29T12:15:32.188Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 0
File: :0-0
Timestamp: 2025-05-29T12:15:32.188Z
Learning: Do not review files in `.vortex/installer/tests/Fixtures/install` directory as they are test fixtures.

Applied to files:

  • .vortex/CLAUDE.md
  • .vortex/tooling/tests/Fixtures/test-passthru-failing
📚 Learning: 2025-06-03T03:15:29.849Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1714
File: scripts/vortex/doctor.sh:69-70
Timestamp: 2025-06-03T03:15:29.849Z
Learning: In the Vortex project, logging functions (task(), info(), note(), pass(), fail(), warn()) are intentionally duplicated across all script files to make each script self-contained and independent, rather than sharing them through a common library. This design choice prioritizes script independence over reducing code duplication.

Applied to files:

  • .vortex/CLAUDE.md
📚 Learning: 2025-08-08T12:02:24.652Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1896
File: .vortex/tests/bats/unit/download-db-lagoon.bats:24-25
Timestamp: 2025-08-08T12:02:24.652Z
Learning: In .vortex/tests/bats/unit Bats tests using ../_helper.bash (run_steps), prefixing a STEPS entry with "- " denotes a negative assertion (the substring must NOT appear in output). Unprefixed entries are positive assertions. Example: "- Database dump refresh requested. Will create a new dump." asserts absence; "Database dump refresh requested. Will create a new dump." asserts presence.

Applied to files:

  • .vortex/CLAUDE.md
  • .vortex/tooling/tests/Fixtures/test-passthru-failing
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
  • .vortex/tooling/tests/Fixtures/test-passthru-multiple
  • .vortex/tooling/tests/Fixtures/test-quit-failing
  • .vortex/tooling/tests/Fixtures/test-passthru-passing
📚 Learning: 2025-07-23T01:16:30.963Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1816
File: .docker/cli.dockerfile:72-76
Timestamp: 2025-07-23T01:16:30.963Z
Learning: In the Vortex project, unauthenticated Composer installs should be allowed, so GitHub token secrets should not be marked as `required=true` in Docker build mounts. The conditional check `if [ -s /run/secrets/github_token ]` is the preferred approach to allow builds to proceed without a token when needed.

Applied to files:

  • .github/workflows/vortex-test-tooling.yml
🧬 Code graph analysis (11)
.vortex/tooling/playground/formatters.php (1)
.vortex/tooling/src/helpers.php (1)
  • fail_no_exit (135-139)
.vortex/tooling/tests/Unit/UnitTestCase.php (1)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockTearDown (63-81)
.vortex/tooling/playground/env.php (1)
.vortex/tooling/src/helpers.php (1)
  • load_dotenv (39-71)
.vortex/tooling/playground/request.php (1)
.vortex/tooling/src/helpers.php (3)
  • request_get (237-243)
  • request_post (265-272)
  • request (294-349)
.vortex/tooling/tests/Self/MockRequestSelfTest.php (3)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (14-57)
  • runScript (27-55)
.vortex/tooling/tests/Traits/MockTrait.php (5)
  • mockRequestGet (405-418)
  • mockRequestPost (436-450)
  • mockRequest (373-389)
  • mockRequestAssertAllMocksConsumed (458-469)
  • mockRequestMultiple (229-359)
.vortex/tooling/src/helpers.php (3)
  • request_get (237-243)
  • request_post (265-272)
  • request (294-349)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • __construct (20-26)
.vortex/tooling/tests/Self/MockQuitSelfTest.php (5)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-28)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
  • QuitSuccessException (12-28)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (14-57)
  • runScript (27-55)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/src/helpers.php (1)
  • quit (369-371)
.vortex/tooling/src/helpers.php (1)
.vortex/installer/src/Utils/Tui.php (3)
  • line (57-59)
  • output (38-43)
  • error (53-55)
.vortex/tooling/tests/Traits/MockTrait.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-28)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
  • QuitSuccessException (12-28)
.vortex/tooling/src/helpers.php (2)
  • fail (149-152)
  • quit (369-371)
.vortex/tooling/tests/Self/MockPassthruSelfTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (14-57)
  • runScript (27-55)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockPassthru (161-163)
  • mockPassthruAssertAllMocksConsumed (171-182)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
  • __construct (20-26)
🪛 dotenv-linter (4.0.0)
.vortex/tooling/playground/.env

[warning] 25-25: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 29-29: [UnorderedKey] The PLAYGROUND_FLOAT key should go before the PLAYGROUND_NUMBER key

(UnorderedKey)


[warning] 33-33: [UnorderedKey] The PLAYGROUND_BOOL_FALSE key should go before the PLAYGROUND_BOOL_TRUE key

(UnorderedKey)

🪛 LanguageTool
.vortex/tooling/CLAUDE.md

[grammar] ~140-~140: Ensure spelling is correct
Context: ...ssertAllMocksConsumed(): void #### Passthru Mocking **Response Structure**:php...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
.vortex/CLAUDE.md

92-92: Ordered list item prefix
Expected: 1; Actual: 15; Style: 1/2/3

(MD029, ol-prefix)


93-93: Ordered list item prefix
Expected: 2; Actual: 16; Style: 1/2/3

(MD029, ol-prefix)


94-94: Ordered list item prefix
Expected: 3; Actual: 17; Style: 1/2/3

(MD029, ol-prefix)


95-95: Ordered list item prefix
Expected: 4; Actual: 18; Style: 1/2/3

(MD029, ol-prefix)


96-96: Ordered list item prefix
Expected: 5; Actual: 19; Style: 1/2/3

(MD029, ol-prefix)


97-97: Ordered list item prefix
Expected: 6; Actual: 20; Style: 1/2/3

(MD029, ol-prefix)


98-98: Ordered list item prefix
Expected: 7; Actual: 21; Style: 1/2/3

(MD029, ol-prefix)


99-99: Ordered list item prefix
Expected: 8; Actual: 22; Style: 1/2/3

(MD029, ol-prefix)


100-100: Ordered list item prefix
Expected: 9; Actual: 23; Style: 1/2/3

(MD029, ol-prefix)


101-101: Ordered list item prefix
Expected: 10; Actual: 24; Style: 1/2/3

(MD029, ol-prefix)


102-102: Ordered list item prefix
Expected: 11; Actual: 25; Style: 1/2/3

(MD029, ol-prefix)


103-103: Ordered list item prefix
Expected: 12; Actual: 26; Style: 1/2/3

(MD029, ol-prefix)

.vortex/tooling/scripts_conversion_plan.md

9-9: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


16-16: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


17-17: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


17-17: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


43-43: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


48-48: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


59-59: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


68-68: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


92-92: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


93-93: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


101-101: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


103-103: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


112-112: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


125-125: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


126-126: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


139-139: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


140-140: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


146-146: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


147-147: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


166-166: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


167-167: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


182-182: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


278-278: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


283-283: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


386-386: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


393-393: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


394-394: Ordered list item prefix
Expected: 1; Actual: 6; Style: 1/2/3

(MD029, ol-prefix)


395-395: Ordered list item prefix
Expected: 2; Actual: 7; Style: 1/2/3

(MD029, ol-prefix)


396-396: Ordered list item prefix
Expected: 3; Actual: 8; Style: 1/2/3

(MD029, ol-prefix)


397-397: Ordered list item prefix
Expected: 4; Actual: 9; Style: 1/2/3

(MD029, ol-prefix)


398-398: Ordered list item prefix
Expected: 5; Actual: 10; Style: 1/2/3

(MD029, ol-prefix)


399-399: Ordered list item prefix
Expected: 6; Actual: 11; Style: 1/2/3

(MD029, ol-prefix)


401-401: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


402-402: Ordered list item prefix
Expected: 1; Actual: 12; Style: 1/2/3

(MD029, ol-prefix)


403-403: Ordered list item prefix
Expected: 2; Actual: 13; Style: 1/2/3

(MD029, ol-prefix)


404-404: Ordered list item prefix
Expected: 3; Actual: 14; Style: 1/2/3

(MD029, ol-prefix)


405-405: Ordered list item prefix
Expected: 4; Actual: 15; Style: 1/2/3

(MD029, ol-prefix)


406-406: Ordered list item prefix
Expected: 5; Actual: 16; Style: 1/2/3

(MD029, ol-prefix)


431-431: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


432-432: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


440-440: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


441-441: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


453-453: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


454-454: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


466-466: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


467-467: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


475-475: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


476-476: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


489-489: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


496-496: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


503-503: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

.vortex/tooling/CLAUDE.md

53-53: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


83-83: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


128-128: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


143-143: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


153-153: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


171-171: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


216-216: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


310-310: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


317-317: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


322-322: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


390-390: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


422-422: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


437-437: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


438-438: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


447-447: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


448-448: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


455-455: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


483-483: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


484-484: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


497-497: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


502-502: Ordered list item prefix
Expected: 1; Actual: 4; Style: 1/1/1

(MD029, ol-prefix)


646-646: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


727-727: Bare URL used

(MD034, no-bare-urls)


728-728: Bare URL used

(MD034, no-bare-urls)


729-729: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: build (1)
  • GitHub Check: build (0)
  • GitHub Check: vortex-test-workflow (4)
  • GitHub Check: vortex-test-workflow (3)
  • GitHub Check: vortex-test-workflow (2)
  • GitHub Check: vortex-test-workflow (1)
  • GitHub Check: vortex-test-workflow (0)
  • GitHub Check: vortex-test-common
  • GitHub Check: vortex-test-installer (8.3)
  • GitHub Check: vortex-test-installer (8.4)
🔇 Additional comments (34)
.vortex/tooling/tests/Fixtures/test-passthru-multiple (1)

1-23: LGTM! Clean fixture for testing multiple passthru calls

The fixture correctly demonstrates multiple passthru() invocations with exit code capture. Structure is clean and aligns with the testing infrastructure.

.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)

20-26: LGTM! Well-designed exception for test mocking

The exception correctly validates non-zero exit codes and provides clear error messages. The symmetric design with QuitSuccessException enables clean test assertions.

.vortex/CLAUDE.md (3)

18-22: LGTM! Clear documentation of new tooling subsystem

The addition of the tooling directory structure and cross-reference to .vortex/tooling/CLAUDE.md properly documents the new subsystem.


92-106: Excellent separation of concerns with dedicated tooling documentation

The clear warning to redirect users to .vortex/tooling/CLAUDE.md for tooling-specific work prevents documentation duplication and confusion.


141-184: Testing architecture properly updated to reflect five systems

The documentation accurately describes the addition of the tooling tests as the third system, with appropriate cross-references.

.vortex/tooling/tests/Fixtures/test-quit-passing (1)

1-20: LGTM! Effective fixture for testing quit() success path

The unreachable code after quit(0) serves as a validation check to ensure quit() prevents further execution. Clean implementation.

.vortex/tooling/playground/.env.local (1)

1-16: LGTM! Well-documented example environment file

The file effectively demonstrates .env.local override behavior with clear examples. The "secret" values are obviously example data (e.g., secret_token_12345) appropriate for a playground environment.

.vortex/tooling/.gitignore (1)

2-2: Review comment is based on flawed reasoning about library lockfile practices

The .vortex/tooling package is declared as "type": "library" in its composer.json. According to Composer documentation and community practice, libraries may commit composer.lock but don't need to, and consumers won't be affected. Critically, installed packages ignore a library's lockfile, so the lockfile does not impact downstream projects using this library.

While reproducible local/CI testing is a valid consideration for any project, the original comment frames ignoring composer.lock as a problem specific to this package's testing needs. For a distributed library like this, not committing the lockfile is a legitimate and documented practice. The reasoning conflates application reproducibility (where a lockfile matters) with library distribution (where the lockfile has no effect on consumers).

Likely an incorrect or invalid review comment.

.vortex/tooling/tests/Fixtures/test-request-get-failing (1)

1-24: LGTM - Test fixture for failing GET request scenario.

This fixture appropriately tests the failure path for HTTP GET requests. The structure is clean and follows the same pattern as other fixtures in the test suite.

.vortex/tooling/tests/Fixtures/test-request-post-failing (1)

1-24: LGTM - Test fixture for failing POST request scenario.

The fixture correctly demonstrates a failing POST request with JSON payload and appropriate headers. Implementation is consistent with other request fixtures.

.vortex/tooling/tests/Fixtures/test-passthru-passing (1)

1-19: LGTM - Test fixture for successful passthru operation.

The fixture correctly tests a passing passthru scenario where the command exits with code 0 as expected.

.vortex/tooling/phpstan.neon (1)

1-29: LGTM - PHPStan configuration is appropriately strict.

Level 9 analysis with targeted ignore rules for test files is a solid approach. The configuration correctly excludes vendor/node_modules and includes all relevant source paths.

.vortex/tooling/tests/Fixtures/test-request-get-passing (1)

1-24: LGTM - Test fixture for successful GET request scenario.

The fixture appropriately demonstrates the success path, including response body output. Clean and consistent with the fixture suite pattern.

.vortex/tooling/tests/Fixtures/test-passthru-failing (1)

1-19: LGTM - Test fixture for mock-driven failing passthru scenario.

While the command echo "success" typically returns 0, this fixture likely tests that the mocking framework (MockTrait) can override the actual exit code to simulate failures. This pattern is consistent with testing mock behavior rather than actual command execution.

.vortex/tooling/phpunit.xml (1)

1-42: LGTM - Comprehensive PHPUnit configuration with appropriate strictness.

The configuration includes:

  • Proper bootstrap and strict error handling
  • Coverage reporting with reasonable thresholds (50%/90%)
  • JUnit logging for CI integration
  • Appropriate source filtering including test traits and base cases

The schema version concern from the previous review has been addressed.

.vortex/tooling/tests/Fixtures/test-request-post-passing (1)

1-24: LGTM - Test fixture for successful POST request scenario.

The fixture correctly demonstrates a successful POST with JSON payload and proper Content-Type header. Implementation is clean and consistent with the fixture suite.

.vortex/tooling/playground/.env (1)

1-39: Test fixture file - no review required.

This is a playground/test fixture designed to demonstrate various .env loading behaviors (quotes, equals signs, multi-word values, etc.). Per project learnings, test fixture files should not be reviewed for code quality issues as they may intentionally contain edge cases and non-standard formatting for testing purposes.

The static analysis warnings (unquoted values, key ordering) are intentional test cases to validate the environment loader's handling of diverse input formats.

.vortex/tooling/tests/Fixtures/test-quit-failing (1)

1-20: Test fixture file - no review required.

This is a test fixture located in the tests/Fixtures directory, designed to validate quit() behavior with exit code 1. Per project learnings, test fixture files should not be reviewed for code quality issues as they may intentionally contain specific patterns for testing purposes.

The unreachable echo statement on line 20 is intentional—it verifies that quit() properly terminates execution.

.vortex/tooling/tests/Fixtures/test-request-multiple (1)

1-26: Test fixture file - no review required.

This is a test fixture in the tests/Fixtures directory, designed to validate multiple HTTP request calls in sequence (GET, POST, PUT). Per project learnings, test fixture files should not be reviewed for code quality issues as they serve specific testing purposes.

.vortex/tooling/playground/formatters.php (1)

1-51: LGTM - Playground demo script.

This playground script effectively demonstrates the output formatter functions (note, info, task, pass, fail_no_exit) with clear examples and progressive scenarios. The structure is appropriate for a manual testing/demo script.

Note: A past review suggested adding a file existence check before the require_once on line 15, which remains an optional improvement for robustness.

.vortex/tooling/playground/request.php (1)

1-123: LGTM - Comprehensive HTTP request demo.

This playground script provides excellent coverage of HTTP request scenarios across 9 tests, including success cases, error handling (404, 500), custom methods (PUT, DELETE), custom headers, and timeout behavior. The structure is clear and well-documented for manual testing purposes.

.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)

20-26: LGTM!

The constructor correctly validates that only exit code 0 is accepted, mirroring the complementary QuitErrorException design. The implementation is clean and consistent with the testing infrastructure.

.vortex/tooling/tests/Unit/UnitTestCase.php (2)

19-25: LGTM!

The tearDown order is correct: environment reset, mock cleanup, then parent tearDown. This ensures proper test isolation.


27-55: LGTM!

The runScript method properly isolates script execution with output buffering and directory management. The try-finally pattern ensures directory restoration even on exceptions, and the coverage ignore markers on defensive error checks are appropriate.

.vortex/tooling/playground/env.php (1)

1-151: LGTM! Well-structured environment loading demonstration.

The script effectively demonstrates:

  • Loading from multiple .env files with local overrides
  • Quote handling for values with spaces
  • Preservation of equals signs in values
  • Override behavior when .env.local exists
  • Comment line ignoring

The implementation correctly exercises the load_dotenv() helper and provides clear user feedback. The past suggestion to add checks for multiword, numeric, and boolean-like variables was a nitpick for enhanced coverage, but the current implementation adequately demonstrates the core functionality.

.vortex/tooling/tests/Self/MockQuitSelfTest.php (1)

1-109: LGTM! Comprehensive quit mocking self-tests.

The test suite effectively validates the mockQuit() functionality:

  • Direct tests (lines 21-59): Verify that quit() throws the correct exception types (QuitSuccessException for 0, QuitErrorException for non-zero) and validates exit code mismatches.

  • Script tests (lines 61-107): Exercise quit() behavior through fixture scripts, ensuring proper exception propagation and verifying script output.

The test coverage is thorough, covering both success and failure pathways, and follows the documented naming conventions. The assertions correctly check for expected output and absence of error indicators.

.vortex/tooling/tests/Traits/MockTrait.php (1)

1-471: Overall: Well-architected mocking infrastructure with minor documentation inconsistencies.

The MockTrait provides a robust, queue-based mocking system for passthru, quit, and HTTP request functions. The implementation correctly:

  • Tracks mock state across multiple calls
  • Validates expected vs. actual parameters (URL, method, command)
  • Ensures all mocked responses are consumed via teardown checks
  • Handles curl function chaining properly (init → setopt → exec → errno/error/getinfo)
  • Increments indices before validation to mark mocks as consumed even on exceptions (lines 300-305)

The convenience methods (mockRequestGet, mockRequestPost) provide good ergonomics by mirroring the actual helper function signatures.

The issues flagged in other comments are minor documentation and validation improvements that don't affect the core functionality.

.vortex/tooling/src/helpers.php (1)

1-373: Overall: Well-designed helper utilities with security issues to address.

The helpers module provides a solid foundation for the tooling package:

Strengths:

  • Clean, focused functions with clear responsibilities
  • Consistent output formatting with color support detection
  • Structured HTTP request handling with proper error information
  • Test-friendly quit() wrapper that can be mocked
  • Good use of try-finally for curl resource cleanup (lines 302-348)

Areas for improvement:
The security and robustness issues flagged in other comments should be addressed:

  • Path traversal protection in execute_override()
  • Shell injection prevention in validate_command()
  • curl_init() failure handling in request()
  • Redundant exit() cleanup

Once these issues are resolved, this will be a robust helper module.

.vortex/tooling/tests/Self/MockPassthruSelfTest.php (6)

1-19: LGTM! Well-structured test class.

The class structure, imports, and documentation are clear and appropriate for self-testing the passthru mocking functionality.


21-70: LGTM! Comprehensive coverage of success scenarios.

These three tests effectively validate the passthru mocking for:

  • Explicit default values (lines 21-37)
  • Custom configurations (lines 39-55)
  • Default behavior when only cmd is provided (lines 57-70)

The output buffering and assertions are correctly implemented.


72-109: LGTM! Proper validation of argument constraints.

These tests correctly verify that:

  • Missing cmd key throws appropriate exception (lines 72-89)
  • Invalid return type throws appropriate exception (lines 91-109)

The use of try-finally blocks ensures output buffer cleanup, and @phpstan-ignore annotations are appropriate for intentionally invalid test inputs.


111-237: LGTM! Thorough testing of command validation and call counting.

Excellent coverage of:

  • Command mismatch detection (lines 111-126)
  • Sequential mock consumption (lines 128-160)
  • Over-calling detection (lines 162-206)
  • Under-calling detection (lines 208-237)

The manual call to mockPassthruAssertAllMocksConsumed() on line 236 is intentional and ensures the assertion failure occurs within the test method rather than during tearDown, providing clearer test semantics.


239-368: LGTM! Comprehensive script-based validation.

These tests effectively mirror the direct passthru tests using the runScript() utility, validating that mocking works correctly within script contexts. The tests cover:

  • Passing/failing scenarios (lines 239-265)
  • Custom configurations and defaults (lines 267-290)
  • Argument validation errors (lines 292-317)
  • Command mismatch detection (lines 319-331)
  • Multiple call handling (lines 333-368)

The deliberate duplication between direct and script-based tests ensures both pathways are thoroughly validated.


370-413: LGTM! Complete coverage of script-based edge cases.

The final tests appropriately cover:

  • Under-calling detection with manual assertion (lines 370-399)
  • Failing script validation (lines 401-413)

The manual call to mockPassthruAssertAllMocksConsumed() on line 398 follows the same pattern as line 236, ensuring the assertion failure is attributed to the test method for clearer test reporting.

@AlexSkrypnyk AlexSkrypnyk added DO NOT MERGE Do not merge this pull request Do not review Do not review this pull request labels Nov 21, 2025
@AlexSkrypnyk AlexSkrypnyk added this to the 25.12.0 milestone Nov 21, 2025
@github-actions github-actions bot temporarily deployed to commit November 21, 2025 04:42 Inactive
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: 5

♻️ Duplicate comments (4)
.vortex/tooling/tests/Self/MockRequestSelfTest.php (1)

32-523: Import request helpers and exception classes instead of using fully‑qualified names throughout

This file repeatedly references namespaced helpers and exception classes via leading‑backslash FQNs (e.g. \DrevOps\VortexTooling\request_get, \RuntimeException, \InvalidArgumentException, \PHPUnit\Framework\AssertionFailedError). PHPCS has already flagged this pattern elsewhere in this file, and a patch was suggested in an earlier review.

To satisfy coding standards and keep tests cleaner, import these once at the top and use short names in the body, for example:

 namespace DrevOps\VortexTooling\Tests\Self;
 
 use DrevOps\VortexTooling\Tests\Unit\UnitTestCase;
 use PHPUnit\Framework\Attributes\CoversClass;
+use InvalidArgumentException;
+use PHPUnit\Framework\AssertionFailedError;
+use RuntimeException;
+use function DrevOps\VortexTooling\request;
+use function DrevOps\VortexTooling\request_get;
+use function DrevOps\VortexTooling\request_post;
@@
-    $result = \DrevOps\VortexTooling\request_get('https://example.com/api');
+    $result = request_get('https://example.com/api');
@@
-    $this->expectException(\RuntimeException::class);
+    $this->expectException(RuntimeException::class);
@@
-    $this->expectException(\PHPUnit\Framework\AssertionFailedError::class);
+    $this->expectException(AssertionFailedError::class);
@@
-    $this->expectException(\InvalidArgumentException::class);
+    $this->expectException(InvalidArgumentException::class);

…and similarly for the remaining \request_* and exception references in this class.

This will resolve the PHPCS UseStatementMissing errors without needing to permanently disable that sniff and keeps the tests more idiomatic.

.vortex/tooling/src/helpers.php (3)

43-52: Path traversal protection still needed in execute_override.

As noted in a previous review, the $name parameter is concatenated directly into a file path without validation, creating a potential path traversal vulnerability if future callers pass unsanitized input.


198-202: Command injection risk remains in validate_command.

As noted in a previous review, $command is interpolated directly into a shell string without escaping. Even if current callers only pass constants, using escapeshellarg() is a safer practice.


325-325: Missing error handling for curl_init failure.

As noted in a previous review, curl_init() can return false on failure. Passing false to curl_setopt_array() will cause a TypeError.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b9aeaad and af32f12.

📒 Files selected for processing (8)
  • .vortex/tooling/src/helpers.php (1 hunks)
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/EnvTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/FormatterTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/OverrideTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/RequestTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/TokenTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/ValidateTest.php (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-13T04:14:41.765Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 2011
File: .vortex/tests/phpunit/Traits/Steps/StepAhoyTrait.php:20-23
Timestamp: 2025-09-13T04:14:41.765Z
Learning: In .vortex/tests/phpunit PHPUnit tests using cmd/cmdFail helpers, patterns in the expected output arrays support '*' markers for positive assertions (text must be present) and '!' markers for negative assertions (text must NOT be present). The '*' markers are NOT literal text matches but rather indicate expected presence.

Applied to files:

  • .vortex/tooling/tests/Unit/OverrideTest.php
  • .vortex/tooling/tests/Unit/EnvTest.php
  • .vortex/tooling/tests/Unit/FormatterTest.php
  • .vortex/tooling/tests/Unit/ValidateTest.php
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
📚 Learning: 2025-08-08T12:02:24.652Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1896
File: .vortex/tests/bats/unit/download-db-lagoon.bats:24-25
Timestamp: 2025-08-08T12:02:24.652Z
Learning: In .vortex/tests/bats/unit Bats tests using ../_helper.bash (run_steps), prefixing a STEPS entry with "- " denotes a negative assertion (the substring must NOT appear in output). Unprefixed entries are positive assertions. Example: "- Database dump refresh requested. Will create a new dump." asserts absence; "Database dump refresh requested. Will create a new dump." asserts presence.

Applied to files:

  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
🧬 Code graph analysis (8)
.vortex/tooling/tests/Unit/OverrideTest.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-28)
.vortex/tooling/src/helpers.php (1)
  • execute_override (43-52)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockPassthru (161-163)
  • mockQuit (196-212)
.vortex/tooling/tests/Unit/TokenTest.php (1)
.vortex/tooling/src/helpers.php (1)
  • replace_tokens (216-226)
.vortex/tooling/tests/Unit/EnvTest.php (1)
.vortex/tooling/src/helpers.php (1)
  • load_dotenv (60-94)
.vortex/tooling/tests/Unit/RequestTest.php (1)
.vortex/tooling/src/helpers.php (3)
  • request_get (262-268)
  • request_post (290-297)
  • request (319-376)
.vortex/tooling/tests/Unit/FormatterTest.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-28)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/src/helpers.php (2)
  • fail (172-175)
  • _supports_color (240-242)
.vortex/tooling/tests/Unit/ValidateTest.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-28)
.vortex/tooling/src/helpers.php (3)
  • validate_variable (185-190)
  • validate_command (198-203)
  • fail (172-175)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/tests/Self/MockRequestSelfTest.php (3)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (14-57)
  • runScript (27-55)
.vortex/tooling/tests/Traits/MockTrait.php (5)
  • mockRequestGet (405-418)
  • mockRequestPost (436-450)
  • mockRequest (373-389)
  • mockRequestAssertAllMocksConsumed (458-469)
  • mockRequestMultiple (229-359)
.vortex/tooling/src/helpers.php (3)
  • request_get (262-268)
  • request_post (290-297)
  • request (319-376)
.vortex/tooling/src/helpers.php (1)
.vortex/installer/src/Utils/Tui.php (3)
  • line (57-59)
  • output (38-43)
  • error (53-55)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: build (1)
  • GitHub Check: build (0)
  • GitHub Check: vortex-test-workflow (4)
  • GitHub Check: vortex-test-workflow (3)
  • GitHub Check: vortex-test-workflow (0)
  • GitHub Check: vortex-test-workflow (2)
  • GitHub Check: vortex-test-workflow (1)
  • GitHub Check: vortex-test-common
  • GitHub Check: vortex-test-installer (8.3)
  • GitHub Check: vortex-test-installer (8.4)
  • GitHub Check: vortex-test-docs
🔇 Additional comments (3)
.vortex/tooling/tests/Unit/TokenTest.php (1)

18-72: Token replacement test coverage is accurate and aligned with helper semantics

The data provider covers the key behaviors of replace_tokens() (single/multiple tokens, no matches, repeated tokens) and matches the implementation in helpers.php. Loading helpers in setUp() keeps the tests self-contained. No issues spotted.

.vortex/tooling/tests/Unit/OverrideTest.php (1)

27-97: Override execution tests correctly cover no‑op and executable scenarios

The four tests map cleanly onto the execute_override() branches:

  • Env var unset / dir without script / non‑executable script all correctly assert “no behavior”.
  • Executable case uses mockPassthru and mockQuit to validate the exact command, propagated exit code, and captured output.

This aligns with the helpers implementation and gives good confidence in override behavior.

.vortex/tooling/tests/Unit/EnvTest.php (1)

27-118: Environment loader tests match helper behavior and cover key edge cases

The data provider and dedicated tests accurately reflect load_dotenv() semantics:

  • Trimming and quote removal,
  • Values containing =,
  • Whitespace handling,
  • Skipping comments/empty lines,
  • Graceful behavior for missing files.

Helper loading in setUp() keeps tests independent. No issues found.

Comment on lines +21 to +37
public function testMockRequestGetSuccess(): void {
$this->mockRequestGet(
'https://example.com/api',
[],
10,
['status' => 200, 'body' => 'success response']
);

// Load helpers.php AFTER setting up mocks.
require_once __DIR__ . '/../../src/helpers.php';

$result = \DrevOps\VortexTooling\request_get('https://example.com/api');

$this->assertTrue($result['ok']);
$this->assertEquals(200, $result['status']);
$this->assertEquals('success response', $result['body']);
$this->assertNull($result['error']);
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

Helpers are only loaded in the first test; later tests can fail when run in isolation

helpers.php is required only inside testMockRequestGetSuccess(). All subsequent tests in this class call \DrevOps\VortexTooling\request_get/post/request but never ensure the helpers are loaded themselves.

This couples the whole class to the execution order:

  • Running testMockRequestPostSuccess (or any later test) in isolation, or with randomized test order, can lead to fatal undefined function errors.
  • Tests should be independently runnable.

Recommend centralizing helper loading in setUp() and dropping the inline require_once in the first test, e.g.:

-class MockRequestSelfTest extends UnitTestCase {
-
-  public function testMockRequestGetSuccess(): void {
-    $this->mockRequestGet(
+class MockRequestSelfTest extends UnitTestCase {
+
+  protected function setUp(): void {
+    parent::setUp();
+    require_once __DIR__ . '/../../src/helpers.php';
+  }
+
+  public function testMockRequestGetSuccess(): void {
+    $this->mockRequestGet(
       'https://example.com/api',
@@
-    // Load helpers.php AFTER setting up mocks.
-    require_once __DIR__ . '/../../src/helpers.php';
-
     $result = \DrevOps\VortexTooling\request_get('https://example.com/api');

The curl function mocks created in mockRequest* will still work correctly with this setup, since PHP resolves curl_* calls dynamically against the namespaced mocks.

🤖 Prompt for AI Agents
.vortex/tooling/tests/Self/MockRequestSelfTest.php lines 21-37: helpers.php is
only required inside testMockRequestGetSuccess causing later tests to fail when
run in isolation; move the require_once into the test class setUp() so helpers
are loaded before every test and remove the inline require_once from the test;
update setUp() to require_once __DIR__ . '/../../src/helpers.php' (or ensure
autoloading) so all tests can call
\DrevOps\VortexTooling\request_get/post/request independently and keep existing
mockRequest* curl mocks working.

Comment on lines 106 to 112
public static function providerOutputFormattersWithColor(): array {
return [
'task' => ['task', '[TASK]'],
'info' => ['info', '[INFO]'],
'pass' => ['pass', '[ OK ]'],
];
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add test coverage for fail_no_exit with color support.

The fail_no_exit formatter supports color output (red ANSI codes) but is missing from this data provider. This results in incomplete test coverage for colored output.

Apply this diff to add the missing test case:

   public static function providerOutputFormattersWithColor(): array {
     return [
       'task' => ['task', '[TASK]'],
       'info' => ['info', '[INFO]'],
       'pass' => ['pass', '[ OK ]'],
+      'fail_no_exit' => ['fail_no_exit', '[FAIL]'],
     ];
   }
📝 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
public static function providerOutputFormattersWithColor(): array {
return [
'task' => ['task', '[TASK]'],
'info' => ['info', '[INFO]'],
'pass' => ['pass', '[ OK ]'],
];
}
public static function providerOutputFormattersWithColor(): array {
return [
'task' => ['task', '[TASK]'],
'info' => ['info', '[INFO]'],
'pass' => ['pass', '[ OK ]'],
'fail_no_exit' => ['fail_no_exit', '[FAIL]'],
];
}
🤖 Prompt for AI Agents
In .vortex/tooling/tests/Unit/FormatterTest.php around lines 106 to 112, the
data provider providerOutputFormattersWithColor is missing the colored entry for
the fail_no_exit formatter; add a new array entry for 'fail_no_exit' that maps
to the formatter name and the expected red-colored output (ANSI red start,
[FAIL], ANSI reset) so the provider includes ['fail_no_exit',
"\e[31m[FAIL]\e[0m"] (ensure proper PHP string escaping) to restore full color
test coverage.

@AlexSkrypnyk AlexSkrypnyk force-pushed the feature/1192-tooling-package branch from 2cde55e to 40fadd2 Compare November 23, 2025 05:16
@github-actions github-actions bot temporarily deployed to commit November 23, 2025 05:19 Inactive
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: 5

♻️ Duplicate comments (34)
.vortex/tooling/tests/Self/MockRequestSelfTest.php (2)

18-19: Load helpers.php in setUp() so tests don’t depend on method execution order

helpers.php is currently required only in testMockRequestGetSuccess(), but all tests in this class call request_get/post/request. Running any later test in isolation (or with randomized order) can hit undefined function fatals. Centralizing the require in setUp() keeps every test independently runnable and matches the earlier feedback.

You can refactor like this:

-#[CoversClass(UnitTestCase::class)]
-class MockRequestSelfTest extends UnitTestCase {
-
-  public function testMockRequestGetSuccess(): void {
+#[CoversClass(UnitTestCase::class)]
+class MockRequestSelfTest extends UnitTestCase {
+
+  protected function setUp(): void {
+    parent::setUp();
+    require_once __DIR__ . '/../../src/helpers.php';
+  }
+
+  public function testMockRequestGetSuccess(): void {
@@
-    // Load helpers.php AFTER setting up mocks.
-    require_once __DIR__ . '/../../src/helpers.php';
-
-    $result = \DrevOps\VortexTooling\request_get('https://example.com/api');
+    $result = \DrevOps\VortexTooling\request_get('https://example.com/api');

Also applies to: 21-38


7-8: Replace fully‑qualified helper/exception references with imports and drop the PHPCS suppression

The file disables Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing and then uses \DrevOps\VortexTooling\request_* plus fully-qualified exception classes throughout. This is exactly what PHPCS is flagging in the tooling job and was already called out previously.

Instead, import the functions and exception classes once and use short names in the body; then you can remove the suppression entirely:

 namespace DrevOps\VortexTooling\Tests\Self;
 
 use DrevOps\VortexTooling\Tests\Unit\UnitTestCase;
 use PHPUnit\Framework\Attributes\CoversClass;
+use InvalidArgumentException;
+use PHPUnit\Framework\AssertionFailedError;
+use RuntimeException;
+use function DrevOps\VortexTooling\request;
+use function DrevOps\VortexTooling\request_get;
+use function DrevOps\VortexTooling\request_post;
@@
  /**
   * Self-tests for mocking of HTTP request functions.
@@
- *
- * @phpcs:disable Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing
   */
@@
-    $result = \DrevOps\VortexTooling\request_get('https://example.com/api');
+    $result = request_get('https://example.com/api');
@@
-    $result = \DrevOps\VortexTooling\request_get('https://example.com/api');
+    $result = request_get('https://example.com/api');
@@
-    $result = \DrevOps\VortexTooling\request_get('https://example.com/api');
+    $result = request_get('https://example.com/api');
@@
-    $result = \DrevOps\VortexTooling\request_get('https://example.com/not-found');
+    $result = request_get('https://example.com/not-found');
@@
-    $result = \DrevOps\VortexTooling\request_get('https://example.com/error');
+    $result = request_get('https://example.com/error');
@@
-    $result = \DrevOps\VortexTooling\request_get('https://example.com/timeout');
+    $result = request_get('https://example.com/timeout');
@@
-    $result = \DrevOps\VortexTooling\request_post('https://example.com/api', '{"key":"value"}');
+    $result = request_post('https://example.com/api', '{"key":"value"}');
@@
-    $result = \DrevOps\VortexTooling\request_post(
+    $result = request_post(
@@
-    $result = \DrevOps\VortexTooling\request('https://example.com/resource', ['method' => 'PUT']);
+    $result = request('https://example.com/resource', ['method' => 'PUT']);
@@
-    $result1 = \DrevOps\VortexTooling\request_get('https://example.com/first');
+    $result1 = request_get('https://example.com/first');
@@
-    $result2 = \DrevOps\VortexTooling\request_post('https://example.com/second', 'data');
+    $result2 = request_post('https://example.com/second', 'data');
@@
-    $result3 = \DrevOps\VortexTooling\request('https://example.com/third', ['method' => 'PUT']);
+    $result3 = request('https://example.com/third', ['method' => 'PUT']);
@@
-    \DrevOps\VortexTooling\request_get('https://example.com/first');
-    \DrevOps\VortexTooling\request_get('https://example.com/second');
+    request_get('https://example.com/first');
+    request_get('https://example.com/second');
@@
-    \DrevOps\VortexTooling\request_get('https://example.com/third');
+    request_get('https://example.com/third');
@@
-    \DrevOps\VortexTooling\request_get('https://example.com/first');
-    \DrevOps\VortexTooling\request_get('https://example.com/second');
+    request_get('https://example.com/first');
+    request_get('https://example.com/second');
@@
-    \DrevOps\VortexTooling\request_get('https://example.com/actual');
+    request_get('https://example.com/actual');
@@
-    \DrevOps\VortexTooling\request_get('https://example.com/api');
+    request_get('https://example.com/api');
@@
-    \DrevOps\VortexTooling\request_get('https://example.com/api');
+    request_get('https://example.com/api');
@@
-    \DrevOps\VortexTooling\request_get('https://example.com/api');
+    request_get('https://example.com/api');
@@
-    $this->expectException(\RuntimeException::class);
+    $this->expectException(RuntimeException::class);
@@
-    $this->expectException(\PHPUnit\Framework\AssertionFailedError::class);
+    $this->expectException(AssertionFailedError::class);
@@
-    $this->expectException(\RuntimeException::class);
+    $this->expectException(RuntimeException::class);
@@
-    $this->expectException(\RuntimeException::class);
+    $this->expectException(RuntimeException::class);
@@
-    $this->expectException(\PHPUnit\Framework\AssertionFailedError::class);
+    $this->expectException(AssertionFailedError::class);
@@
-    $this->expectException(\RuntimeException::class);
+    $this->expectException(RuntimeException::class);
@@
-    $this->expectException(\InvalidArgumentException::class);
+    $this->expectException(InvalidArgumentException::class);
@@
-    $this->expectException(\RuntimeException::class);
+    $this->expectException(RuntimeException::class);

This should clear the PHPCS violations and keeps the tests cleaner and more idiomatic.

Also applies to: 16-16, 32-35, 54-57, 72-76, 92-96, 111-115, 131-136, 148-152, 164-172, 182-186, 211-221, 228-235, 242-247, 260-263, 277-283, 297-303, 316-320, 330-334, 349-353, 367-373, 388-394, 418-425, 435-438, 470-477, 487-491, 494-509, 512-523

.vortex/tooling/scripts_conversion_plan.md (1)

84-166: Align planned helper names with actual implementation

The planning document references functions like load_env(), execute_override_if_exists(), require_env(), and require_command(), but the actual helpers.php implementation uses load_dotenv(), execute_override(), getenv_required(), and command_exists(). Updating these examples will prevent confusion when implementing or referencing this plan.

Also applies to: 196-261, 408-417

.vortex/tooling/phpcs.xml (2)

21-30: Consider simplifying file specification if PHPCS scans recursively

Lines 22-28 individually list each src/notify-* subdirectory, which may be redundant since line 21 already includes <file>src</file>. If PHPCS is configured to scan directories recursively (the default behavior), you can remove lines 22-28. Confirm PHPCS behavior and either remove the redundant entries or add a comment explaining why explicit enumeration is necessary.


37-56: Verify exclude-pattern glob syntax for test files

The patterns *.Test\.php, *.TestCase\.php, and *.test use backslash-escaped dots, but PHPCS interprets exclude-pattern as shell-style globs where dots don't need escaping. Change them to *Test.php, *TestCase.php, or **/*Test.php / **/*TestCase.php if you need to match subdirectories. Also verify whether *.test matches real filenames or should be adjusted.

.vortex/tooling/src/notify-slack (3)

18-20: Remove blank line after inline comment to fix coding standard violation

Pipeline failure indicates a blank line following the inline comment separator at line 18 violates Drupal.Commenting.InlineComment.SpacingAfter. Remove the blank line so the comment is immediately followed by the next code line.

Apply this diff:

 // -----------------------------------------------------------------------------
-
 // Slack notification project name.

62-64: Remove blank line after inline comment to fix coding standard violation

Pipeline failure indicates a blank line following the inline comment separator at line 62 violates Drupal.Commenting.InlineComment.SpacingAfter. Remove the blank line so the comment is immediately followed by the next code line.

Apply this diff:

 // ------------------------------------------------------------------------------
-
 info('Started Slack notification.');

132-134: Optionally validate Slack response body for stricter error detection

Currently the script only fails on non-200 HTTP status. Slack webhooks can return HTTP 200 with error strings in the body (e.g., "invalid_payload", "no_text"). Consider checking that the body equals "ok" or doesn't contain error indicators for more robust error detection.

Example enhancement:

 if ($response['status'] !== 200) {
   fail('Unable to send notification to Slack. HTTP status: %s', $response['status']);
 }
+
+$body = trim($response['body'] ?? '');
+if ($body !== 'ok') {
+  fail('Slack webhook returned unexpected response: %s', $body);
+}
.vortex/tooling/tests/Unit/NotifyGithubTest.php (1)

24-32: Change test token value to avoid secret scanner false positive

The hard-coded value 'ghp_test123456' matches GitHub's personal access token prefix and triggers Gitleaks as a generic API key, causing pipeline failures. Change it to a clearly fake value that won't match token patterns (e.g., 'test-token', 'TEST_GITHUB_TOKEN', or a split concatenation).

Apply this diff:

     $this->envSetMultiple([
-      'VORTEX_NOTIFY_GITHUB_TOKEN' => 'ghp_test123456',
+      'VORTEX_NOTIFY_GITHUB_TOKEN' => 'test-github-token',
       'VORTEX_NOTIFY_GITHUB_REPOSITORY' => 'owner/repo',

You'll also need to update the token in mock assertions throughout the file (lines 48, 73, 91, 118, 134, 148, 161, 211).

.vortex/tooling/tests/Unit/GetenvTest.php (1)

137-171: Fix unreachable output assertions in exception tests

Tests at lines 137-146, 148-160, and 162-171 combine expectException() with captureOutput(), but since captureOutput() rethrows exceptions (line 187), the assignment to $output never completes and the subsequent assertStringContainsString assertions are unreachable. The tests aren't validating the error messages.

Fix by removing expectException() and manually catching the exception:

  public function testGetenvRequiredWithNoVarsSetFails(): void {
    $this->mockQuit(1);
-   $this->expectException(QuitErrorException::class);

-   $output = $this->captureOutput(function (): void {
-     \DrevOps\VortexTooling\getenv_required('UNSET_VAR_1', 'UNSET_VAR_2');
-   });
-
-   $this->assertStringContainsString('Missing required value for UNSET_VAR_1, UNSET_VAR_2', $output);
+   try {
+     $output = $this->captureOutput(function (): void {
+       \DrevOps\VortexTooling\getenv_required('UNSET_VAR_1', 'UNSET_VAR_2');
+     });
+     $this->fail('Expected QuitErrorException was not thrown');
+   }
+   catch (QuitErrorException $e) {
+     $output = ob_get_clean() ?: '';
+     $this->assertStringContainsString('Missing required value for UNSET_VAR_1, UNSET_VAR_2', $output);
+   }
  }

Apply similar fixes to testGetenvRequiredWithAllEmptyStringsFails() (lines 148-160) and testGetenvRequiredWithSingleVarNotSetFails() (lines 162-171).

Also applies to: 148-160, 162-171

.vortex/tooling/src/notify-newrelic (2)

95-116: Tighten app ID discovery: guard JSON decode and only log “Discovered” when ID is actually found

In the discovery block, two things are slightly off:

  • $data = json_decode(...) is not validated before indexing, and
  • pass('Discovered application ID: %s', $newrelic_appid); runs even when no ID was resolved or the API call failed (so it can claim discovery with an empty ID).

You can keep behavior but make the control flow stricter:

   if ($response['ok'] && $response['body']) {
     $data = json_decode((string) $response['body'], TRUE);
-    if (isset($data['applications'][0]['id'])) {
-      $newrelic_appid = $data['applications'][0]['id'];
-    }
-
-    if (!$newrelic_appid) {
-      pass('Notification skipped: No New Relic application ID found for %s. This is expected for non-configured environments.', $newrelic_app_name);
-      quit();
-    }
-  }
-
-  pass('Discovered application ID: %s', $newrelic_appid);
+    if (is_array($data) && isset($data['applications'][0]['id'])) {
+      $newrelic_appid = $data['applications'][0]['id'];
+    }
+
+    if (!$newrelic_appid) {
+      pass(
+        'Notification skipped: No New Relic application ID found for %s. This is expected for non-configured environments.',
+        $newrelic_app_name,
+      );
+      quit();
+    }
+
+    pass('Discovered application ID: %s', $newrelic_appid);
+  }

This keeps the “skip” behavior for non-configured environments and avoids a misleading “Discovered application ID” message when nothing was resolved, while also avoiding indexing into a non-array decode result.


78-80: PHPCS: remove blank line after separator comment

There is a blank line immediately after the separator comment, which Drupal’s InlineComment.SpacingAfter rule flags (same issue as in earlier runs):

-// ------------------------------------------------------------------------------
-
-info('Started New Relic notification.');
+// ------------------------------------------------------------------------------
+info('Started New Relic notification.');
.vortex/tooling/src/notify-webhook (2)

16-17: PHPCS: remove blank lines after separator comments

Both separator comments are followed by a blank line, which violates Drupal’s InlineComment.SpacingAfter rule already reported by tooling:

-// -----------------------------------------------------------------------------
-
+// -----------------------------------------------------------------------------
@@
-// -----------------------------------------------------------------------------
-
+// -----------------------------------------------------------------------------

Removing the blank lines will keep coding standards happy.

Also applies to: 63-64


44-49: Harden header parsing to drop empty entries from VORTEX_NOTIFY_WEBHOOK_HEADERS

If VORTEX_NOTIFY_WEBHOOK_HEADERS has trailing or repeated |, explode('|', $webhook_headers) yields empty strings which are then passed through to CURLOPT_HTTPHEADER. Cheap to filter them out:

-// Webhook notification pipe-separated headers.
+// Webhook notification pipe-separated headers.
@@
-$webhook_headers = getenv_default('VORTEX_NOTIFY_WEBHOOK_HEADERS', 'Content-type: application/json');
+$webhook_headers = getenv_default('VORTEX_NOTIFY_WEBHOOK_HEADERS', 'Content-type: application/json');
@@
-$response = request($webhook_url, [
-  'method' => $webhook_method,
-  'headers' => array_map(trim(...), explode('|', $webhook_headers)),
-  'body' => $webhook_payload,
-]);
+$response = request($webhook_url, [
+  'method' => $webhook_method,
+  'headers' => array_filter(
+    array_map(trim(...), explode('|', $webhook_headers)),
+    static fn(string $header): bool => $header !== ''
+  ),
+  'body' => $webhook_payload,
+]);

This keeps the request cleaner and avoids passing meaningless empty header values to cURL.

Also applies to: 97-102

.vortex/tooling/src/notify-github (2)

47-66: Relax deployment ID length constraint to avoid future breakage

deployment_id_is_valid() enforces a 9–11 digit length:

$length = strlen($id_str);
// Check length (9-11 digits) and numeric format.
return $length >= 9 && $length <= 11 && ctype_digit($id_str);

This is brittle if GitHub ever changes ID size (or if current IDs outside that range appear). Keeping the numeric check but dropping the upper bound (and possibly the lower bound) makes the validation future‑proof while still rejecting obviously bad values:

-  $length = strlen($id_str);
-
-  // Check length (9-11 digits) and numeric format.
-  return $length >= 9 && $length <= 11 && ctype_digit($id_str);
+  $length = strlen($id_str);
+
+  // Check length (at least 1 digit) and numeric format.
+  return $length >= 1 && ctype_digit($id_str);

Adjust the comment accordingly.


23-24: Coding standards and GitHub API robustness: remove blank after separator and set JSON Content‑Type

Two small, related tweaks:

  1. PHPCS separator comment – same as other scripts, there’s a blank line after the separator:
-// -----------------------------------------------------------------------------
-
+// -----------------------------------------------------------------------------
  1. Explicit JSON Content-Type for GitHub POSTs – both deployment creation and status update send JSON bodies, but only set Accept. It’s safer to be explicit:
   $response = request_post($url, json_encode($payload_data), [
     'Authorization: token ' . $github_token,
     'Accept: application/vnd.github.v3+json',
+    'Content-Type: application/json',
   ]);
@@
   $response = request_post($url, json_encode($status_data), [
-    'Accept: application/vnd.github.v3+json',
-    'Authorization: token ' . $github_token,
+    'Accept: application/vnd.github.v3+json',
+    'Authorization: token ' . $github_token,
+    'Content-Type: application/json',
   ]);

Together this keeps both PHPCS and the GitHub API interactions tidy.

Also applies to: 84-88, 146-150

.vortex/tooling/tests/Unit/RequestTest.php (1)

10-100: Avoid real network calls in unit tests; prefer mocked or isolated integration tests

These tests exercise request_* by calling example.com directly (including timeouts and non‑200 codes). That ties the unit suite to external network/DNS and example.com’s behavior, making it flaky and environment‑dependent.

Consider instead:

  • Using your existing helpers/mocks (e.g., MockTrait + mockRequest*) to test request_get, request_post, and request behavior against predictable fake responses; or
  • Moving these into a clearly marked integration group that can be skipped when network access isn’t available.

That keeps unit runs deterministic while still validating the HTTP helpers where needed.

.vortex/tooling/src/notify-email (3)

100-114: Reuse a single timestamp for subject and body tokens.

date('d/m/Y H:i:s T') is computed twice, so %timestamp% in subject and body can drift by a second. Compute it once and reuse:

[ suggest_optional_refactor ]

-$email_subject = replace_tokens($email_subject, [
-  'project' => $notify_project,
-  'label' => $notify_label,
-  'timestamp' => date('d/m/Y H:i:s T'),
-  'environment_url' => $notify_env_url,
-  'login_url' => $notify_login_url,
-]);
-
-$email_message = replace_tokens($email_message, [
-  'project' => $notify_project,
-  'label' => $notify_label,
-  'timestamp' => date('d/m/Y H:i:s T'),
-  'environment_url' => $notify_env_url,
-  'login_url' => $notify_login_url,
-]);
+$timestamp = date('d/m/Y H:i:s T');
+
+$email_subject = replace_tokens($email_subject, [
+  'project' => $notify_project,
+  'label' => $notify_label,
+  'timestamp' => $timestamp,
+  'environment_url' => $notify_env_url,
+  'login_url' => $notify_login_url,
+]);
+
+$email_message = replace_tokens($email_message, [
+  'project' => $notify_project,
+  'label' => $notify_label,
+  'timestamp' => $timestamp,
+  'environment_url' => $notify_env_url,
+  'login_url' => $notify_login_url,
+]);

128-160: Sanitize email header fields to prevent header injection (CRITICAL).

$email_from, $email_subject, $email, and $name come from environment input and are interpolated directly into To:, Subject: and From: headers (and into the sendmail -f argument) without stripping CR/LF. An attacker controlling these values can inject additional headers or recipients via \r / \n.

Introduce a small sanitizer and apply it before composing headers and shell commands:

@@
 namespace DrevOps\VortexTooling;
 
 require_once __DIR__ . '/helpers.php';
 execute_override(basename(__FILE__));
 
+/**
+ * Sanitize email header value to prevent header injection.
+ */
+function sanitize_email_header(string $value): string {
+  return str_replace(["\r", "\n", "\0"], '', $value);
+}
@@
-$email_recipients = array_map(trim(...), explode(',', $email_recipients));
+$email_recipients = array_map(trim(...), explode(',', $email_recipients));
@@
 $sent = [];
 foreach ($email_recipients as $email_recipient) {
   $parts = array_map(trim(...), explode('|', $email_recipient, 2));
   $email = $parts[0];
   $name = $parts[1] ?? '';
+
+  // Sanitize values used in headers.
+  $email = sanitize_email_header($email);
+  $name = sanitize_email_header($name);
+  $safe_subject = sanitize_email_header($email_subject);
+  $safe_from = sanitize_email_header($email_from);
 
-  $to = empty($name) ? $email : sprintf('"%s" <%s>', $name, $email);
+  $to = empty($name)
+    ? $email
+    : sprintf('"%s" <%s>', addslashes($name), $email);
@@
   if ($has_sendmail) {
     $mail_content = sprintf('To: %s%s', $to, PHP_EOL);
-    $mail_content .= sprintf('Subject: %s%s', $email_subject, PHP_EOL);
-    $mail_content .= sprintf('From: %s%s', $email_from, PHP_EOL);
+    $mail_content .= sprintf('Subject: %s%s', $safe_subject, PHP_EOL);
+    $mail_content .= sprintf('From: %s%s', $safe_from, PHP_EOL);
@@
-    $cmd = sprintf('echo %s | sendmail -t -f %s', escapeshellarg($mail_content), escapeshellarg($email_from));
+    $cmd = sprintf('echo %s | sendmail -t -f %s', escapeshellarg($mail_content), escapeshellarg($safe_from));
@@
   }
   elseif ($has_mail) {
-    $mail_content = "From: {$email_from}\n\n{$email_message}";
-    $cmd = sprintf('echo %s | mail -s %s %s', escapeshellarg($mail_content), escapeshellarg($email_subject), escapeshellarg($to));
+    $mail_content = "From: {$safe_from}\n\n{$email_message}";
+    $cmd = sprintf(
+      'echo %s | mail -s %s %s',
+      escapeshellarg($mail_content),
+      escapeshellarg($safe_subject),
+      escapeshellarg($to)
+    );

This keeps header values single-line and blocks injected headers while preserving current behaviour for valid inputs.


128-160: Harden recipient parsing and improve observability of send failures.

Empty items from explode(',', $email_recipients) (extra commas/whitespace) currently produce blank To: headers and pointless exec() calls, and failures are silent.

Filter out empty recipients and optionally track/log failures per address:

-$email_recipients = array_map(trim(...), explode(',', $email_recipients));
+$email_recipients = array_filter(
+  array_map(trim(...), explode(',', $email_recipients)),
+  static fn(string $value): bool => $value !== ''
+);
 
-$sent = [];
+$sent = [];
+$failed = [];
@@
-    exec($cmd, $output, $return_var);
-
-    if ($return_var === 0) {
-      $sent[] = $email;
-    }
+    exec($cmd, $output, $return_var);
+
+    if ($return_var === 0) {
+      $sent[] = $email;
+    }
+    else {
+      $failed[] = $email;
+      note(sprintf('Failed to send email to: %s', $email));
+    }
@@
-    exec($cmd, $output, $return_var);
-
-    if ($return_var === 0) {
-      $sent[] = $email;
-    }
+    exec($cmd, $output, $return_var);
+
+    if ($return_var === 0) {
+      $sent[] = $email;
+    }
+    else {
+      $failed[] = $email;
+      note(sprintf('Failed to send email to: %s', $email));
+    }
@@
-if (!empty($sent)) {
-  pass('Email notification sent successfully to %d recipient(s).', count($sent));
+if (!empty($sent)) {
+  pass('Email notification sent successfully to %d recipient(s).', count($sent));
+  if (!empty($failed)) {
+    note('Failed to send email notification to %d recipient(s).', count($failed));
+  }

This avoids invalid recipients and makes it easier to debug partial failures.

.vortex/tooling/tests/Unit/TokenTest.php (1)

32-55: Consider adding data-provider cases that exercise JSON escaping in replace_tokens().

Current scenarios cover basic replacement but not values that need escaping (quotes, backslashes, newlines), even though replace_tokens() uses json_encode() internally.

You could extend dataProviderReplaceTokens() with cases where replacements include:

  • A string containing double quotes (He said "hi").
  • A Windows-style path with backslashes (C:\folder\file).
  • A multi-line value with \n.

Asserting the exact escaped output would lock in this behaviour and make regressions in the escaping logic easier to catch, based on replace_tokens() in .vortex/tooling/src/helpers.php.

.vortex/tooling/tests/Unit/OverrideTest.php (1)

26-64: Use EnvTrait helpers instead of direct putenv() in tests.

These tests manipulate VORTEX_TOOLING_CUSTOM_DIR via putenv(), bypassing EnvTrait’s tracking and automatic cleanup in UnitTestCase. That can leak state into other tests.

Replace direct putenv() calls with EnvTrait methods:

   public function testExecuteOverrideNoCustomDir(): void {
-    putenv('VORTEX_TOOLING_CUSTOM_DIR');
+    $this->envUnset('VORTEX_TOOLING_CUSTOM_DIR');
@@
   public function testExecuteOverrideCustomDirNoFile(): void {
     $custom_dir = self::$tmp . '/custom';
     mkdir($custom_dir, 0777, TRUE);
-    putenv('VORTEX_TOOLING_CUSTOM_DIR=' . $custom_dir);
+    $this->envSet('VORTEX_TOOLING_CUSTOM_DIR', $custom_dir);
@@
   public function testExecuteOverrideCustomDirNonExecutable(): void {
@@
-    chmod($script_path, 0644);
-    putenv('VORTEX_TOOLING_CUSTOM_DIR=' . $custom_dir);
+    chmod($script_path, 0644);
+    $this->envSet('VORTEX_TOOLING_CUSTOM_DIR', $custom_dir);
@@
   public function testExecuteOverrideWithExecutable(): void {
@@
-    chmod($script_path, 0755);
-    putenv('VORTEX_TOOLING_CUSTOM_DIR=' . $custom_dir);
+    chmod($script_path, 0755);
+    $this->envSet('VORTEX_TOOLING_CUSTOM_DIR', $custom_dir);

This keeps environment mutations under the same control as the rest of the tooling tests. Based on learnings.

.vortex/tooling/tests/Unit/FormatterTest.php (2)

33-107: Avoid direct putenv() for TERM; use EnvTrait helpers for consistent env management.

The formatter tests manipulate TERM with putenv(), which bypasses EnvTrait tracking and teardown in UnitTestCase. This can leave the process environment in a surprising state for other tests.

Update to use envSet / envUnset:

   #[DataProvider('dataProviderOutputFormatters')]
   public function testOutputFormatters(string $function, string $expected_prefix): void {
-    putenv('TERM=dumb');
+    $this->envSet('TERM', 'dumb');
@@
   #[DataProvider('dataProviderOutputFormattersWithColor')]
   public function testOutputFormattersWithColor(string $function, string $expected_prefix): void {
-    putenv('TERM=xterm-256color');
+    $this->envSet('TERM', 'xterm-256color');
@@
   #[DataProvider('dataProviderTermSupportsColor')]
   public function testTermSupportsColor(string|bool $term_value, bool $expected): void {
     if ($term_value === FALSE) {
-      putenv('TERM');
+      $this->envUnset('TERM');
     }
     else {
-      putenv('TERM=' . $term_value);
+      $this->envSet('TERM', $term_value);
     }

This keeps env mutations under EnvTrait’s control and consistent with other tests. Based on learnings.


77-118: Broaden formatter/color test coverage (fail_no_exit and positive color support).

Two small gaps in coverage:

  1. fail_no_exit is not included in dataProviderOutputFormattersWithColor(), so its colored output isn’t exercised.
  2. dataProviderTermSupportsColor() only includes cases where color is not supported; there’s no positive case.

You can address both via the data providers:

   public static function dataProviderOutputFormattersWithColor(): array {
     return [
       'task' => ['task', '[TASK]'],
       'info' => ['info', '[INFO]'],
       'pass' => ['pass', '[ OK ]'],
+      'fail_no_exit' => ['fail_no_exit', '[FAIL]'],
     ];
   }
@@
   public static function dataProviderTermSupportsColor(): array {
     return [
       'dumb terminal' => ['term_value' => 'dumb', 'expected' => FALSE],
       'no terminal' => ['term_value' => FALSE, 'expected' => FALSE],
+      'color terminal' => ['term_value' => 'xterm-256color', 'expected' => TRUE],
     ];
   }

This ensures fail_no_exit is covered in color-mode output and that term_supports_color() is asserted for a color-capable TERM as well.

.vortex/tooling/tests/Self/MockQuitSelfTest.php (1)

61-107: Script-based tests: output assertions after expectException() are unreachable

In the script tests (testMockQuitScript0Success, testMockQuitScript0Failure, testMockQuitScript1Success, testMockQuitScript1Failure) you:

  • Call mockQuit(...).
  • Configure expectException(...) / expectExceptionMessage(...).
  • Call $this->runScript(...).
  • Then assert on $output.

With PHPUnit, once the expected exception is thrown out of runScript(), the test method stops executing; the $output assignment and subsequent assertions never run. As a result, these tests do not currently validate the script output.

Given the new UnitTestCase::runScript / runScriptEarlyPass / runScriptError helpers, consider one of these patterns instead:

  • Manual catch pattern (no expectException):
-    $this->mockQuit();
-
-    $this->expectException(QuitSuccessException::class);
-    $this->expectExceptionCode(0);
-
-    $output = $this->runScript('tests/Fixtures/test-quit-passing');
-
-    $this->assertStringContainsString('Script will exit with code 0', $output);
-    $this->assertStringNotContainsString('ERROR Script continued after quit()', $output);
+    $this->mockQuit();
+
+    try {
+      $this->runScript('tests/Fixtures/test-quit-passing');
+      $this->fail('Expected QuitSuccessException to be thrown');
+    }
+    catch (QuitSuccessException $e) {
+      $this->assertSame(0, $e->getCode());
+      $output = $e->getOutput();
+      $this->assertStringContainsString('Script will exit with code 0', $output);
+      $this->assertStringNotContainsString('ERROR Script continued after quit()', $output);
+    }
  • Or switch to runScriptEarlyPass / runScriptError and let them assert on the output internally, without adding expectException or extra assertions in these tests.
.vortex/tooling/tests/Self/CheckNoExitSelfTest.php (3)

29-36: Use a unique temporary directory per test run and avoid mkdir() warnings

Reusing a fixed $this->testDir and calling mkdir() on it and its subdirectories in every setUp() risks:

  • PHP warnings when directories already exist (often fatal with failOnWarning="true").
  • Leftover fixtures from previous tests (e.g., files with exit()) breaking testHandlesEmptyDirectories() and making tests order-dependent.

Create a per-test directory (e.g., with uniqid()) and use recursive mkdir() so parents are created as needed:

   protected function setUp(): void {
     parent::setUp();
     $this->scriptPath = dirname(__DIR__, 2) . '/check-no-exit.php';
-    $this->testDir = self::$tmp . '/check-exit-test';
-    mkdir($this->testDir);
-    mkdir($this->testDir . '/src');
-    mkdir($this->testDir . '/tests');
+    $this->testDir = self::$tmp . '/check-exit-test-' . uniqid('', true);
+    mkdir($this->testDir . '/src', 0777, true);
+    mkdir($this->testDir . '/tests', 0777, true);
   }

47-50: Factor out the exec() invocation and escape shell arguments

Each test builds the same exec('cd %s && php %s 2>&1', …) inline and does not escape $this->testDir / $this->scriptPath. This is:

  • Repetitive across six call sites.
  • Fragile if paths ever contain spaces or shell metacharacters.

Consider extracting a small helper that escapes both arguments and returns [$output, $exit_code], then reuse it across tests. For example:

/**
 * Run check-no-exit.php from the current test directory.
 *
 * @return array{0: string[], 1: int}
 */
private function runCheckNoExitScript(): array {
  $output = [];
  $exit_code = 0;

  $cmd = sprintf(
    'cd %s && php %s 2>&1',
    escapeshellarg($this->testDir),
    escapeshellarg($this->scriptPath),
  );
  exec($cmd, $output, $exit_code);

  return [$output, $exit_code];
}

And in tests:

[$output, $exit_code] = $this->runCheckNoExitScript();

Apply the pattern to all the listed exec() sites.

Also applies to: 65-68, 82-85, 103-106, 120-123, 133-136


113-126: Also assert on the non‑zero exit code in the line‑number test

testReportsCorrectLineNumbers() currently only checks that 'line-test.php:5' appears in the output. To ensure the checker fails the build when an exit() is detected, also assert that $exit_code is non‑zero, consistent with your other “exit detected” tests:

   $output_str = implode("\n", $output);
-  $this->assertStringContainsString('line-test.php:5', $output_str);
+  $this->assertEquals(1, $exit_code);
+  $this->assertStringContainsString('line-test.php:5', $output_str);
.vortex/tooling/tests/Unit/NotifyNewrelicTest.php (1)

37-52: New Relic deployment payload callbacks are not exercised due to mockRequestPost() limitations

Here and in other tests you use a $this->callback(function ($body) { … }) argument to mockRequestPost() to assert on the JSON deployment payload (revision, user, description, changelog, etc.). Given the current MockTrait::mockRequestPost() implementation only forwards url, method, and response to mockRequestMultiple, those callbacks are never invoked, so these assertions don’t actually run.

Once you update the mocking helper as suggested in NotifySlackTest (propagating body/headers/timeout through mockRequestPost/mockRequestMultiple and having the stub invoke any callback on the body), these tests will start validating the New Relic request payloads as intended.

.vortex/tooling/tests/Unit/NotifyJiraTest.php (1)

48-57: JIRA comment‑payload callbacks suffer from the same mockRequestPost() limitation

In this test (and others) you pass a $this->callback(function ($body) { … }) to mockRequestPost() to inspect the JSON body sent to JIRA. As noted in the Slack/New Relic tests, the current MockTrait::mockRequestPost() forwards only url, method, and response to mockRequestMultiple, so the $body callback is never invoked and these payload assertions never execute.

After you update the mocking helper to propagate and use the body/headers/timeout data, the JIRA tests that rely on body callbacks will start validating the request content as intended.

.vortex/tooling/src/helpers.php (4)

43-52: Path traversal vulnerability remains unaddressed

The past review correctly identified that $name is concatenated directly into a file path without validation (line 46). While current usage may pass basename(__FILE__), the function signature accepts any string, creating a path traversal vulnerability if called with untrusted input or if usage evolves.

As recommended in the past review, validate $name before use:

  • Ensure it contains no directory separators (/, \, DIRECTORY_SEPARATOR)
  • Verify the resolved path remains within VORTEX_TOOLING_CUSTOM_DIR
  • Check the file is executable before calling passthru

Based on past review comments.


278-283: Shell command injection risk in command_exists()

The $command parameter is interpolated directly into a shell command without escaping (line 279). This creates a command injection vulnerability if $command ever contains shell metacharacters.

Apply the fix suggested in past reviews:

 function command_exists(string $command): void {
-  exec(sprintf('command -v %s 2>/dev/null', $command), $output, $code);
+  exec(sprintf('command -v %s 2>/dev/null', escapeshellarg($command)), $output, $code);
   if ($code !== 0) {
     fail(sprintf("Command '%s' is not available", $command));
   }
 }

Based on past review comments.


404-404: Missing curl_init() failure check

curl_init() can return false on failure. Passing false to curl_setopt_array() on line 426 will cause a TypeError in PHP 8+.

Apply the fix from past reviews:

   $ch = curl_init($url);
+  if ($ch === FALSE) {
+    fail('Failed to initialize curl for URL: %s', $url);
+  }

Based on past review comments.


426-426: Missing curl_setopt_array() failure check

curl_setopt_array() returns false if setting options fails, leaving the cURL handle misconfigured. The request would proceed with incorrect settings.

Add error handling after the call:

   curl_setopt_array($ch, $opts);
+  if (curl_error($ch)) {
+    fail('Failed to set curl options: %s', curl_error($ch));
+  }

Note: This is a separate issue from the curl_init() check above; both should be addressed.

Based on past review comments.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2cde55e and 40fadd2.

📒 Files selected for processing (32)
  • .vortex/tooling/CLAUDE.md (1 hunks)
  • .vortex/tooling/composer.json (1 hunks)
  • .vortex/tooling/phpcs.xml (1 hunks)
  • .vortex/tooling/plan.test-notify.md (1 hunks)
  • .vortex/tooling/scripts_conversion_plan.md (1 hunks)
  • .vortex/tooling/src/helpers.php (1 hunks)
  • .vortex/tooling/src/notify-email (1 hunks)
  • .vortex/tooling/src/notify-github (1 hunks)
  • .vortex/tooling/src/notify-jira (1 hunks)
  • .vortex/tooling/src/notify-newrelic (1 hunks)
  • .vortex/tooling/src/notify-slack (1 hunks)
  • .vortex/tooling/src/notify-webhook (1 hunks)
  • .vortex/tooling/tests/Exceptions/QuitErrorException.php (1 hunks)
  • .vortex/tooling/tests/Exceptions/QuitSuccessException.php (1 hunks)
  • .vortex/tooling/tests/Self/CheckNoExitSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockPassthruSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockQuitSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/CommandExistsTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/DotenvTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/FormatterTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/GetenvTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyEmailTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyGithubTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyJiraTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyNewrelicTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifySlackTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyWebhookTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/OverrideTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/RequestTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/TokenTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/UnitTestCase.php (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-09-13T04:14:41.765Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 2011
File: .vortex/tests/phpunit/Traits/Steps/StepAhoyTrait.php:20-23
Timestamp: 2025-09-13T04:14:41.765Z
Learning: In .vortex/tests/phpunit PHPUnit tests using cmd/cmdFail helpers, patterns in the expected output arrays support '*' markers for positive assertions (text must be present) and '!' markers for negative assertions (text must NOT be present). The '*' markers are NOT literal text matches but rather indicate expected presence.

Applied to files:

  • .vortex/tooling/tests/Unit/GetenvTest.php
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
  • .vortex/tooling/tests/Unit/TokenTest.php
  • .vortex/tooling/tests/Exceptions/QuitSuccessException.php
  • .vortex/tooling/tests/Self/MockQuitSelfTest.php
  • .vortex/tooling/tests/Unit/OverrideTest.php
  • .vortex/tooling/tests/Unit/DotenvTest.php
  • .vortex/tooling/phpcs.xml
  • .vortex/tooling/tests/Self/CheckNoExitSelfTest.php
  • .vortex/tooling/tests/Unit/FormatterTest.php
  • .vortex/tooling/tests/Self/MockPassthruSelfTest.php
  • .vortex/tooling/tests/Unit/UnitTestCase.php
  • .vortex/tooling/tests/Unit/CommandExistsTest.php
  • .vortex/tooling/tests/Unit/NotifyEmailTest.php
  • .vortex/tooling/tests/Unit/NotifyJiraTest.php
  • .vortex/tooling/CLAUDE.md
  • .vortex/tooling/tests/Exceptions/QuitErrorException.php
  • .vortex/tooling/tests/Unit/NotifySlackTest.php
📚 Learning: 2025-08-08T12:02:24.652Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1896
File: .vortex/tests/bats/unit/download-db-lagoon.bats:24-25
Timestamp: 2025-08-08T12:02:24.652Z
Learning: In .vortex/tests/bats/unit Bats tests using ../_helper.bash (run_steps), prefixing a STEPS entry with "- " denotes a negative assertion (the substring must NOT appear in output). Unprefixed entries are positive assertions. Example: "- Database dump refresh requested. Will create a new dump." asserts absence; "Database dump refresh requested. Will create a new dump." asserts presence.

Applied to files:

  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
📚 Learning: 2025-06-01T08:09:18.993Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1693
File: .vortex/installer/tests/Fixtures/install/hosting_acquia/docs/deployment.md:6-8
Timestamp: 2025-06-01T08:09:18.993Z
Learning: Do not review files in test fixture directories (e.g., files under paths containing `/tests/Fixtures/` or similar test fixture patterns). These files are used for testing purposes and should remain as-is.

Applied to files:

  • .vortex/tooling/phpcs.xml
  • .vortex/tooling/tests/Self/CheckNoExitSelfTest.php
📚 Learning: 2025-06-01T08:08:59.071Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1693
File: .vortex/installer/tests/Fixtures/install/deploy_type_none_gha/.github/workflows/build-test-deploy.yml:1-4
Timestamp: 2025-06-01T08:08:59.071Z
Learning: Test fixture files (typically located in directories containing "fixture" or "test") should not be reviewed for code quality issues as they may intentionally contain incomplete, invalid, or malformed data for testing purposes.

Applied to files:

  • .vortex/tooling/phpcs.xml
  • .vortex/tooling/tests/Unit/NotifyEmailTest.php
  • .vortex/tooling/tests/Unit/NotifySlackTest.php
📚 Learning: 2025-07-23T01:16:30.963Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1816
File: .docker/cli.dockerfile:72-76
Timestamp: 2025-07-23T01:16:30.963Z
Learning: In the Vortex project, unauthenticated Composer installs should be allowed, so GitHub token secrets should not be marked as `required=true` in Docker build mounts. The conditional check `if [ -s /run/secrets/github_token ]` is the preferred approach to allow builds to proceed without a token when needed.

Applied to files:

  • .vortex/tooling/tests/Unit/NotifyGithubTest.php
🧬 Code graph analysis (21)
.vortex/tooling/tests/Unit/GetenvTest.php (4)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • setUp (21-28)
.vortex/tooling/src/helpers.php (2)
  • getenv_default (119-136)
  • getenv_required (161-182)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/tests/Self/MockRequestSelfTest.php (3)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • runScript (38-92)
.vortex/tooling/tests/Traits/MockTrait.php (5)
  • mockRequestGet (405-418)
  • mockRequestPost (436-450)
  • mockRequest (373-389)
  • mockRequestAssertAllMocksConsumed (458-469)
  • mockRequestMultiple (229-359)
.vortex/tooling/src/helpers.php (3)
  • request_get (341-347)
  • request_post (369-376)
  • request (398-455)
.vortex/tooling/tests/Unit/TokenTest.php (1)
.vortex/tooling/src/helpers.php (1)
  • replace_tokens (296-312)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (2)
  • __construct (22-31)
  • getOutput (39-41)
.vortex/tooling/tests/Self/MockQuitSelfTest.php (5)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
  • QuitSuccessException (12-43)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • runScript (38-92)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/src/helpers.php (1)
  • quit (476-478)
.vortex/tooling/tests/Unit/RequestTest.php (1)
.vortex/tooling/src/helpers.php (3)
  • request_get (341-347)
  • request_post (369-376)
  • request (398-455)
.vortex/tooling/tests/Unit/OverrideTest.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/src/helpers.php (1)
  • execute_override (43-52)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockPassthru (161-163)
  • mockQuit (196-212)
.vortex/tooling/tests/Unit/DotenvTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • setUp (21-28)
.vortex/tooling/src/helpers.php (1)
  • load_dotenv (60-94)
.vortex/tooling/tests/Self/CheckNoExitSelfTest.php (1)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • setUp (21-28)
.vortex/tooling/tests/Unit/FormatterTest.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/src/helpers.php (2)
  • fail (260-263)
  • term_supports_color (268-270)
.vortex/tooling/tests/Self/MockPassthruSelfTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • runScript (38-92)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockPassthru (161-163)
  • mockPassthruAssertAllMocksConsumed (171-182)
.vortex/tooling/tests/Unit/UnitTestCase.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (2)
  • QuitErrorException (12-43)
  • getOutput (39-41)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (2)
  • QuitSuccessException (12-43)
  • getOutput (39-41)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockTearDown (63-81)
  • mockQuit (196-212)
.vortex/tooling/tests/Unit/CommandExistsTest.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/src/helpers.php (2)
  • command_exists (278-283)
  • fail (260-263)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/tests/Unit/NotifyEmailTest.php (1)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/tests/Unit/NotifyGithubTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (4)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockRequestPost (436-450)
  • mockRequestGet (405-418)
.vortex/tooling/tests/Unit/NotifyJiraTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (3)
  • mockRequestGet (405-418)
  • mockRequestPost (436-450)
  • mockRequest (373-389)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (2)
  • __construct (22-31)
  • getOutput (39-41)
.vortex/tooling/tests/Unit/NotifyNewrelicTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockRequestPost (436-450)
  • mockRequestGet (405-418)
.vortex/tooling/tests/Unit/NotifyWebhookTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockRequest (373-389)
.vortex/tooling/tests/Unit/NotifySlackTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (4)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockRequestPost (436-450)
.vortex/tooling/src/helpers.php (1)
.vortex/installer/src/Utils/Tui.php (3)
  • line (57-59)
  • output (38-43)
  • error (53-55)
🪛 Gitleaks (8.29.0)
.vortex/tooling/tests/Unit/NotifyGithubTest.php

[high] 25-25: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

.vortex/tooling/tests/Unit/NotifyNewrelicTest.php

[high] 26-26: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 49-49: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 74-74: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 92-92: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 101-101: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 119-119: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 145-145: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 167-167: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 184-184: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 201-201: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 LanguageTool
.vortex/tooling/CLAUDE.md

[grammar] ~230-~230: Ensure spelling is correct
Context: ...ssertAllMocksConsumed(): void #### Passthru Mocking **Response Structure**:php...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

.vortex/tooling/plan.test-notify.md

[style] ~400-~400: To elevate your writing, try using a synonym here.
Context: ...t count per script - Any lines that are hard to cover (with explanation) - Overall c...

(HARD_TO)

🪛 markdownlint-cli2 (0.18.1)
.vortex/tooling/scripts_conversion_plan.md

104-104: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


155-155: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


179-179: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


190-190: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


200-200: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


211-211: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


222-222: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


233-233: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


333-333: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


338-338: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

.vortex/tooling/CLAUDE.md

104-104: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


155-155: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


179-179: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


190-190: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


200-200: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


211-211: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


222-222: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


233-233: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


333-333: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


338-338: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: build (1)
  • GitHub Check: build (0)
  • GitHub Check: vortex-test-workflow (1)
  • GitHub Check: vortex-test-workflow (2)
  • GitHub Check: vortex-test-workflow (4)
  • GitHub Check: vortex-test-workflow (3)
  • GitHub Check: vortex-test-workflow (0)
  • GitHub Check: vortex-test-common
  • GitHub Check: vortex-test-installer (8.3)
  • GitHub Check: vortex-test-installer (8.4)
  • GitHub Check: vortex-test-docs

Comment on lines +75 to +99
/**
* Convert a multiline string into a JIRA ADF paragraph node.
*
* @param string $text
* Multiline text to convert.
*
* @return array
* ADF paragraph node.
*/
function text_to_adf(string $text): array {
$text = str_replace(["\r\n", "\r"], "\n", $text);
$lines = explode("\n", $text);

$content = [];
foreach ($lines as $index => $line) {
if ($index > 0) {
$content[] = ['type' => 'hardBreak'];
}
if ($line !== '') {
$content[] = ['type' => 'text', 'text' => $line];
}
}

return ['type' => 'paragraph', 'content' => $content];
}
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

Fix ADF doc content shape: wrap paragraph node in an array

text_to_adf() returns a single paragraph node, but comment_body['content'] is set to that node directly, whereas the ADF doc schema expects content to be an array of top‑level nodes. This can break Jira’s validation of the comment payload.

Consider wrapping the paragraph node:

- $jira_message_adf = text_to_adf($jira_message);
-
- $comment_body = [
-   'type' => 'doc',
-   'version' => 1,
-   'content' => $jira_message_adf,
- ];
+ $jira_message_adf = text_to_adf($jira_message);
+
+ $comment_body = [
+   'type' => 'doc',
+   'version' => 1,
+   'content' => [
+     $jira_message_adf,
+   ],
+ ];

Please double‑check against the latest Jira ADF documentation and adjust any existing tests (e.g. NotifyJiraTest) that assert on the payload structure.

Also applies to: 153-159


I'll verify this review comment by checking the Jira ADF specification and examining the actual code and tests.

Now let me examine the actual code and tests in the repository:

Now let me examine the actual code in the repository:

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Find and examine the notify-jira file
find . -name "*notify-jira*" -o -name "*notify_jira*" 2>/dev/null | head -20

Length of output: 108


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Examine the notify-jira file with line numbers, focusing on the target lines
cat -n ./.vortex/tooling/src/notify-jira | sed -n '70,105p'

Length of output: 1246


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Examine the notify-jira file around lines 140-160 for comment_body construction
cat -n ./.vortex/tooling/src/notify-jira | sed -n '135,165p'

Length of output: 971


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Search for other instances of text_to_adf usage or similar comment_body patterns
rg -n "text_to_adf|comment_body" ./.vortex/tooling/src/notify-jira

Length of output: 167


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Check full file length and search for other 'doc' type structures or transitions/assignee handling
wc -l ./.vortex/tooling/src/notify-jira
echo "---"
rg -n "'type'\s*=>\s*'doc'|'type'\s*=>\s*'paragraph'" ./.vortex/tooling/src/notify-jira

Length of output: 124


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Examine the test file for payload structure assertions
cat ./.vortex/tests/bats/unit/notify-jira.bats

Length of output: 5485


Wrap ADF doc content in array: single paragraph node violates schema

The review comment is correct. ADF document structure requires content to be an array of block-level nodes.

Verification confirms the issue:

  • text_to_adf() (line 84) returns a single paragraph node: ['type' => 'paragraph', 'content' => ...]
  • comment_body (lines 155–159) assigns this node directly: 'content' => $jira_message_adf
  • This violates ADF schema; JIRA's API expects content as an array of nodes

The suggested fix is correct—wrap the paragraph in an array:

- $comment_body = [
-   'type' => 'doc',
-   'version' => 1,
-   'content' => $jira_message_adf,
- ];
+ $comment_body = [
+   'type' => 'doc',
+   'version' => 1,
+   'content' => [$jira_message_adf],
+ ];

This is the only location in the file where this pattern occurs. The test file (notify-jira.bats) does not validate payload structure, so the issue would not be caught by existing tests. After applying the fix, verify with JIRA's API documentation or by testing the comment posting flow.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In .vortex/tooling/src/notify-jira around lines 75–99, text_to_adf() currently
returns a single paragraph node (['type'=>'paragraph','content'=>...]) which
causes the top-level ADF `content` to be a node instead of an array of block
nodes; change text_to_adf to return an array of block nodes (i.e., wrap the
paragraph node in an array) or, alternatively, wrap the paragraph node in an
array where it is assigned to comment_body so that the final ADF payload uses
'content' => [<paragraph node>], and adjust any callers if necessary.

Comment on lines +161 to +170
note('JIRA notification summary:');
note('Project : ' . $notify_project);
note('Deployment : ' . $notify_label);
note('Issue : ' . $jira_issue);
note('Environment URL: ' . $notify_env_url);
note('Login URL : ' . $notify_login_url);
note('Endpoint : ' . $jira_endpoint);
note('User email : ' . $jira_user_email);
note('Transition : ' . ($jira_transition ?: '<none>'));
note('Assignee email : ' . ($jira_assignee_email ?: '<none>'));
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

Reconsider logging JIRA user email in standard output

note('User email : ' . $jira_user_email); logs a concrete user identifier to whatever sink consumes these logs (CI, shared logs, etc.). Depending on your privacy/compliance posture, this may be considered sensitive PII.

If that’s a concern in your environment, consider removing the email from the summary or guarding it behind a verbose/debug flag.

🤖 Prompt for AI Agents
.vortex/tooling/src/notify-jira around lines 161 to 170: the summary currently
prints the JIRA user email which may expose PII; either remove the note('User
email     : ' . $jira_user_email); line entirely or wrap it in a verbosity/debug
check (e.g., only log when a VERBOSE/DEBUG flag is set) or mask the email (show
only domain or initials) before logging so that sensitive user identifiers are
not emitted to standard output by default.

Comment on lines +191 to +228
if (!empty($jira_transition)) {
task('Discovering transition ID for %s', $jira_transition);
$url = sprintf('%s/rest/api/3/issue/%s/transitions', $jira_endpoint, $jira_issue);
$response = request_get($url, [
'Authorization: Basic ' . $token,
'Content-Type: application/json',
]);

$transition_id = NULL;
if ($response['ok'] && $response['body']) {
$payload = json_decode((string) $response['body'], TRUE);
if (isset($payload['transitions']) && is_array($payload['transitions'])) {
foreach ($payload['transitions'] as $t) {
if (isset($t['name']) && $t['name'] === $jira_transition) {
$transition_id = $t['id'] ?? NULL;
break;
}
}
}
}

if (!$transition_id || !ctype_digit((string) $transition_id)) {
fail('Unable to retrieve transition ID');
}

pass('Found transition ID %s', $transition_id);

task('Transitioning issue to ' . $jira_transition);

$payload_data = ['transition' => ['id' => $transition_id]];
$url = sprintf('%s/rest/api/3/issue/%s/transitions', $jira_endpoint, $jira_issue);
request_post($url, json_encode($payload_data), [
'Authorization: Basic ' . $token,
'Content-Type: application/json',
]);

pass('Transitioned issue to %s ', $jira_transition);
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Handle failures from transition and assignee update requests

Both the transition request (request_post at lines 220‑225) and the assignee update (request at lines 259‑266) ignore HTTP responses. If Jira returns a 4xx/5xx or malformed body, the script will still report success, which makes troubleshooting hard.

You could, for example:

  • Capture and inspect the responses.
  • Fail with a clear message if $response['ok'] is false or the body is missing/invalid.

This would align error handling for these steps with the stricter checks used for authentication and comment creation earlier in the script.

Also applies to: 230-269

Comment on lines +9 to +13
/**
* Self-tests for check-exit-usage.php script.
*
* These tests verify that the exit() usage checker works correctly.
*/
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

Align class docblock with actual script name

The docblock still references check-exit-usage.php while $this->scriptPath targets check-no-exit.php. Update the description to avoid confusion when tracking which tool these self-tests validate.

🤖 Prompt for AI Agents
.vortex/tooling/tests/Self/CheckNoExitSelfTest.php lines 9-13: the class
docblock mentions check-exit-usage.php and "exit() usage checker" while the test
actually targets check-no-exit.php via $this->scriptPath; update the docblock
text to reference check-no-exit.php (and adjust the short description to say it
verifies the check-no-exit script) so the comment accurately reflects which tool
the self-tests validate.

Comment on lines +34 to +50
public function testSuccessfulNotificationPostDeployment(): void {
$this->mockRequestPost(
'https://hooks.slack.com/services/T00/B00/XXXX',
$this->callback(function ($body): true {
/** @var array {username: string, icon_emoji: string, attachments: array} $payload */
$payload = json_decode($body, TRUE);
// Verify payload structure.
$this->assertEquals('Deployment Bot', $payload['username']);
$this->assertEquals(':rocket:', $payload['icon_emoji']);
$this->assertEquals('good', $payload['attachments'][0]['color']);
$this->assertStringContainsString('Deployment Complete', $payload['attachments'][0]['title']);
$this->assertEquals('test-project', $this->getFieldValue($payload, 'Deployment'));
return TRUE;
}),
['Content-Type: application/json'],
10,
['status' => 200, 'body' => 'ok']
);
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

Ensure mockRequestPost() actually uses the $body callback so payload assertions run

In this and other tests you pass a $this->callback(...) as the second argument to mockRequestPost() and assert heavily on the decoded JSON payload. However, the current MockTrait::mockRequestPost() implementation only forwards url, method and response to mockRequestMultiple:

protected function mockRequestPost(
    string $url,
    $body = NULL,
    array $headers = [],
    int $timeout = 10,
    ?array $response = NULL,
    string $namespace = 'DrevOps\\VortexTooling',
  ): void {
  $this->mockRequestMultiple([[
    'url' => $url,
    'method' => 'POST',
    'response' => $response ?? [],
  ],
  ], $namespace);
}

As a result, the $body callback you pass here is never consumed, and these tests do not currently verify the outbound Slack payload.

Consider updating the mocking layer so mockRequestPost() (and, if needed, mockRequestMultiple()) propagates and uses the body/headers/timeout information, e.g. by including them in the request definition passed into mockRequestMultiple and having the underlying stub invoke the callback on the actual body. That way, regressions in the payload shape will be caught by these tests as intended.

🤖 Prompt for AI Agents
In .vortex/tooling/tests/Unit/NotifySlackTest.php around lines 34 to 51 the test
passes a $this->callback(...) as the body argument to mockRequestPost() but the
MockTrait::mockRequestPost() currently ignores body/headers/timeout and only
sends url/method/response to mockRequestMultiple, so the assertions never run;
update mockRequestPost() to include the provided $body, $headers and $timeout in
the request array passed to mockRequestMultiple (e.g. add keys 'body',
'headers', 'timeout'), and then update mockRequestMultiple() (or its underlying
stub) to detect when a 'body' entry is a PHPUnit callback and invoke it with the
actual request payload string (and otherwise store/pass the body), and ensure
headers and timeout are forwarded to the stub so tests receive and can assert
against the real outbound payload.

@AlexSkrypnyk AlexSkrypnyk force-pushed the feature/1192-tooling-package branch from 40fadd2 to 4836323 Compare November 23, 2025 05:41
@github-actions github-actions bot temporarily deployed to commit November 23, 2025 05:43 Inactive
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: 8

♻️ Duplicate comments (38)
.vortex/tooling/src/notify-slack (1)

128-134: Optionally validate Slack response body content

The current check only verifies HTTP 200 status. Slack webhooks can return 200 with error strings in the body (e.g., invalid_payload, no_service). Consider parsing and validating the response body matches the expected success indicator (typically "ok") for stricter error detection during misconfiguration or invalid payloads.

.vortex/tooling/scripts_conversion_plan.md (1)

84-166: Update helper function names to match actual implementation

The plan references load_env(), execute_override_if_exists(), validate_variable(), and validate_command(), but the actual helpers.php implementation uses load_dotenv(), execute_override(), getenv_required(), and command_exists(). Update the examples throughout the document to use the correct function names.

Also applies to: 196-261, 408-417

.vortex/tooling/phpcs.xml (2)

21-30: Simplify file specification by removing redundant entries

Lines 22-28 individually list src/notify-* subdirectories even though line 21 already includes src. Since PHPCS scans directories recursively by default, these explicit entries are redundant and could be removed for cleaner configuration.


37-56: Fix exclude-pattern glob syntax

The patterns use backslash-escaped dots (e.g., *.Test\.php) but PHPCS treats exclude-patterns as shell-style globs, not regex. The backslashes are unnecessary and may not match correctly. Use *Test.php and *TestCase.php instead, or use directory-aware patterns like **/*Test.php to match subdirectories.

.vortex/tooling/src/notify-email (4)

136-161: CRITICAL: Email header injection vulnerability

Lines 136-143 and 148-154 construct email headers using user-controlled values ($email_from, $email_subject, $name, $email) without sanitizing newline characters. An attacker could inject arbitrary headers (Bcc, Cc, additional recipients) by including \n or \r\n in these environment variables.

Required fix: Strip or reject CR/LF characters from all header values:

// Add before line 128:
function sanitize_email_header(string $value): string {
  return str_replace(["\r", "\n", "\0"], '', $value);
}

// Then sanitize before use:
$email_from = sanitize_email_header($email_from);
$email_subject = sanitize_email_header($email_subject);
// In the loop:
$email = sanitize_email_header($email);
$name = sanitize_email_header($name);

100-114: Reuse timestamp for consistency

The subject and message each call date() separately, which can result in timestamps differing by a second. Compute the timestamp once before both token replacements to ensure they're identical.


128-128: Filter empty recipient entries

Extra commas or whitespace in VORTEX_NOTIFY_EMAIL_RECIPIENTS can produce empty strings after explode/trim, leading to blank To: headers and unnecessary mail command invocations. Add array_filter to remove empty entries after trimming.


148-160: Log failed email deliveries

Only successful sends are tracked in $sent. Failed deliveries are silent, making debugging difficult. Consider capturing and logging which recipients failed with the command exit code and output for troubleshooting.

.vortex/tooling/src/notify-jira (3)

155-159: CRITICAL: ADF content must be an array of nodes

Line 158 assigns $jira_message_adf (a single paragraph node) directly to content, but the ADF document schema requires content to be an array of block-level nodes. This violates Jira's ADF specification and may cause API validation errors.

Fix: Wrap the paragraph node in an array:

 $comment_body = [
   'type' => 'doc',
   'version' => 1,
-  'content' => $jira_message_adf,
+  'content' => [$jira_message_adf],
 ];

168-168: Consider PII implications of logging user email

Logging $jira_user_email to standard output may expose personally identifiable information in CI logs or shared environments. Consider removing this log line, masking the email (e.g., showing only the domain), or guarding it behind a debug/verbose flag if compliance is a concern.


218-228: Add error handling for transition API request

The transition request at lines 222-225 doesn't validate the response. If Jira returns an error (4xx/5xx), the script reports success anyway. Consider capturing the response and failing with a clear message if $response['ok'] is false.

Also applies to: 255-269

.vortex/tooling/tests/Self/CheckNoExitSelfTest.php (4)

9-13: Update class docblock to reference correct script name.

The docblock mentions "check-exit-usage.php" but the test actually targets "check-no-exit.php" (line 31). Update the description to avoid confusion.


29-36: Use unique temporary directories per test to prevent failures.

The setUp() creates a fixed $testDir path, causing two critical issues:

  1. mkdir() warnings fail tests: Subsequent tests attempt to mkdir existing directories, triggering PHP warnings that fail tests (given phpunit.xml's failOnWarning="true").
  2. File persistence breaks isolation: Earlier tests' PHP files persist in the shared directory, making testHandlesEmptyDirectories() find unexpected files and become order-dependent.

Apply this fix:

  protected function setUp(): void {
    parent::setUp();
    $this->scriptPath = dirname(__DIR__, 2) . '/check-no-exit.php';
-   $this->testDir = self::$tmp . '/check-exit-test';
-   mkdir($this->testDir);
-   mkdir($this->testDir . '/src');
-   mkdir($this->testDir . '/tests');
+   $this->testDir = self::$tmp . '/check-exit-test-' . uniqid('', true);
+   mkdir($this->testDir . '/src', 0777, true);
+   mkdir($this->testDir . '/tests', 0777, true);
  }

This creates an isolated directory per test and uses recursive mkdir to avoid warnings.


47-50: Extract and escape shell command execution to a helper method.

The inline exec() calls are duplicated across six tests and don't escape $this->testDir or $this->scriptPath, which could break with paths containing spaces or special characters.

Add a helper method:

/**
 * Run check-no-exit.php from the current test directory.
 *
 * @return array{0: string[], 1: int}
 */
private function runCheckNoExitScript(): array {
  $output = [];
  $exit_code = 0;
  
  $cmd = sprintf(
    'cd %s && php %s 2>&1',
    escapeshellarg($this->testDir),
    escapeshellarg($this->scriptPath),
  );
  exec($cmd, $output, $exit_code);
  
  return [$output, $exit_code];
}

Then replace each exec() block with:

[$output, $exit_code] = $this->runCheckNoExitScript();

Also applies to: 65-68, 82-84, 103-105, 120-122, 133-135


113-126: Assert exit code in line-number test for consistency.

testReportsCorrectLineNumbers() only verifies the reported filename:line but doesn't assert that the script exits with a non-zero code. If the script ever started logging correctly but mistakenly exited 0, this test would still pass.

Add the assertion:

  $output_str = implode("\n", $output);
+ $this->assertNotEquals(0, $exit_code, 'Script should exit with non-zero code when exit() is detected');
  $this->assertStringContainsString('line-test.php:5', $output_str);
.vortex/tooling/src/helpers.php (4)

43-52: Add path traversal protection to execute_override.

The $name parameter is concatenated directly into a file path without validation. While current usage passes basename(__FILE__), future callers might not, creating a path traversal vulnerability.

Add validation before path construction:

function execute_override(string $name): void {
+ // Prevent path traversal by ensuring name doesn't contain separators.
+ if (str_contains($name, DIRECTORY_SEPARATOR) || str_contains($name, '/') || str_contains($name, '\\')) {
+   fail('Invalid script name for override: %s', $name);
+ }
+
  $dir = getenv('VORTEX_TOOLING_CUSTOM_DIR');
  if ($dir) {
    $path = $dir . DIRECTORY_SEPARATOR . $name;

278-283: Shell-escape the command argument to prevent injection.

command_exists() interpolates $command directly into a shell string. If $command ever comes from untrusted input, this becomes a command-injection vector.

Apply this fix:

function command_exists(string $command): void {
- exec(sprintf('command -v %s 2>/dev/null', $command), $output, $code);
+ exec(sprintf('command -v %s 2>/dev/null', escapeshellarg($command)), $output, $code);
  if ($code !== 0) {
    fail(sprintf("Command '%s' is not available", $command));
  }
}

404-404: Check curl_init return value before using the handle.

curl_init() can return false on failure. Passing false to curl_setopt_array() will cause a TypeError.

Add error handling:

  $ch = curl_init($url);
+ if ($ch === FALSE) {
+   fail('Failed to initialize curl for URL: %s', $url);
+ }

  try {

426-426: Add error handling for curl_setopt_array failure.

While the missing curl_init check is already flagged, there's also no verification that curl_setopt_array() succeeded. If setting options fails, the request proceeds with incorrect configuration.

Add a check after the call:

  curl_setopt_array($ch, $opts);
+ if (curl_errno($ch)) {
+   fail('Failed to set curl options: %s', curl_error($ch));
+ }

Note: This is separate from the curl_init check and should also be addressed.

.vortex/tooling/tests/Unit/TokenTest.php (1)

32-55: Consider adding test cases for JSON-escaping behavior.

Since replace_tokens() escapes values via json_encode() before substitution, it would be useful to add data-provider scenarios where replacement values contain quotes, backslashes, or newlines to verify that escaping behavior. For example:

'value with quotes' => [
  'template' => 'Message: %msg%',
  'replacements' => ['msg' => 'He said "hello"'],
  'expected' => 'Message: He said \"hello\"',
],
'value with newlines' => [
  'template' => 'Text: %text%',
  'replacements' => ['text' => "Line1\nLine2"],
  'expected' => 'Text: Line1\nLine2',
],

This would make regressions in the escaping logic easier to catch.

.vortex/tooling/src/notify (1)

66-66: Use DIRECTORY_SEPARATOR for cross-platform compatibility.

The script path is constructed with a hardcoded forward slash, which may not work correctly on Windows systems.

Apply this fix:

- $script = __DIR__ . ('/notify-' . $channel);
+ $script = __DIR__ . DIRECTORY_SEPARATOR . 'notify-' . $channel;
.vortex/tooling/tests/Unit/NotifyGithubTest.php (1)

23-31: Use a clearly fake token value to avoid Gitleaks detection.

The hard-coded test token 'ghp_test123456' matches GitHub's personal access token prefix and is flagged by Gitleaks as a potential secret. Even though this is obviously a test value, it still fails secret scanning.

Change it to a format that doesn't resemble a real token:

  $this->envSetMultiple([
-   'VORTEX_NOTIFY_GITHUB_TOKEN' => 'ghp_test123456',
+   'VORTEX_NOTIFY_GITHUB_TOKEN' => 'test-token-value',
    'VORTEX_NOTIFY_GITHUB_REPOSITORY' => 'owner/repo',

Alternatives include 'TEST_TOKEN', 'fake-github-token', or concatenating strings like 'ghp_' . 'TEST_' . 'TOKEN' to break the pattern.

.vortex/tooling/tests/Unit/GetenvTest.php (1)

137-146: Fix unreachable output assertions in error tests.

These tests use captureOutput() together with expectException(), but captureOutput() rethrows exceptions (line 187), so $output is never assigned and the subsequent assertStringContainsString(..., $output) lines are unreachable. The tests therefore don't actually verify the error message text.

Refactor to manually catch the exception:

public function testGetenvRequiredWithNoVarsSetFails(): void {
  $this->mockQuit(1);
  
  try {
    $output = $this->captureOutput(function (): void {
      \DrevOps\VortexTooling\getenv_required('UNSET_VAR_1', 'UNSET_VAR_2');
    });
    $this->fail('Expected QuitErrorException was not thrown');
  }
  catch (QuitErrorException $e) {
    $output = $e->getOutput();
    $this->assertStringContainsString('Missing required value for UNSET_VAR_1, UNSET_VAR_2', $output);
  }
}

Apply similar changes to testGetenvRequiredWithAllEmptyStringsFails() and testGetenvRequiredWithSingleVarNotSetFails().

Also applies to: 148-160, 162-171

.vortex/tooling/src/notify-webhook (1)

16-17: Remove blank lines after separator comments to fix PHPCS warnings.

The pipeline flags separator comments for having blank lines immediately after them. Remove these blank lines to satisfy Drupal.Commenting.InlineComment.SpacingAfter:

// -----------------------------------------------------------------------------
-
// Webhook notification project name.
// -----------------------------------------------------------------------------
-
info('Started Webhook notification.');

Based on learnings, this coding standard is enforced by the project's PHPCS configuration.

Also applies to: 63-64

.vortex/tooling/CLAUDE.md (1)

104-104: Address markdown linting violations to keep CI green.

The markdownlint tool flags several formatting issues that are straightforward to fix:

  • Fenced code blocks need blank lines before and after them (lines 104, 155, 333, 338)
  • Headings need blank lines before and after them (lines 179, 190, 200, 211, 222, 233)

Also applies to: 155-155, 179-179, 190-190, 200-200, 211-211, 222-222, 233-233, 333-333, 338-338

.vortex/tooling/src/notify-github (2)

56-66: Consider removing the upper length bound for future-proofing.

The current validation enforces a maximum of 11 digits for deployment IDs. While this matches current GitHub IDs, it may reject future larger IDs if GitHub changes their format.

As noted in a previous review, keeping the numeric check (ctype_digit()) but removing the upper bound would make the validation more resilient.

Apply this diff to relax the constraint:

 function deployment_id_is_valid(mixed $deployment_id): bool {
   if (empty($deployment_id)) {
     return FALSE;
   }
 
   $id_str = (string) $deployment_id;
   $length = strlen($id_str);
 
-  // Check length (9-11 digits) and numeric format.
-  return $length >= 9 && $length <= 11 && ctype_digit($id_str);
+  // Check minimum length (9 digits) and numeric format.
+  return $length >= 9 && ctype_digit($id_str);
 }

84-88: Add explicit Content-Type header for JSON POST requests.

The deployment creation sends a JSON body but doesn't specify Content-Type: application/json. While cURL may infer this, being explicit prevents potential issues with the GitHub API.

The same issue appears in the status update POST at lines 146-150.

Apply this diff:

   $response = request_post($url, json_encode($payload_data), [
     'Authorization: token ' . $github_token,
     'Accept: application/vnd.github.v3+json',
+    'Content-Type: application/json',
   ]);
.vortex/tooling/tests/Unit/RequestTest.php (1)

30-100: Network-dependent tests remain in unit test suite.

These tests still make real HTTP calls to example.com, which makes them fragile and environment-dependent. While marked as addressed in a past review (commit ef08bb3), the actual network calls remain in the code.

Consider either:

  • Moving these to a separate integration test group that can be skipped in CI
  • Replacing with mocked tests using MockTrait as suggested previously
  • Keeping minimal integration checks but ensuring the default unit run doesn't require network access

The tests rely on:

  • Network availability
  • DNS resolution
  • example.com's response behavior (status codes, timing)

This can cause non-deterministic failures in restricted environments.

.vortex/tooling/tests/Unit/OverrideTest.php (4)

26-32: Replace direct putenv() with EnvTrait helper.

Direct putenv() bypasses EnvTrait's automatic cleanup in tearDown(), potentially affecting other tests.

Apply this diff:

   public function testExecuteOverrideNoCustomDir(): void {
-    putenv('VORTEX_TOOLING_CUSTOM_DIR');
+    $this->envUnset('VORTEX_TOOLING_CUSTOM_DIR');
 
     $this->expectNotToPerformAssertions();

Based on learnings


34-42: Replace direct putenv() with EnvTrait helper.

Use $this->envSet() for consistent environment management and automatic cleanup.

Apply this diff:

   public function testExecuteOverrideCustomDirNoFile(): void {
     $custom_dir = self::$tmp . '/custom';
     mkdir($custom_dir, 0777, TRUE);
-    putenv('VORTEX_TOOLING_CUSTOM_DIR=' . $custom_dir);
+    $this->envSet('VORTEX_TOOLING_CUSTOM_DIR', $custom_dir);

Based on learnings


44-55: Replace direct putenv() with EnvTrait helper.

Use $this->envSet() instead of putenv() for proper test isolation.

Apply this diff:

     chmod($script_path, 0644);
-    putenv('VORTEX_TOOLING_CUSTOM_DIR=' . $custom_dir);
+    $this->envSet('VORTEX_TOOLING_CUSTOM_DIR', $custom_dir);

Based on learnings


57-84: Replace direct putenv() with EnvTrait helper.

Use $this->envSet() for environment variable management.

Apply this diff:

     chmod($script_path, 0755);
-    putenv('VORTEX_TOOLING_CUSTOM_DIR=' . $custom_dir);
+    $this->envSet('VORTEX_TOOLING_CUSTOM_DIR', $custom_dir);

Based on learnings

.vortex/tooling/tests/Unit/FormatterTest.php (3)

33-111: Replace direct putenv() with EnvTrait methods.

Multiple test methods use putenv() directly to set or unset the TERM environment variable (lines 35, 64, 103, 106). This bypasses EnvTrait's automatic cleanup and can affect other tests.

Apply this diff:

   #[DataProvider('dataProviderOutputFormatters')]
   public function testOutputFormatters(string $function, string $expected_prefix): void {
-    putenv('TERM=dumb');
+    $this->envSet('TERM', 'dumb');
 
     ob_start();
@@
   #[DataProvider('dataProviderOutputFormattersWithColor')]
   public function testOutputFormattersWithColor(string $function, string $expected_prefix): void {
-    putenv('TERM=xterm-256color');
+    $this->envSet('TERM', 'xterm-256color');
 
     ob_start();
@@
   #[DataProvider('dataProviderTermSupportsColor')]
   public function testTermSupportsColor(string|bool $term_value, bool $expected): void {
     if ($term_value === FALSE) {
-      putenv('TERM');
+      $this->envUnset('TERM');
     }
     else {
-      putenv('TERM=' . $term_value);
+      $this->envSet('TERM', $term_value);
     }

Based on learnings


77-82: Add missing fail_no_exit test case for colored output.

The dataProviderOutputFormattersWithColor is missing coverage for fail_no_exit, which supports color output. This results in incomplete test coverage.

Apply this diff:

   public static function dataProviderOutputFormattersWithColor(): array {
     return [
       'task' => ['task', '[TASK]'],
       'info' => ['info', '[INFO]'],
       'pass' => ['pass', '[ OK ]'],
+      'fail_no_exit' => ['fail_no_exit', '[FAIL]'],
     ];
   }

113-117: Add positive test case for color support detection.

The data provider only tests scenarios where color support should return FALSE. Add a test case to verify that term_supports_color() returns TRUE for color-capable terminals.

Apply this diff:

   public static function dataProviderTermSupportsColor(): array {
     return [
       'dumb terminal' => ['term_value' => 'dumb', 'expected' => FALSE],
       'no terminal' => ['term_value' => FALSE, 'expected' => FALSE],
+      'color terminal' => ['term_value' => 'xterm-256color', 'expected' => TRUE],
     ];
   }
.vortex/tooling/tests/Self/MockQuitSelfTest.php (1)

61-107: Tests should use runScriptEarlyPass/runScriptError to validate both exit behavior and output

The past review correctly identified that assertions on $output never execute when expectException() is used with runScript(). Once the mocked quit() throws the expected exception, PHPUnit stops test execution—assignments and assertions after the call are unreachable.

Although this issue was marked as addressed in commit 2cde55e, the current code still follows the broken pattern. Use the helper methods provided in UnitTestCase:

  • For testMockQuitScript0Success and similar success cases: $this->runScriptEarlyPass('tests/Fixtures/test-quit-passing', 'Script will exit with code 0');
  • For testMockQuitScript0Failure and similar failure cases: wrap in a try-catch expecting RuntimeException, or refactor to match the working pattern in NotifyEmailTest.php.

Apply this pattern (example for lines 61–71):

  public function testMockQuitScript0Success(): void {
    $this->mockQuit();

-    $this->expectException(QuitSuccessException::class);
-    $this->expectExceptionCode(0);
-
-    $output = $this->runScript('tests/Fixtures/test-quit-passing');
-
-    $this->assertStringContainsString('Script will exit with code 0', $output);
-    $this->assertStringNotContainsString('ERROR Script continued after quit()', $output);
+    $this->runScriptEarlyPass(
+      'tests/Fixtures/test-quit-passing',
+      'Script will exit with code 0'
+    );
  }

Repeat for the other three script-based tests (lines 73–83, 85–95, 97–107).

.vortex/tooling/tests/Self/MockRequestSelfTest.php (1)

21-37: Move helper loading to setUp() to ensure test isolation

The past review correctly identified that helpers.php is only loaded in testMockRequestGetSuccess(). All subsequent tests depend on this side-effect, which breaks test isolation. Running any later test individually (or with randomized order) will fail with "undefined function" errors.

Move the require to a shared setup method:

+  protected function setUp(): void {
+    parent::setUp();
+    require_once __DIR__ . '/../../src/helpers.php';
+  }
+
  public function testMockRequestGetSuccess(): void {
    $this->mockRequestGet(
      'https://example.com/api',
      [],
      10,
      ['status' => 200, 'body' => 'success response']
    );

-    // Load helpers.php AFTER setting up mocks.
-    require_once __DIR__ . '/../../src/helpers.php';
-
    $result = \DrevOps\VortexTooling\request_get('https://example.com/api');

Note: The past review also identified PHPCS violations for fully-qualified names throughout this file and provided a complete patch. That issue remains unaddressed.

.vortex/tooling/tests/Unit/NotifyJiraTest.php (1)

200-331: Error‑path tests are structured well but inherit the runScriptError() syntax issue

The various failure tests (testPreDeploymentEventSkipped, testAuthenticationFailure, testCommentCreationFailure, testTransitionDiscoveryFailure, testAssigneeDiscoveryFailure, testMissingRequiredVariables, and the data‑provider–driven failure tests at Lines 200–331, 364–378, 578–599, 601–633, 635–680, 682–727) consistently use runScriptEarlyPass() / runScriptError() with expected output substrings, which is a good pattern.

However, as noted from the UnitTestCase::runScriptError() snippet, there is a syntax error in the assertion (the $output) that would prevent any of these tests from running. Once that helper is fixed as suggested earlier, this suite’s error‑path coverage should behave as designed.

Also applies to: 364-378, 578-599, 601-633, 635-680, 682-727

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 40fadd2 and 4836323.

📒 Files selected for processing (34)
  • .vortex/tooling/CLAUDE.md (1 hunks)
  • .vortex/tooling/composer.json (1 hunks)
  • .vortex/tooling/phpcs.xml (1 hunks)
  • .vortex/tooling/plan.test-notify.md (1 hunks)
  • .vortex/tooling/scripts_conversion_plan.md (1 hunks)
  • .vortex/tooling/src/helpers.php (1 hunks)
  • .vortex/tooling/src/notify (1 hunks)
  • .vortex/tooling/src/notify-email (1 hunks)
  • .vortex/tooling/src/notify-github (1 hunks)
  • .vortex/tooling/src/notify-jira (1 hunks)
  • .vortex/tooling/src/notify-newrelic (1 hunks)
  • .vortex/tooling/src/notify-slack (1 hunks)
  • .vortex/tooling/src/notify-webhook (1 hunks)
  • .vortex/tooling/tests/Exceptions/QuitErrorException.php (1 hunks)
  • .vortex/tooling/tests/Exceptions/QuitSuccessException.php (1 hunks)
  • .vortex/tooling/tests/Self/CheckNoExitSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockPassthruSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockQuitSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/CommandExistsTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/DotenvTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/FormatterTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/GetenvTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyEmailTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyGithubTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyJiraTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyNewrelicTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyRouterTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifySlackTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyWebhookTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/OverrideTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/RequestTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/TokenTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/UnitTestCase.php (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-09-13T04:14:41.765Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 2011
File: .vortex/tests/phpunit/Traits/Steps/StepAhoyTrait.php:20-23
Timestamp: 2025-09-13T04:14:41.765Z
Learning: In .vortex/tests/phpunit PHPUnit tests using cmd/cmdFail helpers, patterns in the expected output arrays support '*' markers for positive assertions (text must be present) and '!' markers for negative assertions (text must NOT be present). The '*' markers are NOT literal text matches but rather indicate expected presence.

Applied to files:

  • .vortex/tooling/phpcs.xml
  • .vortex/tooling/tests/Unit/OverrideTest.php
  • .vortex/tooling/tests/Self/MockQuitSelfTest.php
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
  • .vortex/tooling/tests/Exceptions/QuitSuccessException.php
  • .vortex/tooling/tests/Unit/DotenvTest.php
  • .vortex/tooling/tests/Unit/NotifySlackTest.php
  • .vortex/tooling/tests/Unit/CommandExistsTest.php
  • .vortex/tooling/tests/Unit/UnitTestCase.php
  • .vortex/tooling/tests/Unit/FormatterTest.php
  • .vortex/tooling/tests/Self/CheckNoExitSelfTest.php
  • .vortex/tooling/tests/Unit/GetenvTest.php
  • .vortex/tooling/CLAUDE.md
  • .vortex/tooling/tests/Unit/NotifyEmailTest.php
  • .vortex/tooling/tests/Exceptions/QuitErrorException.php
  • .vortex/tooling/tests/Self/MockPassthruSelfTest.php
  • .vortex/tooling/tests/Unit/TokenTest.php
  • .vortex/tooling/tests/Unit/NotifyJiraTest.php
📚 Learning: 2025-06-01T08:09:18.993Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1693
File: .vortex/installer/tests/Fixtures/install/hosting_acquia/docs/deployment.md:6-8
Timestamp: 2025-06-01T08:09:18.993Z
Learning: Do not review files in test fixture directories (e.g., files under paths containing `/tests/Fixtures/` or similar test fixture patterns). These files are used for testing purposes and should remain as-is.

Applied to files:

  • .vortex/tooling/phpcs.xml
  • .vortex/tooling/tests/Self/CheckNoExitSelfTest.php
📚 Learning: 2025-06-01T08:08:59.071Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1693
File: .vortex/installer/tests/Fixtures/install/deploy_type_none_gha/.github/workflows/build-test-deploy.yml:1-4
Timestamp: 2025-06-01T08:08:59.071Z
Learning: Test fixture files (typically located in directories containing "fixture" or "test") should not be reviewed for code quality issues as they may intentionally contain incomplete, invalid, or malformed data for testing purposes.

Applied to files:

  • .vortex/tooling/phpcs.xml
  • .vortex/tooling/tests/Unit/NotifyEmailTest.php
📚 Learning: 2025-08-08T12:02:24.652Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1896
File: .vortex/tests/bats/unit/download-db-lagoon.bats:24-25
Timestamp: 2025-08-08T12:02:24.652Z
Learning: In .vortex/tests/bats/unit Bats tests using ../_helper.bash (run_steps), prefixing a STEPS entry with "- " denotes a negative assertion (the substring must NOT appear in output). Unprefixed entries are positive assertions. Example: "- Database dump refresh requested. Will create a new dump." asserts absence; "Database dump refresh requested. Will create a new dump." asserts presence.

Applied to files:

  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
📚 Learning: 2025-07-23T01:16:30.963Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1816
File: .docker/cli.dockerfile:72-76
Timestamp: 2025-07-23T01:16:30.963Z
Learning: In the Vortex project, unauthenticated Composer installs should be allowed, so GitHub token secrets should not be marked as `required=true` in Docker build mounts. The conditional check `if [ -s /run/secrets/github_token ]` is the preferred approach to allow builds to proceed without a token when needed.

Applied to files:

  • .vortex/tooling/tests/Unit/NotifyGithubTest.php
🧬 Code graph analysis (21)
.vortex/tooling/tests/Unit/OverrideTest.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/src/helpers.php (1)
  • execute_override (43-52)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockPassthru (161-163)
  • mockQuit (196-212)
.vortex/tooling/tests/Self/MockRequestSelfTest.php (3)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • runScript (38-92)
.vortex/tooling/tests/Traits/MockTrait.php (5)
  • mockRequestGet (405-418)
  • mockRequestPost (436-450)
  • mockRequest (373-389)
  • mockRequestAssertAllMocksConsumed (458-469)
  • mockRequestMultiple (229-359)
.vortex/tooling/src/helpers.php (3)
  • request_get (341-347)
  • request_post (369-376)
  • request (398-455)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (2)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (2)
  • __construct (22-31)
  • getOutput (39-41)
.vortex/installer/src/Utils/Tui.php (1)
  • output (38-43)
.vortex/tooling/tests/Unit/DotenvTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • setUp (21-28)
.vortex/tooling/src/helpers.php (1)
  • load_dotenv (60-94)
.vortex/tooling/tests/Unit/NotifyWebhookTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockRequest (373-389)
.vortex/tooling/tests/Unit/NotifySlackTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (4)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockRequestPost (436-450)
.vortex/tooling/tests/Unit/CommandExistsTest.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/src/helpers.php (2)
  • command_exists (278-283)
  • fail (260-263)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/tests/Unit/UnitTestCase.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (2)
  • QuitErrorException (12-43)
  • getOutput (39-41)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (2)
  • QuitSuccessException (12-43)
  • getOutput (39-41)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockTearDown (63-81)
  • mockQuit (196-212)
.vortex/tooling/tests/Unit/FormatterTest.php (4)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • setUp (21-28)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/src/helpers.php (2)
  • fail (260-263)
  • term_supports_color (268-270)
.vortex/tooling/tests/Self/CheckNoExitSelfTest.php (1)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • setUp (21-28)
.vortex/tooling/tests/Unit/GetenvTest.php (4)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • setUp (21-28)
.vortex/tooling/src/helpers.php (2)
  • getenv_default (119-136)
  • getenv_required (161-182)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/tests/Unit/NotifyEmailTest.php (1)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/tests/Unit/RequestTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • setUp (21-28)
.vortex/tooling/src/helpers.php (3)
  • request_get (341-347)
  • request_post (369-376)
  • request (398-455)
.vortex/tooling/tests/Unit/NotifyRouterTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
  • runScript (38-92)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockPassthru (161-163)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (2)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (2)
  • __construct (22-31)
  • getOutput (39-41)
.vortex/installer/src/Utils/Tui.php (1)
  • output (38-43)
.vortex/tooling/tests/Self/MockPassthruSelfTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • runScript (38-92)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockPassthru (161-163)
  • mockPassthruAssertAllMocksConsumed (171-182)
.vortex/tooling/tests/Unit/NotifyGithubTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (4)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockRequestPost (436-450)
  • mockRequestGet (405-418)
.vortex/tooling/tests/Unit/TokenTest.php (1)
.vortex/tooling/src/helpers.php (1)
  • replace_tokens (296-312)
.vortex/tooling/tests/Unit/NotifyNewrelicTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockRequestPost (436-450)
  • mockRequestGet (405-418)
.vortex/tooling/tests/Unit/NotifyJiraTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (3)
  • mockRequestGet (405-418)
  • mockRequestPost (436-450)
  • mockRequest (373-389)
.vortex/tooling/src/helpers.php (1)
.vortex/installer/src/Utils/Tui.php (3)
  • line (57-59)
  • output (38-43)
  • error (53-55)
🪛 Gitleaks (8.29.0)
.vortex/tooling/tests/Unit/NotifyGithubTest.php

[high] 24-24: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

.vortex/tooling/tests/Unit/NotifyNewrelicTest.php

[high] 25-25: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 48-48: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 73-73: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 91-91: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 100-100: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 118-118: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 144-144: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 166-166: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 183-183: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 200-200: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 LanguageTool
.vortex/tooling/CLAUDE.md

[grammar] ~230-~230: Ensure spelling is correct
Context: ...ssertAllMocksConsumed(): void #### Passthru Mocking **Response Structure**:php...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

.vortex/tooling/plan.test-notify.md

[style] ~400-~400: To elevate your writing, try using a synonym here.
Context: ...t count per script - Any lines that are hard to cover (with explanation) - Overall c...

(HARD_TO)

🪛 markdownlint-cli2 (0.18.1)
.vortex/tooling/CLAUDE.md

104-104: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


155-155: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


179-179: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


190-190: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


200-200: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


211-211: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


222-222: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


233-233: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


333-333: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


338-338: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: build (1)
  • GitHub Check: build (0)
  • GitHub Check: vortex-test-workflow (2)
  • GitHub Check: vortex-test-workflow (3)
  • GitHub Check: vortex-test-workflow (4)
  • GitHub Check: vortex-test-workflow (0)
  • GitHub Check: vortex-test-workflow (1)
  • GitHub Check: vortex-test-common
  • GitHub Check: vortex-test-installer (8.4)
  • GitHub Check: vortex-test-installer (8.3)
  • GitHub Check: vortex-test-docs
🔇 Additional comments (22)
.vortex/tooling/composer.json (1)

1-67: LGTM! Well-structured tooling manifest

The composer.json is properly configured for an internal tooling package with appropriate dev dependencies, autoloading, and helper scripts. The PHP constraint placement in require-dev is appropriate for this use case.

.vortex/tooling/src/notify-slack (1)

1-138: Slack notification script looks solid

The implementation correctly handles configuration, builds proper Slack payloads, manages token replacement, and provides good logging throughout. The PHPCS issues from past reviews have been addressed.

.vortex/tooling/scripts_conversion_plan.md (1)

1-574: Comprehensive conversion plan for Bash to PHP migration

This planning document provides a well-structured approach to converting Vortex scripts from Bash to PHP, with clear phases, patterns, and success criteria.

.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)

1-43: LGTM! Well-designed test exception

The QuitErrorException is properly implemented with appropriate validation to ensure it only accepts non-zero exit codes. The symmetry with QuitSuccessException and the captured output feature make this suitable for testing termination scenarios.

.vortex/tooling/phpcs.xml (1)

1-57: PHPCS configuration properly customizes Drupal standard

The configuration appropriately extends Drupal coding standards and adds DrevOps-specific rules for the tooling codebase, with sensible exclusions for test files.

.vortex/tooling/plan.test-notify.md (1)

1-449: Comprehensive and well-structured test plan

This testing plan provides clear objectives, scope, strategy, and implementation phases for achieving high test coverage of the notify scripts. The phased approach and detailed examples will guide effective test implementation.

.vortex/tooling/src/notify-jira (2)

103-116: Good: Proper early exit when issue not found

The script correctly calls quit() after logging the skip message when the deployment label doesn't contain an issue number, preventing undefined variable errors downstream.


1-271: Well-implemented Jira integration with comprehensive features

The notify-jira script properly handles authentication, comment posting with ADF formatting, optional transitions, and optional assignee updates. Past issues with unused functions and inconsistent output have been addressed.

.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)

22-41: LGTM!

The exception class correctly enforces that only exit code 0 can be used, maintaining symmetry with QuitErrorException (which enforces non-zero codes). The validation and output capture are properly implemented.

.vortex/tooling/tests/Unit/CommandExistsTest.php (1)

26-48: LGTM!

The tests correctly validate both successful and failed command existence checks. The error path properly captures output and verifies the exception and error message.

.vortex/tooling/tests/Unit/DotenvTest.php (1)

26-97: LGTM!

The test suite provides comprehensive coverage of dotenv loading behavior, including edge cases like quoted values, values with equals signs, spaces, comments, and missing files. The data-driven approach ensures thorough validation.

.vortex/tooling/tests/Unit/NotifyEmailTest.php (1)

1-223: LGTM! Tests follow the correct pattern for script execution with early exits

The tests properly use runScriptEarlyPass() and runScriptError() to validate both exit behavior and output. The data-driven approach for missing variables and the comprehensive coverage of email notification scenarios (sendmail, mail command, multiple recipients, custom messages, fallback variables, token replacement) demonstrate thorough testing.

.vortex/tooling/tests/Unit/NotifyWebhookTest.php (1)

1-223: LGTM! Comprehensive webhook notification test coverage

The tests properly handle script execution with early exits using runScriptEarlyPass() and runScriptError(). The coverage is thorough: default/custom payloads, pre-deployment handling, data-driven missing variables, fallback variables, HTTP methods, headers, status codes, error handling, token replacement, and URL sanitization. Well structured.

.vortex/tooling/tests/Unit/UnitTestCase.php (1)

1-122: LGTM! Well-designed test base class with proper exception and output handling

The runScript() family of methods correctly handles the complexity of testing scripts that call quit():

  • runScript() properly manages output buffering and working directory, catching Quit exceptions to attach output before rethrowing
  • runScriptEarlyPass() and runScriptError() provide clean APIs for tests that need to validate both exit code and output
  • The exception-then-rethrow pattern ensures PHPUnit's expectException mechanism works while preserving output for assertions

The composition of MockTrait and EnvTrait provides a solid foundation for the notify script tests.

.vortex/tooling/tests/Unit/NotifySlackTest.php (1)

1-263: Tests follow correct pattern; note that payload assertions require MockTrait enhancement

The test structure is correct—using runScriptError() for failure cases and properly organizing test methods. The comprehensive coverage of Slack notification scenarios (post/pre-deployment, custom messages, channels, bot settings, missing variables, fallback, URL sanitization, token replacement) is excellent.

As noted in the past review, mockRequestPost() currently ignores the $body callback parameter (lines 34–46, 63–72, etc.), so payload assertions inside those callbacks won't execute. The past review provided guidance on updating MockTrait::mockRequestPost() to support body validation. Until that's implemented, these tests verify the HTTP call is made but don't validate the payload structure.

Based on past review feedback, this limitation should be addressed separately.

.vortex/tooling/tests/Unit/NotifyNewrelicTest.php (1)

1-254: LGTM! Comprehensive New Relic deployment notification tests

The tests properly use runScriptEarlyPass() and runScriptError() helpers to validate both exit behavior and output. Coverage is thorough: provided/auto-discovered App IDs, auto-generated revisions, custom descriptions/changelogs/endpoints, pre-deployment skipping, failure handling, and data-driven missing variables with fallback.

Note: Gitleaks flags the test API keys (lines 25, 48, 73, etc.) as potential secrets. These are clearly test fixtures (NRAK-TEST123456, NRAK-FALLBACK456) with no production value. The warnings can be safely ignored.

.vortex/tooling/tests/Self/MockPassthruSelfTest.php (1)

1-415: LGTM! Thorough passthru() mocking test coverage

The tests correctly validate mockPassthru() behavior across multiple scenarios:

  • Direct passthru calls with proper output buffering (lines 21–160)
  • Argument validation and error conditions (lines 72–126)
  • Multiple sequential mocks with consumption checks (lines 128–237)
  • Script-driven validations using fixtures (lines 239–414)

The manual triggering of mockPassthruAssertAllMocksConsumed() in under-consumption tests (lines 236, 398) is appropriate. Output buffering management with try-finally blocks ensures cleanup even when exceptions are expected. Well structured tests.

.vortex/tooling/tests/Unit/NotifyRouterTest.php (3)

16-33: Setup and baseline env configuration look solid

Using setUp() (Line 20) to seed VORTEX_NOTIFY_PROJECT, VORTEX_NOTIFY_LABEL, and VORTEX_NOTIFY_ENVIRONMENT_URL gives all tests a consistent baseline and keeps individual tests focused on deltas only. No issues here.


30-46: Early‑exit skip/missing‑label tests are correctly delegated to shared helpers

testSkipWhenNotifySkipIsSet() and testSkipWhenNoChannelsSpecified() (Lines 30–40), plus testFailureWhenLabelIsMissing() (Lines 42–46), use runScriptEarlyPass() / runScriptError() with expected output strings, which centralizes the exit‑code and output assertions and keeps tests concise. This pattern is clear and consistent.


64-85: Channel existence/permissions and per‑channel execution behavior are well covered

The tests for a missing channel script (Lines 64–68), non‑executable script (Lines 70–85), failing channel exit code (Lines 87–97), and “all channels executed even if one has no output” (Lines 266–288) together exercise the main unhappy paths and aggregation behavior of the router. The use of a temporary file and finally cleanup in testFailureWhenChannelScriptNotExecutable() is particularly good practice.

Also applies to: 87-97, 266-288

.vortex/tooling/tests/Unit/NotifyJiraTest.php (2)

20-33: Jira notifier setup is comprehensive and keeps tests focused

The setUp() method (Lines 20–33) initializes all Jira‑specific environment variables (project, user, token, label, URLs, event, endpoint), so individual tests only need to override what they care about. This keeps the suite readable and reduces duplication.


380-455: Fallbacks, default endpoint, full workflow, and issue‑format coverage are strong

The tests for generic env fallbacks, issue extraction from labels, default Jira endpoint, complete workflow with all features, event defaulting, and multiple issue formats (Lines 380–455, 457–487, 489–570, 729–758, 760–801) together provide thorough coverage of key behaviors and edge cases. The use of data providers for issue formats and clear output assertions (project, deployment, issue, URLs) is particularly good.

Also applies to: 457-487, 489-570, 729-758, 760-801

Comment on lines +333 to +362
public function testSuccessfulNotificationWithCustomMessage(): void {
$this->envSet('VORTEX_NOTIFY_JIRA_MESSAGE', 'Custom deployment of %project% to %label% at %timestamp%');

// Mock authentication.
$this->mockRequestGet(
'https://jira.example.com/rest/api/3/myself',
[
'Authorization: Basic ' . base64_encode('[email protected]:test-token-123'),
'Content-Type: application/json',
],
10,
['status' => 200, 'body' => '{"accountId": "123456789012345678901234"}']
);

// Mock comment creation.
$this->mockRequestPost(
'https://jira.example.com/rest/api/3/issue/TEST-123/comment',
$this->callback(fn(): true => TRUE),
[
'Authorization: Basic ' . base64_encode('[email protected]:test-token-123'),
'Content-Type: application/json',
],
10,
['status' => 201, 'body' => '{"id": "10001"}']
);

$output = $this->runScript('src/notify-jira');

$this->assertStringContainsString('Finished JIRA notification', $output);
}
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

Custom‑message test doesn’t currently verify the rendered message

testSuccessfulNotificationWithCustomMessage() (Lines 333–362) sets VORTEX_NOTIFY_JIRA_MESSAGE to a templated string and only asserts that the notification finished successfully. It doesn’t check that the custom message was actually used in the comment body or output.

If the notifier logs the rendered message or includes it in the request payload (which you’re already mocking), consider asserting on that so regressions in message templating are detectable.

Comment on lines +337 to +345
$this->mockRequestGet(
'https://jira.example.com/rest/api/3/myself',
[
'Authorization: Basic ' . base64_encode('[email protected]:test-token-123'),
'Content-Type: application/json',
],
10,
['status' => 200, 'body' => '{"accountId": "123456789012345678901234"}']
);
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Optional: factor out repeated headers/authorization into small helpers

Across many mocks you repeat the same Authorization + JSON headers and status/ID patterns (e.g., Lines 337–345, 393–414, 460–481, 493–514, 517–537, 540–562, 603–611, 649–658, 687–705, 733–752, 764–783). Extracting a tiny helper (e.g., private function authHeaders(): array and maybe builders for common 200/201/204 responses) would reduce duplication and make it easier to tweak these values in one place if needed.

Not mandatory, but would improve maintainability.

Also applies to: 393-414, 460-481, 493-514, 517-537, 540-562, 603-611, 649-658, 687-705, 733-752, 764-783

🤖 Prompt for AI Agents
.vortex/tooling/tests/Unit/NotifyJiraTest.php around lines 337-345: the test
file repeats identical Authorization and Content-Type headers and common
response payloads across many mocks; factor this duplication by adding a private
helper (e.g., private function authHeaders(): array) that returns the standard
Authorization + JSON headers and small helper builders for common responses
(e.g., okResponse(array $body), createdResponse(array $body),
noContentResponse()) and then replace the repeated literal header/response
arrays in each mock block (including the other listed ranges) with calls to
these helpers so tests remain identical but duplication is eliminated and easy
to change.

Comment on lines +48 to +62
#[DataProvider('dataProviderFailureWithInvalidEventType')]
public function testFailureWithInvalidEventType(string $event): void {
$this->envSet('VORTEX_NOTIFY_EVENT', $event);

$this->runScriptError('src/notify', 'Unsupported event ' . $event . ' provided');
}

public static function dataProviderFailureWithInvalidEventType(): array {
return [
'invalid event' => ['invalid_event'],
'deployment' => ['deployment'],
'pre-deployment' => ['pre-deployment'],
'postdeployment' => ['postdeployment'],
];
}
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

Event type coverage is good, but the “default is post_deployment” test is a bit weak

The invalid/valid event type data providers and tests (Lines 48–62, 194–217) cover a good range of supported and unsupported values. However, testDefaultEventTypeIsPostDeployment() (Lines 219–232) only asserts that notifications complete successfully; it doesn’t actually verify that the default event used is post_deployment vs. some other non‑skipping value.

If the router logs or exposes the resolved event, consider asserting on that specific text/value so the test fails if the default ever changes.

Also applies to: 194-217, 219-232

🤖 Prompt for AI Agents
In .vortex/tooling/tests/Unit/NotifyRouterTest.php around lines 48–62 (and also
update the related test at lines ~219–232), the default-event test only asserts
success but not that the resolved event is post_deployment; update the test to
explicitly assert that the router used the 'post_deployment' event by capturing
the script/router output or log and asserting that it contains the literal
'post_deployment' (or the exact log message that names the resolved event). If
necessary adjust the helper (e.g. runScript/runScriptError) to return
stdout/stderr so you can assert the presence of 'post_deployment', and add a
targeted assertion after the success check so the test will fail if the default
event changes.

Comment on lines +99 to +165
public function testSuccessfulNotificationWithDefaultChannel(): void {
$this->envUnset('VORTEX_NOTIFY_CHANNELS');

$script_path = realpath(__DIR__ . '/../../src/notify-email');
$this->mockPassthru([
'cmd' => '"' . $script_path . '"',
'output' => 'Email notification sent successfully',
'result_code' => 0,
]);

$output = $this->runScript('src/notify');

$this->assertStringContainsString('Started dispatching notifications', $output);
$this->assertStringContainsString('Email notification sent successfully', $output);
$this->assertStringContainsString('Finished dispatching notifications', $output);
}

public function testSuccessfulNotificationWithSingleChannel(): void {
$this->envSet('VORTEX_NOTIFY_CHANNELS', 'slack');

$script_path = realpath(__DIR__ . '/../../src/notify-slack');
$this->mockPassthru([
'cmd' => '"' . $script_path . '"',
'output' => 'Slack notification sent successfully',
'result_code' => 0,
]);

$output = $this->runScript('src/notify');

$this->assertStringContainsString('Started dispatching notifications', $output);
$this->assertStringContainsString('Slack notification sent successfully', $output);
$this->assertStringContainsString('Finished dispatching notifications', $output);
}

public function testSuccessfulNotificationWithMultipleChannels(): void {
$this->envSet('VORTEX_NOTIFY_CHANNELS', 'email,slack,webhook');

$email_path = realpath(__DIR__ . '/../../src/notify-email');
$slack_path = realpath(__DIR__ . '/../../src/notify-slack');
$webhook_path = realpath(__DIR__ . '/../../src/notify-webhook');

$this->mockPassthru([
'cmd' => '"' . $email_path . '"',
'output' => 'Email notification sent',
'result_code' => 0,
]);

$this->mockPassthru([
'cmd' => '"' . $slack_path . '"',
'output' => 'Slack notification sent',
'result_code' => 0,
]);

$this->mockPassthru([
'cmd' => '"' . $webhook_path . '"',
'output' => 'Webhook notification sent',
'result_code' => 0,
]);

$output = $this->runScript('src/notify');

$this->assertStringContainsString('Started dispatching notifications', $output);
$this->assertStringContainsString('Email notification sent', $output);
$this->assertStringContainsString('Slack notification sent', $output);
$this->assertStringContainsString('Webhook notification sent', $output);
$this->assertStringContainsString('Finished dispatching notifications', $output);
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Optional: assert realpath() success to make failures easier to debug

In the success-path tests for default, single, and multiple channels (Lines 99–165, 167–192), realpath() is used to build the notifier script paths, but its return value isn’t checked. If a script were accidentally missing or misnamed, you’d end up mocking a command with "" rather than getting a clear failure.

Consider adding an explicit assertion before using $script_path:

-    $script_path = realpath(__DIR__ . '/../../src/notify-email');
+    $script_path = realpath(__DIR__ . '/../../src/notify-email');
+    $this->assertNotFalse($script_path, 'notify-email script path must resolve via realpath().');

and similarly for the Slack/Webhook variants. This will surface configuration issues more clearly during test failures.

Also applies to: 167-192

🤖 Prompt for AI Agents
.vortex/tooling/tests/Unit/NotifyRouterTest.php around lines 99-165: the tests
call realpath() to compute notifier script paths but do not verify the return
value, so a missing/misnamed script yields an empty string in mocks and a
confusing failure; before using the path values assert that realpath(...)
returned a valid path (e.g. assertNotFalse or assertFileExists on the computed
path) and only use the returned value in mockPassthru; apply the same check for
the Slack and Webhook path usages in the other test blocks (lines referenced and
also the 167-192 range mentioned in the comment).

Comment on lines +219 to +248
public function testDefaultEventTypeIsPostDeployment(): void {
$this->envUnset('VORTEX_NOTIFY_EVENT');
$this->envSet('VORTEX_NOTIFY_CHANNELS', 'email');

$script_path = realpath(__DIR__ . '/../../src/notify-email');
$this->mockPassthru([
'cmd' => '"' . $script_path . '"',
'result_code' => 0,
]);

$output = $this->runScript('src/notify');

$this->assertStringContainsString('Finished dispatching notifications', $output);
}

public function testProjectFallbackToGenericVariable(): void {
$this->envUnset('VORTEX_NOTIFY_PROJECT');
$this->envSet('VORTEX_PROJECT', 'fallback-project');
$this->envSet('VORTEX_NOTIFY_CHANNELS', 'email');

$script_path = realpath(__DIR__ . '/../../src/notify-email');
$this->mockPassthru([
'cmd' => '"' . $script_path . '"',
'result_code' => 0,
]);

$output = $this->runScript('src/notify');

$this->assertStringContainsString('Finished dispatching notifications', $output);
}
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

Fallback/login tests don’t currently validate the behavior implied by their names

testProjectFallbackToGenericVariable() (Lines 234–248) and testDefaultLoginUrlGeneration() (Lines 250–264) only assert that notifications “Finished dispatching”; they do not validate that the router actually used the generic project variables or derived login URL, even though the method names suggest this is what they guarantee.

If the router logs the effective project or login URL, or if the notifier scripts expose this via their output, consider tightening these tests to assert on those values (e.g., expecting fallback-project or a derived login URL) so regressions in fallback logic are caught. Otherwise, renaming the tests to reflect what they actually verify would reduce false confidence.

Also applies to: 250-264

🤖 Prompt for AI Agents
.vortex/tooling/tests/Unit/NotifyRouterTest.php lines 219-264 — The two tests
named to validate fallback behavior only assert the generic "Finished
dispatching notifications" message and do not actually verify that the router
used the fallback project or generated the expected login URL; update each test
to either make the mocked notifier emit the effective value and assert it, or
rename the test to match current coverage. Concretely: modify the mockPassthru
response for the notifier script to include a predictable marker containing the
effective project/login (e.g., "PROJECT: fallback-project" or "LOGIN_URL:
https://..."), then after runScript assertStringContainsString for that marker;
if you prefer not to extend mocking, rename the tests to reflect they only
assert dispatch completion.

@AlexSkrypnyk AlexSkrypnyk force-pushed the feature/1192-tooling-package branch 2 times, most recently from 916116b to d641987 Compare November 23, 2025 06:18
@github-actions github-actions bot temporarily deployed to commit November 23, 2025 06:21 Inactive
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: 5

♻️ Duplicate comments (37)
.vortex/tooling/scripts_conversion_plan.md (1)

84-166: Update helper function names in examples to match actual implementation

The planning document still references outdated function names that don't match the actual helpers.php implementation:

  • load_env() should be load_dotenv()
  • execute_override_if_exists() should be execute_override()
  • validate_variable() should be getenv_required()
  • validate_command() should be command_exists()

These mismatches appear in multiple sections (lines 84-166, 196-261, 408-417) and could confuse developers implementing scripts based on this plan.

Apply this diff to update the function names throughout the document:

-function load_env() {
+function load_dotenv() {
   // Load .env file
-function execute_override_if_exists($script_name) {
+function execute_override($script_name) {
-function require_env($var_name, $message = null) {
+function getenv_required($var_name, $message = null) {
-function require_command($command) {
+function command_exists($command) {
.vortex/tooling/phpcs.xml (1)

37-56: Fix exclude-pattern glob syntax

The exclude patterns use backslash-escaped dots (*.Test\.php, *.TestCase\.php) which PHPCS interprets as shell-style globs, not regex. The backslashes are unnecessary and may prevent proper matching.

Apply this diff to fix the glob patterns:

-    <exclude-pattern>*.Test\.php</exclude-pattern>
-    <exclude-pattern>*.TestCase\.php</exclude-pattern>
-    <exclude-pattern>*.test</exclude-pattern>
+    <exclude-pattern>*Test.php</exclude-pattern>
+    <exclude-pattern>*TestCase.php</exclude-pattern>
+    <exclude-pattern>*.test</exclude-pattern>

If you need to match test files in subdirectories, use **/*Test.php patterns instead.

.vortex/tooling/tests/Unit/TokenTest.php (1)

32-55: Add test cases for JSON-escaping edge values

The data provider covers basic scenarios but misses critical edge cases where replace_tokens() relies on json_encode() for escaping. Consider adding test cases for values containing:

  • Double quotes: 'He said "hi"'
  • Backslashes: 'C:\folder\file'
  • Newlines: "Line1\nLine2"
  • Other JSON-escaped characters: tabs, control characters

These tests would lock in the escaping behavior and catch regressions if the implementation changes.

Example additional test cases:

'values with quotes' => [
  'template' => 'Message: %msg%',
  'replacements' => ['msg' => 'He said "hello"'],
  'expected' => 'Message: He said \"hello\"',
],
'values with backslashes' => [
  'template' => 'Path: %path%',
  'replacements' => ['path' => 'C:\folder\file'],
  'expected' => 'Path: C:\\folder\\file',
],
'values with newlines' => [
  'template' => 'Text: %text%',
  'replacements' => ['text' => "Line1\nLine2"],
  'expected' => 'Text: Line1\nLine2',
],
.vortex/tooling/src/helpers.php (4)

44-57: Add path traversal protection to execute_override

The function concatenates $path directly into a file path without validation. While current callers pass basename(__FILE__), the function signature accepts any string, creating a potential path traversal vulnerability if used incorrectly in the future.

Apply this diff to add validation:

 function execute_override(string $path): void {
+  // Prevent path traversal by ensuring path doesn't contain directory separators.
+  if (str_contains($path, DIRECTORY_SEPARATOR) || str_contains($path, '/') || str_contains($path, '\\')) {
+    fail('Invalid script name for override: %s', $path);
+  }
+
   $dir = getenv('VORTEX_TOOLING_CUSTOM_DIR');
   if ($dir) {
     if (!is_dir($dir)) {
       fail('Custom directory specified in VORTEX_TOOLING_CUSTOM_DIR does not exist: %s', $dir);
     }
 
     $full_path = $dir . DIRECTORY_SEPARATOR . $path;

283-288: Shell-escape $command to prevent injection risk

The function interpolates $command directly into a shell string without escaping. While current usage may only pass trusted constants, adding escapeshellarg() is a simple defense-in-depth measure that prevents command injection if usage patterns change.

Apply this diff:

 function command_exists(string $command): void {
-  exec(sprintf('command -v %s 2>/dev/null', $command), $output, $code);
+  exec(sprintf('command -v %s 2>/dev/null', escapeshellarg($command)), $output, $code);
   if ($code !== 0) {
     fail(sprintf("Command '%s' is not available", $command));
   }
 }

409-409: Check curl_init return value to prevent TypeError

curl_init() can return false on failure (e.g., malformed URL). Passing false to curl_setopt_array() on line 431 will cause a TypeError in PHP 8+.

Apply this diff:

   $ch = curl_init($url);
+  if ($ch === FALSE) {
+    return [
+      'ok' => FALSE,
+      'status' => 0,
+      'body' => FALSE,
+      'error' => 'Failed to initialize curl',
+      'info' => [],
+    ];
+  }
 
   try {

431-431: Check curl_setopt_array return value

While less critical than the curl_init check, curl_setopt_array() returns false if setting options fails. Checking the return value would allow graceful handling of configuration errors rather than proceeding with a misconfigured handle.

Apply this diff:

     curl_setopt_array($ch, $opts);
+    if (curl_errno($ch)) {
+      $error = curl_error($ch);
+      return [
+        'ok' => FALSE,
+        'status' => 0,
+        'body' => FALSE,
+        'error' => $error,
+        'info' => [],
+      ];
+    }
 
     // With CURLOPT_RETURNTRANSFER, curl_exec returns string|false.
.vortex/tooling/src/notify-webhook (2)

16-17: Fix PHPCS: remove blank line after separator comments

There’s still an empty line after the // ----------------------------------------------------------------------------- separators (Lines 16 and 63), which violates Drupal.Commenting.InlineComment.SpacingAfter as flagged by the tooling pipeline. Remove the blank lines so the next code/comment line follows immediately.

Also applies to: 63-64


44-49: Filter out empty headers from VORTEX_NOTIFY_WEBHOOK_HEADERS

If the headers env var has trailing or double pipes, explode('|', $webhook_headers) can produce empty strings that are passed through to request(). Trim and then drop empty entries for cleaner behavior.

-$response = request($webhook_url, [
-  'method' => $webhook_method,
-  'headers' => array_map(trim(...), explode('|', $webhook_headers)),
-  'body' => $webhook_payload,
-]);
+$response = request($webhook_url, [
+  'method' => $webhook_method,
+  'headers' => array_filter(
+    array_map(trim(...), explode('|', $webhook_headers)),
+    static fn(string $header): bool => $header !== ''
+  ),
+  'body' => $webhook_payload,
+]);

Also applies to: 97-102

.vortex/tooling/src/notify-slack (2)

18-19: Fix PHPCS: remove blank line after separator comments

The separator comments at Lines 18 and 62 are still followed by a blank line, which violates Drupal.Commenting.InlineComment.SpacingAfter and is flagged by the tooling checks. Drop the empty lines so code follows immediately.

Also applies to: 62-63


128-134: Optionally validate Slack response body, not just HTTP 200

Slack webhooks can return HTTP 200 with an error string body (e.g., invalid_payload), which would currently be treated as success. If you want stricter detection of misconfiguration, also inspect the body (e.g., expect 'ok').

-$response = request_post($slack_webhook, $payload, [
-  'Content-Type: application/json',
-]);
-
-if ($response['status'] !== 200) {
-  fail('Unable to send notification to Slack. HTTP status: %s', $response['status']);
-}
+$response = request_post($slack_webhook, $payload, [
+  'Content-Type: application/json',
+]);
+
+$body = (string) ($response['body'] ?? '');
+if ($response['status'] !== 200 || trim($body) !== 'ok') {
+  fail(
+    'Unable to send notification to Slack. HTTP status: %s, body: %s',
+    $response['status'],
+    $body
+  );
+}
.vortex/tooling/tests/Unit/GetenvTest.php (2)

88-93: Fix getenv_default exception message expectation

helpers.php throws:

throw new \InvalidArgumentException(
  'getenv_default() requires at least 2 arguments: one or more variable names and a default value'
);

but this test expects only 'getenv_default() requires at least 2 arguments'. The mismatch will cause the test to fail.

Update the expectation to match the full message or relax it to assertStringContainsString('getenv_default() requires at least 2 arguments', ...).


137-171: captureOutput + expectException makes output assertions unreachable

In the getenv_required failure tests, you:

  • Call $this->mockQuit(1);
  • Set expectException(QuitErrorException::class);
  • Assign $output = $this->captureOutput(...)
  • Assert against $output.

However, captureOutput() rethrows on any Throwable and never returns when an exception is thrown, so the assignment to $output never completes and the subsequent assertStringContainsString(..., $output) lines are never executed. The tests currently only verify that an exception is thrown, not that the expected error text is emitted.

Refactor these tests to either:

  • Drop expectException() and wrap the getenv_required(...) call in a try/catch (QuitErrorException $e) that also captures buffered output (e.g., using ob_start()/ob_get_clean() directly in the test), or
  • Adjust captureOutput() to not be used together with expectException, and instead have a dedicated helper that returns both $output and the caught exception for assertion.

As written, the output assertions are dead code.

Also applies to: 180-189

.vortex/tooling/tests/Unit/NotifyGithubTest.php (1)

23-31: Avoid PAT‑like test tokens to satisfy secret scanners

The hard-coded values 'ghp_test123456' and 'ghp_fallback123' resemble real GitHub personal access tokens and are flagged by Gitleaks as a generic API key, even though they’re only test data.

To keep tests representative without tripping secret detection, switch to clearly fake/non-PAT formats (e.g. 'test-token', 'GITHUB_TOKEN_TEST', or a 'ghp_' prefix assembled from concatenated strings) in env setup and header expectations.

Also applies to: 47-48, 72-73, 91-92, 117-118, 133-134, 147-148, 160-161, 198-199, 210-211, 235-236

.vortex/tooling/src/notify-email (1)

100-114: Sanitize email header values to prevent header injection

Values derived from environment/config ($email_from, $email_subject, $email_recipients$email, $name) are interpolated directly into To:, Subject:, and From: headers and into the sendmail/mail commands without stripping CR/LF. An attacker (or misconfigured input) could inject additional headers or recipients via \r/\n sequences.

Introduce a small sanitizer for header fields and use it consistently before composing headers and shell commands. For example:

@@
-$email_message = getenv_default('VORTEX_NOTIFY_EMAIL_MESSAGE', "## This is an automated message ##\nSite %project% %label% has been deployed at %timestamp% and is available at %environment_url%.\nLogin at: %login_url%");
+$email_message = getenv_default('VORTEX_NOTIFY_EMAIL_MESSAGE', "## This is an automated message ##\nSite %project% %label% has been deployed at %timestamp% and is available at %environment_url%.\nLogin at: %login_url%");
+
+/**
+ * Sanitize an email header value to prevent header injection.
+ */
+function sanitize_email_header(string $value): string {
+  return str_replace(["\r", "\n", "\0"], '', $value);
+}
@@
-$email_recipients = array_map(trim(...), explode(',', $email_recipients));
+$email_recipients = array_filter(
+  array_map(trim(...), explode(',', $email_recipients)),
+  static fn(string $value): bool => $value !== ''
+);
@@
-  $parts = array_map(trim(...), explode('|', $email_recipient, 2));
-  $email = $parts[0];
-  $name = $parts[1] ?? '';
+  $parts = array_map(trim(...), explode('|', $email_recipient, 2));
+  $email = sanitize_email_header($parts[0]);
+  $name = sanitize_email_header($parts[1] ?? '');
+
+  $safe_subject = sanitize_email_header($email_subject);
+  $safe_from = sanitize_email_header($email_from);
@@
-  $to = empty($name) ? $email : sprintf('"%s" <%s>', $name, $email);
+  $to = empty($name) ? $email : sprintf('"%s" <%s>', addslashes($name), $email);
@@
-    $mail_content .= sprintf('Subject: %s%s', $email_subject, PHP_EOL);
-    $mail_content .= sprintf('From: %s%s', $email_from, PHP_EOL);
+    $mail_content .= sprintf('Subject: %s%s', $safe_subject, PHP_EOL);
+    $mail_content .= sprintf('From: %s%s', $safe_from, PHP_EOL);
@@
-    $cmd = sprintf('echo %s | sendmail -t -f %s', escapeshellarg($mail_content), escapeshellarg($email_from));
+    $cmd = sprintf('echo %s | sendmail -t -f %s', escapeshellarg($mail_content), escapeshellarg($safe_from));
@@
-    $mail_content = "From: {$email_from}\n\n{$email_message}";
-    $cmd = sprintf('echo %s | mail -s %s %s', escapeshellarg($mail_content), escapeshellarg($email_subject), escapeshellarg($to));
+    $mail_content = "From: {$safe_from}\n\n{$email_message}";
+    $cmd = sprintf('echo %s | mail -s %s %s', escapeshellarg($mail_content), escapeshellarg($safe_subject), escapeshellarg($to));

This keeps behavior while closing the header injection vector and also filters out empty recipient entries.

Also applies to: 121-155

.vortex/tooling/src/notify-newrelic (1)

95-116: Fix app‑ID discovery control flow; avoid posting with empty application ID

In the discovery block:

if ($response['ok'] && $response['body']) {
  $data = json_decode((string) $response['body'], TRUE);
  if (isset($data['applications'][0]['id'])) {
    $newrelic_appid = $data['applications'][0]['id'];
  }

  if (!$newrelic_appid) {
    pass('Notification skipped: No New Relic application ID found for %s. This is expected for non-configured environments.', $newrelic_app_name);
    quit();
  }
}

pass('Discovered application ID: %s', $newrelic_appid);

If the HTTP request fails or the body is empty, the if block is skipped, $newrelic_appid remains empty, but you still log "Discovered application ID" and later attempt to POST to /applications//deployments.json, only failing at the final status check. That’s misleading and fragile.

Consider:

   $response = request_get($url, [
     'Api-Key: ' . $newrelic_user_key,
   ]);
 
-  if ($response['ok'] && $response['body']) {
-    $data = json_decode((string) $response['body'], TRUE);
-    if (isset($data['applications'][0]['id'])) {
-      $newrelic_appid = $data['applications'][0]['id'];
-    }
-
-    if (!$newrelic_appid) {
-      pass('Notification skipped: No New Relic application ID found for %s. This is expected for non-configured environments.', $newrelic_app_name);
-      quit();
-    }
-  }
-
-  pass('Discovered application ID: %s', $newrelic_appid);
+  if ($response['ok'] && $response['body']) {
+    $data = json_decode((string) $response['body'], TRUE);
+    if (is_array($data) && isset($data['applications'][0]['id'])) {
+      $newrelic_appid = $data['applications'][0]['id'];
+    }
+
+    if (!$newrelic_appid) {
+      pass('Notification skipped: No New Relic application ID found for %s. This is expected for non-configured environments.', $newrelic_app_name);
+      quit();
+    }
+
+    pass('Discovered application ID: %s', $newrelic_appid);
+  }
+  else {
+    pass('Notification skipped: Unable to discover New Relic application ID for %s. API request failed.', $newrelic_app_name);
+    quit();
+  }

This ensures you never proceed to the deployment POST with an empty app ID and handles both "no app found" and "API failure" as explicit skip paths.

Also applies to: 149-159

.vortex/tooling/tests/Unit/RequestTest.php (1)

13-15: Avoid real network‑coupled tests in the unit suite

All tests here rely on live HTTP calls to example.com (including a timeout case to port 81). That couples the unit suite to external DNS/network availability and example.com’s behavior, making it brittle and potentially flaky in CI or locked‑down environments.

Prefer to:

  • Test request_get, request_post, and request using MockTrait/mockRequest* to validate options and normalized response shape; and/or
  • Reclassify these as explicit integration tests (separate group) so they can be skipped or isolated when network access is unavailable.

Also applies to: 30-100

.vortex/tooling/src/notify-github (2)

56-66: Relax upper-bound length check for deployment IDs

deployment_id_is_valid() enforces 9–11 digits. GitHub deployment IDs may grow; keeping the lower bound and numeric check but dropping the <= 11 constraint would avoid future false negatives while still rejecting bad values.

Example:

-  // Check length (9-11 digits) and numeric format.
-  return $length >= 9 && $length <= 11 && ctype_digit($id_str);
+  // Check minimum length and numeric format.
+  return $length >= 9 && ctype_digit($id_str);

84-88: Set explicit JSON Content-Type for GitHub POST requests

Both deployment creation and status update send JSON bodies but only set Accept. Adding Content-Type: application/json clarifies intent and avoids relying on cURL defaults when talking to the GitHub API.

   $response = request_post($url, json_encode($payload_data), [
     'Authorization: token ' . $github_token,
-    'Accept: application/vnd.github.v3+json',
+    'Accept: application/vnd.github.v3+json',
+    'Content-Type: application/json',
   ]);
@@
   $response = request_post($url, json_encode($status_data), [
-    'Accept: application/vnd.github.v3+json',
-    'Authorization: token ' . $token,
+    'Accept: application/vnd.github.v3+json',
+    'Authorization: token ' . $token,
+    'Content-Type: application/json',
   ]);

Also applies to: 146-150

.vortex/tooling/src/notify-jira (3)

75-99: Fix Jira ADF document content shape

text_to_adf() returns a single paragraph node, but comment_body['content'] is set to that node directly. Jira’s ADF doc schema expects content to be an array of block nodes.

Wrap the paragraph node:

 $jira_message_adf = text_to_adf($jira_message);

 $comment_body = [
   'type' => 'doc',
   'version' => 1,
-  'content' => $jira_message_adf,
+  'content' => [
+    $jira_message_adf,
+  ],
 ];

Also applies to: 153-159


161-170: Avoid logging raw Jira user email by default

note('User email : ' . $jira_user_email); emits a concrete email address to standard output, which may be considered PII in CI logs.

Consider either removing this line, masking the email (e.g., local‑part initial + domain), or guarding it behind a debug/verbose flag.


191-228: Check Jira responses for transition and assignee updates

The transition POST (request_post on the transitions endpoint) and the assignee PUT (request) ignore HTTP responses. If Jira returns 4xx/5xx or malformed JSON, the script still logs success, making failures hard to spot.

Consider:

  • Capturing the responses.
  • Failing if $response['ok'] is false or the body is missing/invalid, similar to earlier authentication and comment creation checks.

Also applies to: 230-269

.vortex/tooling/src/notify (1)

65-67: Use DIRECTORY_SEPARATOR when building script path

The router builds per‑channel script paths with a hardcoded '/'. For portability, especially if these tools might ever run on non‑Unix PHP, prefer DIRECTORY_SEPARATOR:

-  $script = __DIR__ . ('/notify-' . $channel);
+  $script = __DIR__ . DIRECTORY_SEPARATOR . 'notify-' . $channel;
.vortex/tooling/tests/Unit/FormatterTest.php (3)

35-36: Use EnvTrait envSet/envUnset instead of raw putenv for TERM in tests

These tests mutate TERM directly via putenv(), bypassing EnvTrait’s tracking and envReset() in UnitTestCase::tearDown(). That can leak TERM state between tests and make them order‑dependent.

Switch to EnvTrait helpers so env changes are tracked and reset, e.g.:

   public function testOutputFormatters(string $function, string $expected_prefix): void {
-    putenv('TERM=dumb');
+    $this->envSet('TERM', 'dumb');
@@
   public function testOutputFormattersWithColor(string $function, string $expected_prefix): void {
-    putenv('TERM=xterm-256color');
+    $this->envSet('TERM', 'xterm-256color');
@@
   public function testTermSupportsColor(string|bool $term_value, bool $expected): void {
     if ($term_value === FALSE) {
-      putenv('TERM');
+      $this->envUnset('TERM');
     }
     else {
-      putenv('TERM=' . $term_value);
+      $this->envSet('TERM', $term_value);
     }

This keeps env mutations consistent with the rest of the tooling tests.

Also applies to: 64-65, 102-107


77-83: Add colored coverage for fail_no_exit in dataProviderOutputFormattersWithColor()

The with‑color data provider exercises task, info, and pass, but not fail_no_exit, even though fail_no_exit has a distinct colored prefix. That leaves a gap in formatter coverage under color‑enabled terminals.

Consider adding a case, e.g.:

   public static function dataProviderOutputFormattersWithColor(): array {
     return [
       'task' => ['task', '[TASK]'],
       'info' => ['info', '[INFO]'],
       'pass' => ['pass', '[ OK ]'],
+      'fail_no_exit' => ['fail_no_exit', '[FAIL]'],
     ];
   }

(Adjust $expected_prefix to match the actual colored output if you want to assert on the full ANSI sequence.)


100-118: Add a positive TERM case to dataProviderTermSupportsColor()

The provider only covers cases where color support is expected to be FALSE. Adding at least one “color‑capable” TERM (e.g. xterm-256color) improves confidence that term_supports_color() can return TRUE when both TERM and posix_isatty(STDOUT) allow it.

For example:

   public static function dataProviderTermSupportsColor(): array {
     return [
       'dumb terminal' => ['term_value' => 'dumb', 'expected' => FALSE],
       'no terminal' => ['term_value' => FALSE, 'expected' => FALSE],
+      'color terminal' => ['term_value' => 'xterm-256color', 'expected' => TRUE],
     ];
   }

You may need to mock posix_isatty() or ensure CI runs with a TTY‑like STDOUT to keep this deterministic.

.vortex/tooling/tests/Self/MockRequestSelfTest.php (1)

21-32: Load helpers.php in setUp() so all tests are independent

helpers.php is required only inside testMockRequestGetSuccess(). All later tests call \DrevOps\VortexTooling\request_get/post/request but never ensure the helpers are loaded themselves. This couples the class to execution order and will break when running a single later test in isolation (undefined function).

Move the require_once into setUp() and drop the inline require, e.g.:

 #[CoversClass(UnitTestCase::class)]
 class MockRequestSelfTest extends UnitTestCase {
 
+  protected function setUp(): void {
+    parent::setUp();
+    require_once __DIR__ . '/../../src/helpers.php';
+  }
+
   public function testMockRequestGetSuccess(): void {
@@
-    // Load helpers.php AFTER setting up mocks.
-    require_once __DIR__ . '/../../src/helpers.php';
-
-    $result = \DrevOps\VortexTooling\request_get('https://example.com/api');
+    $result = \DrevOps\VortexTooling\request_get('https://example.com/api');

The curl mocks created by mockRequest* will still work correctly with this arrangement.

.vortex/tooling/tests/Self/MockQuitSelfTest.php (1)

61-107: Script-based quit tests never reach the output assertions

In the script tests (testMockQuitScript0* / testMockQuitScript1*), the flow is:

  • mockQuit(...) sets up a mock that throws QuitSuccessException / QuitErrorException or RuntimeException.
  • The test calls expectException(...) / expectExceptionCode(...).
  • $this->runScript('tests/Fixtures/...') is invoked.
  • runScript() catches the QuitException, captures the buffer, and rethrows a new QuitException with the output attached.
  • PHPUnit sees the expected exception and immediately ends the test method.

Lines 69–71, 81–83, 93–95, and 105–107 are therefore unreachable; the tests never actually assert on $output, so they don’t validate the script output they appear to check.

Refactor to capture both exception and output explicitly, e.g.:

   public function testMockQuitScript0Success(): void {
     $this->mockQuit();
-
-    $this->expectException(QuitSuccessException::class);
-    $this->expectExceptionCode(0);
-
-    $output = $this->runScript('tests/Fixtures/test-quit-passing');
-
-    $this->assertStringContainsString('Script will exit with code 0', $output);
-    $this->assertStringNotContainsString('ERROR Script continued after quit()', $output);
+    try {
+      $this->runScript('tests/Fixtures/test-quit-passing');
+      $this->fail('Expected QuitSuccessException was not thrown.');
+    }
+    catch (QuitSuccessException $e) {
+      $this->assertSame(0, $e->getCode());
+      $output = $e->getOutput();
+      $this->assertStringContainsString('Script will exit with code 0', $output);
+      $this->assertStringNotContainsString('ERROR Script continued after quit()', $output);
+    }
   }

Apply the same pattern to the other three script tests, catching QuitErrorException or RuntimeException as appropriate and asserting on $e->getOutput().

This way you genuinely exercise both the exception behavior and the emitted output.

.vortex/tooling/tests/Self/CheckNoExitSelfTest.php (4)

9-13: Update docblock to reference check-no-exit.php

The class docblock still mentions check-exit-usage.php and a generic “exit() usage checker”, but $this->scriptPath points to check-no-exit.php. Aligning the comment with the actual script name/behavior will avoid confusion when tracking which tool these self-tests cover.


29-36: Avoid reusing a fixed $testDir across tests (mkdir warnings and order‑dependent behavior)

setUp() always uses the same $this->testDir = self::$tmp . '/check-exit-test'; and runs mkdir() on it and its subdirectories before every test. With failOnWarning="true" this has two problems:

  • Re-running mkdir() on existing directories will emit warnings and fail the suite.
  • Files created by earlier tests persist into later ones, so testHandlesEmptyDirectories() is not actually running against an empty tree and becomes order‑dependent.

Use a unique temp directory per test and recursive mkdir, e.g.:

   protected function setUp(): void {
     parent::setUp();
     $this->scriptPath = dirname(__DIR__, 2) . '/check-no-exit.php';
-    $this->testDir = self::$tmp . '/check-exit-test';
-    mkdir($this->testDir);
-    mkdir($this->testDir . '/src');
-    mkdir($this->testDir . '/tests');
+    $this->testDir = self::$tmp . '/check-exit-test-' . uniqid('', true);
+    mkdir($this->testDir . '/src', 0777, true);
+    mkdir($this->testDir . '/tests', 0777, true);
   }

This keeps the tests isolated and avoids mkdir‑on‑existing‑dir warnings.

Also applies to: 131-138


47-50: Factor out exec() into a helper and escape shell arguments

Each test builds the same shell command with sprintf('cd %s && php %s 2>&1', $this->testDir, $this->scriptPath). This:

  • Duplicates logic in six places.
  • Does not escape $this->testDir or $this->scriptPath, so paths with spaces/special characters would break.

Refactor into a small helper that uses escapeshellarg() and reuse it, e.g.:

/**
 * Run check-no-exit.php from the current test directory.
 *
 * @return array{0: string[], 1: int}
 */
private function runCheckNoExitScript(): array {
  $output = [];
  $exit_code = 0;

  $cmd = sprintf(
    'cd %s && php %s 2>&1',
    escapeshellarg($this->testDir),
    escapeshellarg($this->scriptPath),
  );
  exec($cmd, $output, $exit_code);

  return [$output, $exit_code];
}

Then replace each inline exec(...) block with:

[$output, $exit_code] = $this->runCheckNoExitScript();

This removes duplication and makes the command robust to unusual paths.

Also applies to: 65-68, 82-84, 103-105, 120-122, 133-135


113-126: Also assert on the non‑zero exit code in testReportsCorrectLineNumbers()

This test only checks that 'line-test.php:5' appears in the output. If the script ever reported the right line but erroneously exited with code 0, the test would still pass.

Tighten it by asserting the exit code as well, similar to your other “exit detected” tests, e.g.:

     $output = [];
     $exit_code = 0;
@@
     exec(sprintf('cd %s && php %s 2>&1', $this->testDir, $this->scriptPath), $output, $exit_code);
 
     $output_str = implode("\n", $output);
+    $this->assertEquals(1, $exit_code);
     $this->assertStringContainsString('line-test.php:5', $output_str);

This keeps behavior consistent across the suite and makes the test more meaningful.

.vortex/tooling/tests/Unit/NotifyRouterTest.php (1)

194-264: Strengthen “default event” and fallback/login tests or rename them

testSuccessfulNotificationWithValidEventTypes(), testDefaultEventTypeIsPostDeployment(), testProjectFallbackToGenericVariable(), and testDefaultLoginUrlGeneration() currently only assert generic success/“Finished dispatching notifications”, even though their names imply they validate the resolved event (post_deployment), project fallback (fallback-project), and derived login URL.

Without asserting on any value that uniquely identifies those behaviors (e.g., an explicit log line or notifier output that includes the event, project, or login URL), regressions in defaulting/fallback logic could slip through while these tests still pass.

Consider either:

  • Extending the router/notifier output or passthru mocks so these tests can assert concrete markers (e.g., resolved event, effective project, generated login URL), or
  • Renaming the tests to reflect the narrower behavior they currently guarantee (successful dispatch under those env conditions).
.vortex/tooling/tests/Unit/NotifySlackTest.php (1)

33-151: Slack payload callbacks are never exercised because mockRequestPost() ignores the $body argument

All the success-path tests that try to validate the Slack JSON payload (testSuccessfulNotificationPostDeployment(), testSuccessfulNotificationPreDeployment(), testSuccessfulNotificationWithCustomMessage(), testSuccessfulNotificationWithChannel(), testSuccessfulNotificationWithCustomBot(), testTokenReplacementInMessage()) pass a $this->callback(...) as the $body argument to mockRequestPost().

However, the current MockTrait::mockRequestPost() implementation only forwards url, method and response to mockRequestMultiple, discarding $body, $headers, and $timeout. As a result, those callbacks are never invoked and the payload assertions inside them never actually run; the tests only verify that some POST request was mocked and that the script’s console output looks right.

To turn these into real payload tests you’ll need to extend the mocking layer, for example:

  • Update mockRequestPost() to pass through body, headers, and timeout into the request definition given to mockRequestMultiple, e.g.:
// In MockTrait.php
protected function mockRequestPost(
    string $url,
    $body = NULL,
    array $headers = [],
    int $timeout = 10,
    ?array $response = NULL,
    string $namespace = 'DrevOps\\VortexTooling',
): void {
    $this->mockRequestMultiple([[
        'url' => $url,
        'method' => 'POST',
        'body' => $body,
        'headers' => $headers,
        'timeout' => $timeout,
        'response' => $response ?? [],
    ]], $namespace);
}
  • Ensure the underlying HTTP stub (used by mockRequestMultiple) detects when 'body' is a PHPUnit callback and calls it with the actual request payload string so your inline assertions execute.

Once that’s wired, these tests will start truly validating payload shape and token replacement rather than just success/failure logs.

Also applies to: 185-252

.vortex/tooling/tests/Unit/NotifyNewrelicTest.php (1)

36-82: New Relic payload/header callbacks are ineffective until mockRequestPost() propagates them

Several tests rely on inspecting the outbound JSON/body or headers via $this->callback(...) passed into mockRequestPost():

  • testSuccessfulNotificationWithProvidedAppId()
  • testSuccessfulNotificationWithAutoGeneratedRevision()
  • testSuccessfulNotificationWithCustomDescription()
  • testSuccessfulNotificationWithCustomChangelog()
  • testNotificationFailureWhenDeploymentCreationFails()
  • testFallbackToGenericVariables()

But, as in the Slack tests, MockTrait::mockRequestPost() currently ignores the $body, $headers, and $timeout parameters and only forwards url, method, and response to mockRequestMultiple. That means:

  • The callbacks are never invoked with the real payload, so assertions on deployment.revision, deployment.user, description, changelog, etc. never actually run.
  • Header expectations (e.g., Api-Key: …) are not validated at the HTTP-mock level.

Once mockRequestPost() and the underlying HTTP stub are updated to carry and consume body/headers (e.g., invoking callbacks with the actual request body), these tests will start exercising the New Relic integration end-to-end rather than just checking console output.

Also applies to: 132-152, 154-174, 176-206, 208-252

.vortex/tooling/tests/Unit/NotifyJiraTest.php (2)

48-56: Request body callback assertions are not executed

The test passes a callback to mockRequestPost() to validate the request payload structure (lines 50-56). However, based on the MockTrait::mockRequestPost() implementation shown in the relevant code snippets, the $body parameter is accepted but never used—it's not forwarded to mockRequestMultiple().

This means:

  • The callback on lines 50-56 (and similar callbacks on lines 118-123, etc.) is never invoked
  • Assertions inside these callbacks never execute
  • Tests provide false confidence about payload validation

To fix, either:

  1. Update MockTrait::mockRequestPost() to capture and validate body via the callback, or
  2. Change tests to validate request bodies through a different mechanism (e.g., by inspecting recorded calls after the fact)

Also applies to: Lines 118-123, and other callback uses throughout the file


333-362: Test doesn't verify custom message rendering

While the test sets VORTEX_NOTIFY_JIRA_MESSAGE with a template containing tokens (line 334), it only asserts that the notification finished successfully (line 361). It doesn't verify that:

  • The custom message was actually rendered
  • Token substitution occurred (%project%, %label%, %timestamp%)
  • The rendered message appears in the output or request payload

Consider adding assertions to verify the custom message template is properly rendered and used.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4836323 and d641987.

📒 Files selected for processing (36)
  • .vortex/tooling/CLAUDE.md (1 hunks)
  • .vortex/tooling/composer.json (1 hunks)
  • .vortex/tooling/phpcs.xml (1 hunks)
  • .vortex/tooling/plan.test-notify.md (1 hunks)
  • .vortex/tooling/scripts_conversion_plan.md (1 hunks)
  • .vortex/tooling/src/helpers.php (1 hunks)
  • .vortex/tooling/src/notify (1 hunks)
  • .vortex/tooling/src/notify-email (1 hunks)
  • .vortex/tooling/src/notify-github (1 hunks)
  • .vortex/tooling/src/notify-jira (1 hunks)
  • .vortex/tooling/src/notify-newrelic (1 hunks)
  • .vortex/tooling/src/notify-slack (1 hunks)
  • .vortex/tooling/src/notify-webhook (1 hunks)
  • .vortex/tooling/tests/Exceptions/QuitErrorException.php (1 hunks)
  • .vortex/tooling/tests/Exceptions/QuitSuccessException.php (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-override-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-override-passing (1 hunks)
  • .vortex/tooling/tests/Self/CheckNoExitSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockPassthruSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockQuitSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/CommandExistsTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/DotenvTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/FormatterTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/GetenvTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyEmailTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyGithubTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyJiraTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyNewrelicTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyRouterTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifySlackTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyWebhookTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/OverrideTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/RequestTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/TokenTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/UnitTestCase.php (1 hunks)
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-09-13T04:14:41.765Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 2011
File: .vortex/tests/phpunit/Traits/Steps/StepAhoyTrait.php:20-23
Timestamp: 2025-09-13T04:14:41.765Z
Learning: In .vortex/tests/phpunit PHPUnit tests using cmd/cmdFail helpers, patterns in the expected output arrays support '*' markers for positive assertions (text must be present) and '!' markers for negative assertions (text must NOT be present). The '*' markers are NOT literal text matches but rather indicate expected presence.

Applied to files:

  • .vortex/tooling/CLAUDE.md
  • .vortex/tooling/tests/Fixtures/test-override-passing
  • .vortex/tooling/tests/Exceptions/QuitErrorException.php
  • .vortex/tooling/phpcs.xml
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
  • .vortex/tooling/tests/Exceptions/QuitSuccessException.php
  • .vortex/tooling/tests/Unit/TokenTest.php
  • .vortex/tooling/tests/Unit/DotenvTest.php
  • .vortex/tooling/tests/Unit/CommandExistsTest.php
  • .vortex/tooling/tests/Unit/NotifyRouterTest.php
  • .vortex/tooling/tests/Unit/GetenvTest.php
  • .vortex/tooling/tests/Unit/OverrideTest.php
  • .vortex/tooling/tests/Unit/NotifyJiraTest.php
  • .vortex/tooling/tests/Self/MockQuitSelfTest.php
  • .vortex/tooling/tests/Self/CheckNoExitSelfTest.php
  • .vortex/tooling/tests/Unit/NotifySlackTest.php
  • .vortex/tooling/tests/Unit/FormatterTest.php
  • .vortex/tooling/tests/Fixtures/test-override-failing
  • .vortex/tooling/tests/Unit/NotifyEmailTest.php
  • .vortex/tooling/tests/Self/MockPassthruSelfTest.php
  • .vortex/tooling/tests/Unit/UnitTestCase.php
📚 Learning: 2025-05-29T12:15:32.188Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 0
File: :0-0
Timestamp: 2025-05-29T12:15:32.188Z
Learning: Do not review files in `.vortex/installer/tests/Fixtures/install` directory as they are test fixtures.

Applied to files:

  • .vortex/tooling/tests/Fixtures/test-override-passing
  • .vortex/tooling/tests/Fixtures/test-override-failing
📚 Learning: 2025-06-01T08:09:18.993Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1693
File: .vortex/installer/tests/Fixtures/install/hosting_acquia/docs/deployment.md:6-8
Timestamp: 2025-06-01T08:09:18.993Z
Learning: Do not review files in test fixture directories (e.g., files under paths containing `/tests/Fixtures/` or similar test fixture patterns). These files are used for testing purposes and should remain as-is.

Applied to files:

  • .vortex/tooling/phpcs.xml
  • .vortex/tooling/tests/Self/CheckNoExitSelfTest.php
📚 Learning: 2025-06-01T08:08:59.071Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1693
File: .vortex/installer/tests/Fixtures/install/deploy_type_none_gha/.github/workflows/build-test-deploy.yml:1-4
Timestamp: 2025-06-01T08:08:59.071Z
Learning: Test fixture files (typically located in directories containing "fixture" or "test") should not be reviewed for code quality issues as they may intentionally contain incomplete, invalid, or malformed data for testing purposes.

Applied to files:

  • .vortex/tooling/phpcs.xml
  • .vortex/tooling/tests/Unit/NotifyEmailTest.php
📚 Learning: 2025-08-08T12:02:24.652Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1896
File: .vortex/tests/bats/unit/download-db-lagoon.bats:24-25
Timestamp: 2025-08-08T12:02:24.652Z
Learning: In .vortex/tests/bats/unit Bats tests using ../_helper.bash (run_steps), prefixing a STEPS entry with "- " denotes a negative assertion (the substring must NOT appear in output). Unprefixed entries are positive assertions. Example: "- Database dump refresh requested. Will create a new dump." asserts absence; "Database dump refresh requested. Will create a new dump." asserts presence.

Applied to files:

  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
  • .vortex/tooling/tests/Fixtures/test-override-failing
📚 Learning: 2025-07-23T01:16:30.963Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1816
File: .docker/cli.dockerfile:72-76
Timestamp: 2025-07-23T01:16:30.963Z
Learning: In the Vortex project, unauthenticated Composer installs should be allowed, so GitHub token secrets should not be marked as `required=true` in Docker build mounts. The conditional check `if [ -s /run/secrets/github_token ]` is the preferred approach to allow builds to proceed without a token when needed.

Applied to files:

  • .vortex/tooling/tests/Unit/NotifyGithubTest.php
🧬 Code graph analysis (22)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (2)
  • __construct (22-31)
  • getOutput (39-41)
.vortex/tooling/tests/Self/MockRequestSelfTest.php (3)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • runScript (38-92)
.vortex/tooling/tests/Traits/MockTrait.php (5)
  • mockRequestGet (405-418)
  • mockRequestPost (436-450)
  • mockRequest (373-389)
  • mockRequestAssertAllMocksConsumed (458-469)
  • mockRequestMultiple (229-359)
.vortex/tooling/src/helpers.php (3)
  • request_get (346-352)
  • request_post (374-381)
  • request (403-460)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (2)
  • __construct (22-31)
  • getOutput (39-41)
.vortex/tooling/tests/Unit/TokenTest.php (1)
.vortex/tooling/src/helpers.php (1)
  • replace_tokens (301-317)
.vortex/tooling/tests/Unit/RequestTest.php (1)
.vortex/tooling/src/helpers.php (3)
  • request_get (346-352)
  • request_post (374-381)
  • request (403-460)
.vortex/tooling/tests/Unit/NotifyGithubTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (4)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockRequestPost (436-450)
  • mockRequestGet (405-418)
.vortex/tooling/tests/Unit/DotenvTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • setUp (21-28)
.vortex/tooling/src/helpers.php (1)
  • load_dotenv (65-99)
.vortex/tooling/tests/Unit/CommandExistsTest.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/src/helpers.php (2)
  • command_exists (283-288)
  • fail (265-268)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/tests/Unit/NotifyRouterTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
  • runScript (38-92)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockPassthru (161-163)
.vortex/tooling/tests/Unit/GetenvTest.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/src/helpers.php (2)
  • getenv_default (124-141)
  • getenv_required (166-187)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/tests/Unit/OverrideTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (4)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/src/helpers.php (1)
  • execute_override (44-57)
.vortex/tooling/tests/Unit/NotifyWebhookTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockRequest (373-389)
.vortex/tooling/tests/Unit/NotifyJiraTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (3)
  • mockRequestGet (405-418)
  • mockRequestPost (436-450)
  • mockRequest (373-389)
.vortex/tooling/tests/Self/MockQuitSelfTest.php (5)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
  • QuitSuccessException (12-43)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • runScript (38-92)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/src/helpers.php (1)
  • quit (481-483)
.vortex/tooling/tests/Self/CheckNoExitSelfTest.php (1)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • setUp (21-28)
.vortex/tooling/tests/Unit/NotifySlackTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (4)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockRequestPost (436-450)
.vortex/tooling/tests/Unit/FormatterTest.php (4)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • setUp (21-28)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (196-212)
.vortex/tooling/src/helpers.php (2)
  • fail (265-268)
  • term_supports_color (273-275)
.vortex/tooling/tests/Unit/NotifyEmailTest.php (1)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/tests/Self/MockPassthruSelfTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • runScript (38-92)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockPassthru (161-163)
  • mockPassthruAssertAllMocksConsumed (171-182)
.vortex/tooling/tests/Unit/NotifyNewrelicTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockRequestPost (436-450)
  • mockRequestGet (405-418)
.vortex/tooling/src/helpers.php (1)
.vortex/installer/src/Utils/Tui.php (3)
  • line (57-59)
  • output (38-43)
  • error (53-55)
.vortex/tooling/tests/Unit/UnitTestCase.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (2)
  • QuitErrorException (12-43)
  • getOutput (39-41)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (2)
  • QuitSuccessException (12-43)
  • getOutput (39-41)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockTearDown (63-81)
  • mockQuit (196-212)
🪛 Gitleaks (8.29.0)
.vortex/tooling/tests/Unit/NotifyGithubTest.php

[high] 24-24: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

.vortex/tooling/tests/Unit/NotifyNewrelicTest.php

[high] 25-25: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 48-48: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 73-73: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 91-91: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 100-100: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 118-118: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 144-144: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 166-166: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 183-183: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 200-200: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 LanguageTool
.vortex/tooling/CLAUDE.md

[grammar] ~230-~230: Ensure spelling is correct
Context: ...ssertAllMocksConsumed(): void #### Passthru Mocking **Response Structure**:php...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

.vortex/tooling/plan.test-notify.md

[style] ~400-~400: To elevate your writing, try using a synonym here.
Context: ...t count per script - Any lines that are hard to cover (with explanation) - Overall c...

(HARD_TO)

🪛 markdownlint-cli2 (0.18.1)
.vortex/tooling/scripts_conversion_plan.md

9-9: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


16-16: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


17-17: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


17-17: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


43-43: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


48-48: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


59-59: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


68-68: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


92-92: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


93-93: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


101-101: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


103-103: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


112-112: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


125-125: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


126-126: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


139-139: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


140-140: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


146-146: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


147-147: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


166-166: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


167-167: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


182-182: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


278-278: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


283-283: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


386-386: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


393-393: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


394-394: Ordered list item prefix
Expected: 1; Actual: 6; Style: 1/2/3

(MD029, ol-prefix)


395-395: Ordered list item prefix
Expected: 2; Actual: 7; Style: 1/2/3

(MD029, ol-prefix)


396-396: Ordered list item prefix
Expected: 3; Actual: 8; Style: 1/2/3

(MD029, ol-prefix)


397-397: Ordered list item prefix
Expected: 4; Actual: 9; Style: 1/2/3

(MD029, ol-prefix)


398-398: Ordered list item prefix
Expected: 5; Actual: 10; Style: 1/2/3

(MD029, ol-prefix)


399-399: Ordered list item prefix
Expected: 6; Actual: 11; Style: 1/2/3

(MD029, ol-prefix)


401-401: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


402-402: Ordered list item prefix
Expected: 1; Actual: 12; Style: 1/2/3

(MD029, ol-prefix)


403-403: Ordered list item prefix
Expected: 2; Actual: 13; Style: 1/2/3

(MD029, ol-prefix)


404-404: Ordered list item prefix
Expected: 3; Actual: 14; Style: 1/2/3

(MD029, ol-prefix)


405-405: Ordered list item prefix
Expected: 4; Actual: 15; Style: 1/2/3

(MD029, ol-prefix)


406-406: Ordered list item prefix
Expected: 5; Actual: 16; Style: 1/2/3

(MD029, ol-prefix)


431-431: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


432-432: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


440-440: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


441-441: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


453-453: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


454-454: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


466-466: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


467-467: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


475-475: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


476-476: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


489-489: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


496-496: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


503-503: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

.vortex/tooling/plan.test-notify.md

53-53: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


83-83: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


129-129: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


160-160: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


218-218: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


233-233: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


243-243: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


261-261: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


306-306: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


400-400: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


407-407: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


412-412: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: build (1)
  • GitHub Check: build (0)
  • GitHub Check: vortex-test-workflow (1)
  • GitHub Check: vortex-test-workflow (4)
  • GitHub Check: vortex-test-workflow (3)
  • GitHub Check: vortex-test-workflow (0)
  • GitHub Check: vortex-test-workflow (2)
  • GitHub Check: vortex-test-common
  • GitHub Check: vortex-test-installer (8.4)
  • GitHub Check: vortex-test-installer (8.3)

quit(0);
}

$channels = array_filter(array_map(trim(...), explode(',', $channels)));
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

Confirm PHP version for first-class callable usage in array_map

array_map(trim(...), ...) relies on first‑class callable syntax, which requires PHP 8.1+. If this tooling is expected to run on older PHP versions, this will fatally error and should be changed to array_map('trim', ...).

Please verify the minimum PHP version declared for the tooling package and align this call accordingly.


🏁 Script executed:

#!/bin/bash
# Find and display composer.json in .vortex/tooling
find .vortex/tooling -name "composer.json" -type f | head -5

Length of output: 29


🏁 Script executed:

#!/bin/bash
# Display the notify file around line 54
if [ -f ".vortex/tooling/src/notify" ]; then
  cat ".vortex/tooling/src/notify" | head -60 | tail -15
fi

Length of output: 302


🏁 Script executed:

#!/bin/bash
# Search for the notify file if the exact path doesn't exist
find . -name "notify*" -path "*/tooling/*" -type f 2>/dev/null | head -10

Length of output: 243


🏁 Script executed:

#!/bin/bash
# Display the composer.json to check PHP version requirement
cat .vortex/tooling/composer.json

Length of output: 1988


Add PHP 8.1+ requirement to composer.json

The code at line 54 uses first-class callable syntax (trim(...)), which requires PHP 8.1+. However, the tooling's composer.json has no "require" section declaring a minimum PHP version. Without this constraint, the package can be installed on incompatible PHP versions and will fatally error at runtime.

Add "php": ">=8.1" to the "require" section in .vortex/tooling/composer.json.

🤖 Prompt for AI Agents
In .vortex/tooling/src/notify around line 54 the code uses the PHP 8.1
first-class callable syntax (trim(...)), but .vortex/tooling/composer.json does
not declare a minimum PHP version; update .vortex/tooling/composer.json to add a
"require" section (or merge into the existing one) including "php": ">=8.1" so
Composer enforces the correct PHP version and prevents installation on
incompatible runtimes.

Comment on lines +23 to +33
$this->envSetMultiple([
'VORTEX_NOTIFY_NEWRELIC_PROJECT' => 'test-project',
'VORTEX_NOTIFY_NEWRELIC_USER_KEY' => 'NRAK-TEST123456',
'VORTEX_NOTIFY_NEWRELIC_LABEL' => 'main',
'VORTEX_NOTIFY_NEWRELIC_ENVIRONMENT_URL' => 'https://example.com',
'VORTEX_NOTIFY_NEWRELIC_LOGIN_URL' => 'https://example.com/login',
'VORTEX_NOTIFY_NEWRELIC_APP_NAME' => 'test-app',
'VORTEX_NOTIFY_NEWRELIC_USER' => 'Deploy Bot',
'VORTEX_NOTIFY_NEWRELIC_EVENT' => 'post_deployment',
'VORTEX_NOTIFY_NEWRELIC_APPID' => '12345678',
]);
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

Static-analysis is flagging the fake New Relic keys; consider obfuscating them

Gitleaks is reporting multiple “Generic API Key” hits on the hardcoded values like NRAK-TEST123456 and NRAK-FALLBACK456 in this test. While these are clearly non-production placeholders, leaving them in this form can still cause CI noise or even hard failures in stricter pipelines.

Options to reduce that friction:

  • Adjust the dummy values so they no longer match common key patterns (e.g., NRAK-00000000000000000000 or NRAK-XXXX-PLACEHOLDER), or
  • Add a targeted allowlist/ignore rule for these specific test files/strings in your gitleaks configuration.

This keeps security tooling happy without losing the readability of the tests.

Also applies to: 231-243

🧰 Tools
🪛 Gitleaks (8.29.0)

[high] 25-25: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
.vortex/tooling/tests/Unit/NotifyNewrelicTest.php lines 23-33 (and similarly
231-243): the test hardcodes New Relic-like API keys that trigger Gitleaks
“Generic API Key” matches; replace those dummy values with non-matching
placeholder patterns (e.g., use clearly non-secret strings like
"NRAK-XXXX-PLACEHOLDER" or "NRAK-00000000000000000000") or alternatively add a
targeted gitleaks allowlist/ignore rule for this test file; update all
occurrences in the file to the chosen approach so static-analysis no longer
flags them.

Comment on lines +36 to +48
$this->callback(function ($body): true {
/** @var array {username: string, icon_emoji: string, attachments: array} $payload */
$payload = json_decode($body, TRUE);
// Verify payload structure.
$this->assertEquals('Deployment Bot', $payload['username']);
$this->assertEquals(':rocket:', $payload['icon_emoji']);
$this->assertEquals('good', $payload['attachments'][0]['color']);
$this->assertStringContainsString('Deployment Complete', $payload['attachments'][0]['title']);
$this->assertEquals('test-project', $this->getFieldValue($payload, 'Deployment'));
return TRUE;
}),
['Content-Type: application/json'],
10,
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Fix array-shape phpdoc syntax so static analysis tools can understand payload structure

The phpdoc comments like:

/** @var array {username: string, icon_emoji: string, attachments: array} $payload */

use array {…} instead of the standard shape syntax array{…} expected by tools such as PHPStan/Psalm. This likely means the annotations are ignored or misparsed.

Consider updating them to valid array-shape syntax, for example:

/** @var array{username: string, icon_emoji: string, attachments: array} $payload */

(and similarly for the other Slack payload docs). This keeps IDE/static-analysis feedback aligned with what the tests are asserting.

Also applies to: 65-72, 88-93, 110-114, 135-140

🤖 Prompt for AI Agents
.vortex/tooling/tests/Unit/NotifySlackTest.php around lines 36-48 (also apply
same change at 65-72, 88-93, 110-114, 135-140): the phpdoc array-shape
annotations use invalid syntax `array { ... }` which static analyzers ignore;
replace each occurrence with the proper shape syntax `array{...}` (e.g. change
`/** @var array {username: string, icon_emoji: string, attachments: array}
$payload */` to `/** @var array{username: string, icon_emoji: string,
attachments: array} $payload */`) so tools like PHPStan/Psalm and IDEs correctly
recognize the payload structure.

Comment on lines +51 to +63
public function testSuccessfulNotificationWithCustomPayload(): void {
$this->envSet('VORTEX_NOTIFY_WEBHOOK_PAYLOAD', '{"custom": "%project%", "info": "%label%"}');

$this->mockRequest(
'https://webhook.example.com/endpoint',
['method' => 'POST'],
['status' => 200, 'body' => '{"success": true}']
);

$output = $this->runScript('src/notify-webhook');

$this->assertStringContainsString('Finished Webhook notification', $output);
}
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

Payload/token-focused tests don’t currently assert on the actual webhook body

testSuccessfulNotificationWithCustomPayload(), testTokenReplacementWithSpecialCharacters(), and testAllTokenReplacements() are named and configured as if they validate the payload content (custom JSON and token expansion), but they only assert that the script finished successfully. They never inspect the body sent to the webhook mock.

To make these tests catch regressions in payload/token logic, consider:

  • Extending the HTTP mocking layer (similar to the Slack tests) so mockRequest() or a dedicated mockRequestPost() variant can capture the outgoing body and optionally invoke a callback, and
  • Asserting on that body (e.g., checking that %project%, %label%, %timestamp%, %environment_url%, %login_url%, and %message% are replaced as expected).

If you prefer to keep these high-level, renaming them to emphasize they only assert successful dispatch (not payload shape) would avoid giving a false sense of coverage.

Also applies to: 175-191, 209-221

🤖 Prompt for AI Agents
.vortex/tooling/tests/Unit/NotifyWebhookTest.php lines 51-63: these tests are
named to validate payload/token replacement but only assert a successful run;
update the test to capture and assert the actual HTTP request body sent to the
webhook by extending the test HTTP mock (or adding a mockRequestPost variant) to
record the POST payload or accept a callback, then assert the JSON body contains
the expected expanded tokens (e.g. values for %project%, %label%, %timestamp%,
%environment_url%, %login_url%, %message%); apply the same pattern to the other
affected tests at lines ~175-191 and ~209-221 so they validate payload shape
instead of only success.

@github-actions github-actions bot temporarily deployed to commit November 23, 2025 21:57 Inactive
@github-actions github-actions bot temporarily deployed to commit November 24, 2025 07:59 Inactive
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

♻️ Duplicate comments (5)
.vortex/tooling/phpcs.xml (2)

17-26: Resolve the redundancy in file specifications (unresolved from past review).

Lines 18–24 redundantly enumerate src/notify-* subdirectories when line 17 already specifies src. Since PHPCS scans directories recursively by default, these explicit subdirectory entries are likely unnecessary. Please either remove them or confirm if they serve a specific purpose (e.g., non-recursive scanning, priority ordering).


33-52: Correct glob patterns in exclude-pattern entries (unresolved from past review).

The patterns use backslash-escaped dots (*.Test\.php, *.TestCase\.php) which are not standard PHPCS glob syntax. PHPCS treats these as literal characters rather than escaped dots. Update the patterns to proper glob format:

  • *.Test\.php*Test.php or **/*Test.php (for recursive matching)
  • *.TestCase\.php*TestCase.php or **/*TestCase.php
  • *.test → Clarify intent; this pattern may not match actual test filenames as written

Apply this diff to fix the glob patterns:

     <!-- Allow long array lines in tests. -->
     <rule ref="Drupal.Arrays.Array.LongLineDeclaration">
-        <exclude-pattern>*.Test\.php</exclude-pattern>
-        <exclude-pattern>*.TestCase\.php</exclude-pattern>
+        <exclude-pattern>*Test.php</exclude-pattern>
+        <exclude-pattern>*TestCase.php</exclude-pattern>
         <exclude-pattern>*.test</exclude-pattern>
     </rule>

     <!-- Allow missing class description in tests. -->
     <rule ref="Drupal.Commenting.ClassComment.Missing">
-        <exclude-pattern>*.Test\.php</exclude-pattern>
-        <exclude-pattern>*.TestCase\.php</exclude-pattern>
+        <exclude-pattern>*Test.php</exclude-pattern>
+        <exclude-pattern>*TestCase.php</exclude-pattern>
         <exclude-pattern>*.test</exclude-pattern>
     </rule>

     <!-- Allow missing function names in tests. -->
     <rule ref="Drupal.Commenting.FunctionComment.Missing">
-        <exclude-pattern>*.Test\.php</exclude-pattern>
-        <exclude-pattern>*.TestCase\.php</exclude-pattern>
+        <exclude-pattern>*Test.php</exclude-pattern>
+        <exclude-pattern>*TestCase.php</exclude-pattern>
         <exclude-pattern>*.test</exclude-pattern>
     </rule>
.vortex/tooling/tests/Traits/MockTrait.php (2)

182-192: Docblock still references exit_code instead of result_code

The past review comment indicated this was addressed, but the docblock at line 189 still says "exit_code: The exit code to set" while the actual array key used in the implementation (line 211) is result_code.

Apply this diff to align the docblock with implementation:

  * @param array<int, array{cmd:string, output?: string, result_code?: int, return?: NULL|FALSE}> $responses
  *   Array of responses to return for each passthru call.
  *   Each response should have:
+ *   - cmd: Expected command (required)
  *   - output: The output to echo
- *   - exit_code: The exit code to set (0 for success).
+ *   - result_code: The exit code to set (optional, default 0)
+ *   - return: Return value (optional, NULL or FALSE)
  * @param string $namespace
  *   Namespace to mock the functions in (defaults to DrevOps\VortexTooling).

428-441: Missing type validation before array merge in mockRequestMultiple()

Past review comments suggested adding validation to ensure $response['response'] is an array before the merge operation at line 433. If a caller passes a non-array value (e.g., string), this will cause a fatal error with "unsupported operand types".

Apply this diff to add validation:

   protected function mockRequestMultiple(array $responses, string $namespace = 'DrevOps\\VortexTooling'): void {
     // Normalize responses by applying defaults before storing.
     $normalized_responses = [];
     foreach ($responses as $response) {
+      if (isset($response['response']) && !is_array($response['response'])) {
+        throw new \InvalidArgumentException('Mocked request response "response" key must be an array.');
+      }
+
       /** @var array{url: string, method?: string, response?: array<string, mixed>} $response */
       $response['response'] = ($response['response'] ?? []) + [
.vortex/tooling/tests/Self/MockRequestSelfTest.php (1)

21-38: Helper loading breaks test isolation

The past review comment identified that helpers.php is loaded only in the first test (line 30), which breaks test isolation. When running individual tests or with randomized test order, later tests will fail with undefined function errors.

Move the helper loading to setUp():

+  protected function setUp(): void {
+    parent::setUp();
+    require_once __DIR__ . '/../../src/helpers.php';
+  }
+
   public function testMockRequestGetSuccess(): void {
     $this->mockRequestGet(
       'https://example.com/api',
       [],
       10,
       ['status' => 200, 'body' => 'success response']
     );
 
-    // Load helpers.php AFTER setting up mocks.
-    require_once __DIR__ . '/../../src/helpers.php';
-
     $result = \DrevOps\VortexTooling\request_get('https://example.com/api');
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cbb45cd and ea5018f.

📒 Files selected for processing (5)
  • .vortex/tooling/phpcs.xml (1 hunks)
  • .vortex/tooling/tests/Self/MockInfrastructureSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockMailSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Traits/MockTrait.php (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-13T04:14:41.765Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 2011
File: .vortex/tests/phpunit/Traits/Steps/StepAhoyTrait.php:20-23
Timestamp: 2025-09-13T04:14:41.765Z
Learning: In .vortex/tests/phpunit PHPUnit tests using cmd/cmdFail helpers, patterns in the expected output arrays support '*' markers for positive assertions (text must be present) and '!' markers for negative assertions (text must NOT be present). The '*' markers are NOT literal text matches but rather indicate expected presence.

Applied to files:

  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
  • .vortex/tooling/phpcs.xml
  • .vortex/tooling/tests/Self/MockInfrastructureSelfTest.php
  • .vortex/tooling/tests/Self/MockMailSelfTest.php
📚 Learning: 2025-08-08T12:02:24.652Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1896
File: .vortex/tests/bats/unit/download-db-lagoon.bats:24-25
Timestamp: 2025-08-08T12:02:24.652Z
Learning: In .vortex/tests/bats/unit Bats tests using ../_helper.bash (run_steps), prefixing a STEPS entry with "- " denotes a negative assertion (the substring must NOT appear in output). Unprefixed entries are positive assertions. Example: "- Database dump refresh requested. Will create a new dump." asserts absence; "Database dump refresh requested. Will create a new dump." asserts presence.

Applied to files:

  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
📚 Learning: 2025-06-01T08:09:18.993Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1693
File: .vortex/installer/tests/Fixtures/install/hosting_acquia/docs/deployment.md:6-8
Timestamp: 2025-06-01T08:09:18.993Z
Learning: Do not review files in test fixture directories (e.g., files under paths containing `/tests/Fixtures/` or similar test fixture patterns). These files are used for testing purposes and should remain as-is.

Applied to files:

  • .vortex/tooling/phpcs.xml
📚 Learning: 2025-06-01T08:08:59.071Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1693
File: .vortex/installer/tests/Fixtures/install/deploy_type_none_gha/.github/workflows/build-test-deploy.yml:1-4
Timestamp: 2025-06-01T08:08:59.071Z
Learning: Test fixture files (typically located in directories containing "fixture" or "test") should not be reviewed for code quality issues as they may intentionally contain incomplete, invalid, or malformed data for testing purposes.

Applied to files:

  • .vortex/tooling/phpcs.xml
🧬 Code graph analysis (4)
.vortex/tooling/tests/Self/MockRequestSelfTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • runScript (38-92)
.vortex/tooling/src/helpers.php (3)
  • request_get (346-352)
  • request_post (374-381)
  • request (403-460)
.vortex/tooling/tests/Self/MockInfrastructureSelfTest.php (1)
.vortex/tooling/tests/Traits/MockTrait.php (3)
  • mockPassthruAssertAllMocksConsumed (263-265)
  • mockMailAssertAllMocksConsumed (409-411)
  • mockRequestAssertAllMocksConsumed (666-668)
.vortex/tooling/tests/Self/MockMailSelfTest.php (1)
.vortex/tooling/tests/Traits/MockTrait.php (3)
  • mockMail (399-401)
  • mockMailMultiple (316-389)
  • mockMailAssertAllMocksConsumed (409-411)
.vortex/tooling/tests/Traits/MockTrait.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
  • QuitSuccessException (12-43)
.vortex/tooling/src/helpers.php (2)
  • fail (265-268)
  • quit (481-483)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: build (0)
  • GitHub Check: build (1)
  • GitHub Check: vortex-test-workflow (3)
  • GitHub Check: vortex-test-workflow (4)
  • GitHub Check: vortex-test-workflow (2)
  • GitHub Check: vortex-test-workflow (0)
  • GitHub Check: vortex-test-workflow (1)
  • GitHub Check: vortex-test-common
  • GitHub Check: vortex-test-installer (8.4)
  • GitHub Check: vortex-test-installer (8.3)
  • GitHub Check: vortex-test-tooling (8.3)
  • GitHub Check: vortex-test-tooling (8.4)
  • GitHub Check: vortex-test-docs
🔇 Additional comments (1)
.vortex/tooling/tests/Self/MockInfrastructureSelfTest.php (1)

18-25: LGTM!

This test correctly validates that mock consumption assertions are safe to call when no mocks have been registered. The use of expectNotToPerformAssertions() is appropriate here.

Comment on lines +18 to +30
public function testMockMailSuccess(): void {
$this->mockMail([
'to' => '[email protected]',
'subject' => 'Test Subject',
'message' => 'Test Message',
]);

require_once __DIR__ . '/../../src/helpers.php';

$result = \DrevOps\VortexTooling\mail('[email protected]', 'Test Subject', 'Test Message', []);

$this->assertTrue($result);
}
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

Helper loading breaks test isolation

Similar to MockRequestSelfTest, this test class loads helpers.php only in the first test (line 25). This breaks test isolation and will cause later tests to fail when run individually or in randomized order.

Move the helper loading to setUp():

+  protected function setUp(): void {
+    parent::setUp();
+    require_once __DIR__ . '/../../src/helpers.php';
+  }
+
   public function testMockMailSuccess(): void {
     $this->mockMail([
       'to' => '[email protected]',
       'subject' => 'Test Subject',
       'message' => 'Test Message',
     ]);
 
-    require_once __DIR__ . '/../../src/helpers.php';
-
     $result = \DrevOps\VortexTooling\mail('[email protected]', 'Test Subject', 'Test Message', []);
📝 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
public function testMockMailSuccess(): void {
$this->mockMail([
'to' => '[email protected]',
'subject' => 'Test Subject',
'message' => 'Test Message',
]);
require_once __DIR__ . '/../../src/helpers.php';
$result = \DrevOps\VortexTooling\mail('[email protected]', 'Test Subject', 'Test Message', []);
$this->assertTrue($result);
}
protected function setUp(): void {
parent::setUp();
require_once __DIR__ . '/../../src/helpers.php';
}
public function testMockMailSuccess(): void {
$this->mockMail([
'to' => '[email protected]',
'subject' => 'Test Subject',
'message' => 'Test Message',
]);
$result = \DrevOps\VortexTooling\mail('[email protected]', 'Test Subject', 'Test Message', []);
$this->assertTrue($result);
}
🤖 Prompt for AI Agents
.vortex/tooling/tests/Self/MockMailSelfTest.php lines 18-30: the test loads
helpers.php inside the test method which breaks isolation and ordering; move the
require_once __DIR__ . '/../../src/helpers.php' into a setUp() method so helpers
are loaded before each test. Add a public function setUp(): void {
parent::setUp(); require_once __DIR__ . '/../../src/helpers.php'; } (or
equivalent in your test base) so every test runs with helpers available and test
ordering does not affect behavior.

@AlexSkrypnyk AlexSkrypnyk force-pushed the feature/1192-tooling-package branch from ea5018f to 4272ff9 Compare November 25, 2025 21:11
@github-actions github-actions bot temporarily deployed to commit November 25, 2025 21:13 Inactive
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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.vortex/CLAUDE.md (1)

759-812: Update “three systems” test-harness section to include the new tooling subsystem

This section still describes .vortex/ as having “three separate systems” (docs, installer, tests) and the tree under it omits .vortex/tooling/, which contradicts the earlier “four distinct subsystems” description and the new tooling section.

Consider:

  • Renaming the heading to reflect four subsystems.
  • Adding a tooling/ entry (and a brief description) to the tree.
  • Updating the bullet list of subsystems to include tooling alongside docs, installer, and template tests.

That will keep this overview consistent with the rest of the document and the new tooling package structure.

♻️ Duplicate comments (49)
.vortex/tooling/.gitattributes (1)

1-2: Remove duplicate .gitattributes export-ignore entry

Lines 1 and 2 are identical; you only need one of them.

-/.gitattributes     export-ignore
 /.gitattributes     export-ignore
 /.gitignore         export-ignore
.vortex/tooling/phpcs.xml (2)

35-37: Verify exclude-pattern glob syntax.

PHPCS uses shell-style globs, not regex. The backslash-escaped patterns like *.Test\.php may not match as intended. Consider using standard glob syntax like *Test.php or **/*Test.php.

#!/bin/bash
# Verify that PHPCS exclude patterns are working as expected
cd .vortex/tooling
# List test files that should be excluded
fd -e php -p 'Test\.php$' tests/
# Run PHPCS on a specific test file to see if rules are excluded
vendor/bin/phpcs --standard=phpcs.xml -s tests/Unit/GetenvTest.php 2>&1 | head -20 || echo "PHPCS not available in sandbox"

17-26: Redundant file entries for recursive scanning.

PHPCS recursively scans directories by default. Since <file>src</file> is already listed, the individual src/notify-* entries on lines 18-24 are redundant.

 <file>src</file>
-<file>src/notify</file>
-<file>src/notify-email</file>
-<file>src/notify-github</file>
-<file>src/notify-jira</file>
-<file>src/notify-newrelic</file>
-<file>src/notify-slack</file>
-<file>src/notify-webhook</file>
 <file>playground</file>
.vortex/tooling/src/notify-webhook (1)

98-102: Consider filtering out empty headers.

When VORTEX_NOTIFY_WEBHOOK_HEADERS contains trailing pipes or consecutive separators, explode('|', ...) produces empty strings. These pass through to request() and may confuse downstream handling.

Apply this diff:

 $response = request($webhook_url, [
   'method' => $webhook_method,
-  'headers' => array_map(trim(...), explode('|', $webhook_headers)),
+  'headers' => array_filter(
+    array_map(trim(...), explode('|', $webhook_headers)),
+    static fn(string $header): bool => $header !== ''
+  ),
   'body' => $webhook_payload,
 ]);
.vortex/tooling/tests/Self/MockRequestSelfTest.php (1)

21-38: Move helpers loading to setUp() for test isolation.

helpers.php is only loaded in testMockRequestGetSuccess(). While require_once ensures it's available for subsequent tests in the same process, this couples the suite to execution order. Tests may fail when run in isolation or with randomized order.

 class MockRequestSelfTest extends UnitTestCase {

+  protected function setUp(): void {
+    parent::setUp();
+    require_once __DIR__ . '/../../src/helpers.php';
+  }
+
   public function testMockRequestGetSuccess(): void {
     $this->mockRequestGet(
       'https://example.com/api',
       [],
       10,
       ['status' => 200, 'body' => 'success response']
     );

-    // Load helpers.php AFTER setting up mocks.
-    require_once __DIR__ . '/../../src/helpers.php';
-
     $result = \DrevOps\VortexTooling\request_get('https://example.com/api');
.vortex/tooling/tests/Unit/GetenvTest.php (3)

137-146: Unreachable assertion after expectException().

With expectException() set and captureOutput() rethrowing the exception, the assertStringContainsString() on line 145 is never executed. The test passes only because the expected exception is thrown, not because the output was verified.

To actually verify output content, catch the exception manually:

 public function testGetenvRequiredWithNoVarsSetFails(): void {
   $this->mockQuit(1);
-  $this->expectException(QuitErrorException::class);
-
-  $output = $this->captureOutput(function (): void {
-    \DrevOps\VortexTooling\getenv_required('UNSET_VAR_1', 'UNSET_VAR_2');
-  });
-
-  $this->assertStringContainsString('Missing required value for UNSET_VAR_1, UNSET_VAR_2', $output);
+  $output = '';
+  try {
+    $output = $this->captureOutput(function (): void {
+      \DrevOps\VortexTooling\getenv_required('UNSET_VAR_1', 'UNSET_VAR_2');
+    });
+    $this->fail('Expected QuitErrorException was not thrown');
+  }
+  catch (QuitErrorException $e) {
+    // Expected exception.
+  }
+  $this->assertStringContainsString('Missing required value for UNSET_VAR_1, UNSET_VAR_2', $output);
 }

148-160: Same unreachable assertion issue.

The assertStringContainsString() on line 159 is unreachable due to the expectException() + captureOutput() combination. Apply the same fix pattern as suggested above.


162-171: Same unreachable assertion issue.

The assertStringContainsString() on line 170 is unreachable. Apply the same fix pattern.

.vortex/tooling/src/notify-slack (2)

132-134: Consider validating Slack response body.

Slack webhooks can return HTTP 200 with an error string in the body (e.g., "invalid_payload", "no_text"). The current check only validates HTTP status, which may mask configuration errors.

 if ($response['status'] !== 200) {
   fail('Unable to send notification to Slack. HTTP status: %s', $response['status']);
 }
+
+$body = trim((string) ($response['body'] ?? ''));
+if ($body !== 'ok' && $body !== '') {
+  fail('Slack API returned error: %s', $body);
+}

111-111: Add error handling for json_encode() failure.

json_encode() can return false on encoding errors (e.g., malformed UTF-8 from environment variables). The current code passes the result directly to request_post() without validation.

 $payload = json_encode($data, JSON_UNESCAPED_SLASHES);
+if ($payload === false) {
+  fail('Failed to JSON-encode Slack payload: %s', json_last_error_msg());
+}
.vortex/tooling/src/notify-newrelic (1)

103-116: Critical: Missing error handling when API request fails.

If $response['ok'] is false (API failure, network error), the code skips the discovery block entirely but continues to line 115, logging "Discovered application ID" with an empty $newrelic_appid. The subsequent deployment POST (line 149) will then fail or send an invalid request.

   if ($response['ok'] && $response['body']) {
     $data = json_decode((string) $response['body'], TRUE);
     if (isset($data['applications'][0]['id'])) {
       $newrelic_appid = $data['applications'][0]['id'];
     }

     if (!$newrelic_appid) {
       pass('Notification skipped: No New Relic application ID found for %s. This is expected for non-configured environments.', $newrelic_app_name);
       quit();
     }
+
+    pass('Discovered application ID: %s', $newrelic_appid);
+  }
+  else {
+    pass('Notification skipped: Unable to discover New Relic application ID for %s. API request failed.', $newrelic_app_name);
+    quit();
   }
-
-  pass('Discovered application ID: %s', $newrelic_appid);
 }
.vortex/tooling/CLAUDE.md (2)

225-228: Mock method naming inconsistency with actual MockTrait API.

Lines 225-228 still reference mockCurl, mockCurlMultiple, and mockCurlAssertAllMocksConsumed, but the actual MockTrait (and lines 804-809 of this document) uses mockRequest* naming. This creates confusion for developers writing tests.

Apply this diff:

-protected function mockCurl(array $response): void
-protected function mockCurlMultiple(array $responses): void
-protected function mockCurlAssertAllMocksConsumed(): void
+protected function mockRequest(string $url, array $options = [], array $response = [], string $namespace = 'DrevOps\\VortexTooling'): void
+protected function mockRequestGet(string $url, array $headers = [], int $timeout = 10, array $response = [], string $namespace = 'DrevOps\\VortexTooling'): void
+protected function mockRequestPost(string $url, $body = NULL, array $headers = [], int $timeout = 10, array $response = [], string $namespace = 'DrevOps\\VortexTooling'): void

817-819: Wrap bare URLs in markdown links to satisfy linting.

Static analysis flags these bare URLs (MD034). Wrap them for consistency and to pass markdownlint:

-## Additional Resources
-
-- PHPUnit Documentation: https://phpunit.de/documentation.html
-- php-mock Documentation: https://github.com/php-mock/php-mock-phpunit
-- Vortex Template: https://github.com/drevops/vortex
+## Additional Resources
+
+- PHPUnit Documentation: <https://phpunit.de/documentation.html>
+- php-mock Documentation: <https://github.com/php-mock/php-mock-phpunit>
+- Vortex Template: <https://github.com/drevops/vortex>
.vortex/tooling/src/notify-github (1)

85-88: Consider adding explicit Content-Type header for JSON payloads.

Both POST requests send JSON but only specify Accept header. Adding Content-Type: application/json makes intent explicit.

   $response = request_post($url, json_encode($payload_data), [
     'Authorization: token ' . $github_token,
     'Accept: application/vnd.github.v3+json',
+    'Content-Type: application/json',
   ]);

Same applies to line 147-150.

.vortex/tooling/rector.php (2)

38-50: Paths exclude src/helpers.php and individual notification scripts.

The withPaths() configuration misses src/helpers.php and lists directories like src/notify which don't exist (scripts are files like src/notify-email, not directories). Consider simplifying:

 ->withPaths([
   __DIR__ . '/check-no-exit.php',
-  __DIR__ . '/src/notify',
-  __DIR__ . '/src/notify-email',
-  __DIR__ . '/src/notify-github',
-  __DIR__ . '/src/notify-jira',
-  __DIR__ . '/src/notify-newrelic',
-  __DIR__ . '/src/notify-slack',
-  __DIR__ . '/src/notify-webhook',
+  __DIR__ . '/src',
   __DIR__ . '/playground',
   __DIR__ . '/tests',
 ])

77-79: PHP version comment doesn't match configuration.

Comment states "modernizes syntax to PHP 8.3" but withPhpSets(php82: TRUE) targets PHP 8.2. Align these to avoid confusion.

Either update the comment to say PHP 8.2, or change to php83: TRUE if 8.3 is intended.

.github/workflows/vortex-test-tooling.yml (1)

56-58: Add comment explaining PACKAGE_TOKEN to GITHUB_TOKEN mapping.

The environment variable GITHUB_TOKEN uses secrets.PACKAGE_TOKEN which can be confusing. If this is intentional (e.g., for elevated privileges), add a comment to clarify.

       env:
+        # Uses PACKAGE_TOKEN for elevated API access (if needed by tests)
         GITHUB_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
.vortex/tooling/src/notify-jira (3)

155-159: ADF doc content must be an array of block nodes.

The Jira ADF schema requires content to be an array. text_to_adf() returns a single paragraph node, but it needs to be wrapped in an array.

 $comment_body = [
   'type' => 'doc',
   'version' => 1,
-  'content' => $jira_message_adf,
+  'content' => [$jira_message_adf],
 ];

218-228: Transition request ignores API response.

The request_post call for transitioning the issue doesn't capture or validate the response. If the transition fails, the script reports success anyway.

-  request_post($url, json_encode($payload_data), [
+  $response = request_post($url, json_encode($payload_data), [
     'Authorization: Basic ' . $token,
     'Content-Type: application/json',
   ]);
 
+  if (!$response['ok']) {
+    fail('Unable to transition issue: %s', $response['body'] ?: 'unknown error');
+  }
+
   pass('Transitioned issue to %s ', $jira_transition);

255-268: Assignee update request ignores API response.

Similar to the transition request, the assignee PUT request doesn't validate the response. Silent failures make troubleshooting difficult.

-  request($url, [
+  $response = request($url, [
     'method' => 'PUT',
     'body' => json_encode($payload_data),
     'headers' => [
       'Authorization: Basic ' . $token,
       'Content-Type: application/json',
     ],
   ]);
 
+  if (!$response['ok']) {
+    fail('Unable to assign issue: %s', $response['body'] ?: 'unknown error');
+  }
+
   pass('Assigned issue to ' . $jira_assignee_email);
.vortex/tooling/tests/Unit/TokenTest.php (1)

32-55: Previous feedback on JSON-escaping edge cases still applies.

A past review suggested adding data provider scenarios for replacement values containing quotes, backslashes, or newlines to lock in the json_encode() escaping behavior in replace_tokens(). The current test cases cover basic scenarios but don't exercise this edge-case behavior.

.vortex/tooling/src/notify-email (4)

119-133: Reuse a single timestamp for subject and body.

The subject (line 122) and message (line 130) each call date() separately, which can drift by a second between calls. A previous review flagged this; compute the timestamp once and reuse it.

+$timestamp = date('d/m/Y H:i:s T');
+
 $email_subject = replace_tokens($email_subject, [
   'project' => $notify_project,
   'label' => $notify_label,
-  'timestamp' => date('d/m/Y H:i:s T'),
+  'timestamp' => $timestamp,
   'environment_url' => $notify_env_url,
   'login_url' => $notify_login_url,
 ]);

 $email_message = replace_tokens($email_message, [
   'project' => $notify_project,
   'label' => $notify_label,
-  'timestamp' => date('d/m/Y H:i:s T'),
+  'timestamp' => $timestamp,
   'environment_url' => $notify_env_url,
   'login_url' => $notify_login_url,
 ]);

159-179: Sanitize header values to prevent email header injection.

Values used in email headers ($email_from, $email_subject, $to, CC/BCC lists) should have CR/LF characters stripped to prevent header injection attacks. While modern PHP's mail() has some protections, explicit sanitization is safer and makes intent clear.

A previous review flagged this concern. Consider adding a sanitization helper:

function sanitize_email_header(string $value): string {
  return str_replace(["\r", "\n", "\0"], '', $value);
}

Then apply it to $email_from, $email_subject, and each recipient before use in headers.


181-190: Consider logging failed deliveries for debugging.

Only successful sends are tracked in $sent. When mail() returns FALSE, the failure is silent. A previous review suggested tracking failed recipients for debugging visibility.


90-107: Filter empty entries and escape quotes in display names.

Two concerns with parse_email_recipients():

  1. After explode() and trim(), empty strings from trailing/extra commas will produce invalid addresses.
  2. If $name contains double quotes, the formatted address "$name" <$email> will be malformed.
 function parse_email_recipients(string $recipients): array {
   if (empty($recipients)) {
     return [];
   }

   $parsed = [];
-  $recipient_list = array_map(trim(...), explode(',', $recipients));
+  $recipient_list = array_filter(
+    array_map(trim(...), explode(',', $recipients)),
+    static fn(string $v): bool => $v !== ''
+  );

   foreach ($recipient_list as $recipient) {
     $parts = array_map(trim(...), explode('|', $recipient, 2));
     $email = $parts[0];
     $name = $parts[1] ?? '';

-    $parsed[] = empty($name) ? $email : sprintf('"%s" <%s>', $name, $email);
+    $parsed[] = empty($name) ? $email : sprintf('"%s" <%s>', addslashes($name), $email);
   }

   return $parsed;
 }
.vortex/tooling/src/notify (2)

54-54: First-class callable syntax requires PHP 8.1+

This line uses trim(...) syntax which requires PHP 8.1 or later. Based on past review comments, this has already been flagged and the composer.json PHP version requirement should be updated accordingly.


66-66: Use DIRECTORY_SEPARATOR for cross-platform compatibility

The script path construction uses a hardcoded forward slash. Based on past review comments, this should use DIRECTORY_SEPARATOR for cross-platform compatibility.

.vortex/tooling/tests/Unit/FormatterTest.php (3)

33-60: Use EnvTrait helpers instead of direct putenv

Line 35 uses putenv('TERM=dumb') directly. Based on past review comments, tests should use $this->envSet('TERM', 'dumb') from EnvTrait to ensure proper tracking and cleanup in tearDown.


62-83: Use EnvTrait helpers instead of direct putenv

Line 64 uses putenv('TERM=xterm-256color') directly. Based on past review comments, this should use $this->envSet('TERM', 'xterm-256color') for proper environment tracking.


100-118: Use EnvTrait helpers instead of direct putenv

Lines 103 and 106 use putenv() directly. Based on past review comments, these should use $this->envSet('TERM', $term_value) and $this->envUnset('TERM') from EnvTrait for proper environment management.

.vortex/tooling/tests/Unit/NotifyGithubTest.php (1)

23-31: Test token value triggers secret scanners

Line 24 uses 'ghp_test123456' which resembles a real GitHub Personal Access Token. Based on past review comments, this should be changed to a value that doesn't match PAT patterns (e.g., 'test-token' or 'ghp_TEST_TOKEN') to avoid triggering secret detection tools like Gitleaks.

.vortex/tooling/tests/Self/MockQuitSelfTest.php (1)

61-107: Output assertions after expectException may still be unreachable

Based on past review comments, these tests were flagged because assertions on $output after expectException are unreachable—once the exception is thrown from runScript(), PHPUnit's expectException handler terminates the test method, and the subsequent assertions never execute.

The past review indicates this was "Addressed in commit 2cde55e", but the current code still shows the same pattern (expectException followed by runScript followed by output assertions). Please confirm whether the fix was applied and is working as intended, or if these assertions remain unreachable.

.vortex/tooling/tests/Self/CheckNoExitSelfTest.php (4)

9-13: Update docblock to reference check-no-exit.php and its behavior

The class comment still points to check-exit-usage.php and a generic “exit() usage checker”, while setUp() targets check-no-exit.php. Please align the docblock text with the actual script name and purpose to avoid confusion.


29-36: Use unique per-test temp directory and recursive mkdir()

$this->testDir is a fixed path under self::$tmp, and mkdir() is called on it each setUp(). This can:

  • Emit warnings when directories already exist (especially with failOnWarning="true"),
  • Leave files from earlier tests in place, so testHandlesEmptyDirectories() is not actually testing an empty tree and becomes order‑dependent.

Create a unique directory per test and use recursive mkdir():

   protected function setUp(): void {
     parent::setUp();
     $this->scriptPath = dirname(__DIR__, 2) . '/check-no-exit.php';
-    $this->testDir = self::$tmp . '/check-exit-test';
-    mkdir($this->testDir);
-    mkdir($this->testDir . '/src');
-    mkdir($this->testDir . '/tests');
+    $this->testDir = self::$tmp . '/check-exit-test-' . uniqid('', true);
+    mkdir($this->testDir . '/src', 0777, true);
+    mkdir($this->testDir . '/tests', 0777, true);
   }

This isolates tests and prevents mkdir warnings.


47-50: Refactor duplicated exec() blocks and escape shell arguments

Each test repeats:

$output = [];
$exit_code = 0;
exec(sprintf('cd %s && php %s 2>&1', $this->testDir, $this->scriptPath), $output, $exit_code);

This:

  • Duplicates logic six times, and
  • Builds a shell command without escaping $this->testDir / $this->scriptPath, which is fragile for paths with spaces or special characters.

Factor this into a helper that uses escapeshellarg():

/**
 * Run check-no-exit.php from the current test directory.
 *
 * @return array{0: string[], 1: int}
 */
private function runCheckNoExitScript(): array {
  $output = [];
  $exit_code = 0;

  $cmd = sprintf(
    'cd %s && php %s 2>&1',
    escapeshellarg($this->testDir),
    escapeshellarg($this->scriptPath),
  );
  exec($cmd, $output, $exit_code);

  return [$output, $exit_code];
}

and then e.g.:

[$output, $exit_code] = $this->runCheckNoExitScript();

in each test.

Also applies to: 65-67, 82-84, 103-105, 120-122, 133-135


113-126: Also assert on non‑zero exit code for the line‑number test

testReportsCorrectLineNumbers() only checks that 'line-test.php:5' appears in the output. If the script ever logs the correct line but exits with code 0, this test would still pass.

Add an exit‑code assertion for parity with your other “exit detected” tests:

   $output_str = implode("\n", $output);
-  $this->assertStringContainsString('line-test.php:5', $output_str);
+  $this->assertEquals(1, $exit_code);
+  $this->assertStringContainsString('line-test.php:5', $output_str);
.vortex/tooling/tests/Unit/NotifyRouterTest.php (2)

102-103: Assert realpath() success before using script paths in mocks

All the success‑path tests call realpath() for notifier scripts and then feed the result into mockPassthru() without verifying it’s non‑false. If a script is missing or misnamed, you’d end up mocking "" instead of surfacing a clear failure.

Consider asserting that realpath() resolved correctly before using the path, e.g.:

-    $script_path = realpath(__DIR__ . '/../../src/notify-email');
+    $script_path = realpath(__DIR__ . '/../../src/notify-email');
+    $this->assertNotFalse($script_path, 'notify-email script path must resolve via realpath().');

and similarly for the Slack/Webhook variants. This makes misconfigured or missing scripts much easier to diagnose in CI.

Also applies to: 119-120, 136-138, 170-172, 201-202, 223-224, 239-240, 255-256, 269-270


219-232: Tighten assertions in default/fallback tests or rename them to match behavior

The following tests currently only assert that notifications finish successfully:

  • testDefaultEventTypeIsPostDeployment()
  • testProjectFallbackToGenericVariable()
  • testDefaultLoginUrlGeneration()

Given their names, you would expect them to verify:

  • That the router actually uses post_deployment as the default event,
  • That the project falls back to VORTEX_PROJECT when VORTEX_NOTIFY_PROJECT is unset,
  • That the login URL is correctly derived when VORTEX_NOTIFY_LOGIN_URL is absent.

Right now they don’t assert on the resolved event/project/login URL at all, which can give a false sense of coverage.

Either:

  • Extend the tests to assert on the effective values (e.g., by checking for specific log lines or markers), or
  • Rename them to reflect that they only assert overall dispatch success.

Also applies to: 234-248, 250-264

.vortex/tooling/tests/Unit/NotifyNewrelicTest.php (1)

23-33: Adjust fake New Relic keys to avoid static-analysis “secret” noise

The hardcoded values:

  • VORTEX_NOTIFY_NEWRELIC_USER_KEY => 'NRAK-TEST123456'
  • NEWRELIC_USER_KEY => 'NRAK-FALLBACK456'

still resemble real New Relic API keys and have been flagged by gitleaks as “Generic API Key”.

To keep security tooling quiet while preserving readability, either:

  • Change them to clearly non‑secret placeholders that don’t match common key patterns (e.g. 'NRAK-XXXX-PLACEHOLDER' or 'NR-TEST-KEY'), and update the corresponding header expectations, or
  • Add a narrow allowlist/ignore rule for these exact strings/lines in your gitleaks config.

This avoids recurring CI noise without weakening actual secret scanning.

Also applies to: 223-233

.vortex/tooling/tests/Unit/NotifyWebhookTest.php (1)

51-63: Payload/token‑focused tests should assert on the actual webhook body or be renamed

The tests:

  • testSuccessfulNotificationWithCustomPayload()
  • testTokenReplacementWithSpecialCharacters()
  • testAllTokenReplacements()

are named as if they validate payload content and token replacement, but they only assert that the script finishes successfully (and, in some cases, on high‑level log lines). They never examine the HTTP request body sent via mockRequest().

To make these tests truly cover payload logic, consider:

  • Extending the HTTP mocking layer to capture the POST body (or accept a callback) and assert on it, similar to how Slack/New Relic tests inspect JSON payloads, or
  • If you prefer to keep them high‑level, renaming the tests to emphasize that they only check successful dispatch, not payload shape.

Also applies to: 175-191, 209-221

.vortex/tooling/tests/Unit/NotifySlackTest.php (2)

33-51: Ensure mockRequestPost() actually exercises the $body callbacks used here

All these tests pass $this->callback(...) as the second argument to mockRequestPost() and then assert heavily on the decoded JSON payload. However, the current MockTrait::mockRequestPost() implementation (per the shared snippet) only forwards url, method, and response to mockRequestMultiple, ignoring $body, $headers, and $timeout.

If that’s still the case, the callbacks provided here are never invoked, and none of the payload assertions actually run — the tests only verify that some POST occurred and returned the mocked status code.

Please update the mocking layer so that:

  • mockRequestPost() includes the provided $body (and headers/timeout) in the request spec passed to mockRequestMultiple, and
  • The underlying stub invokes the callback with the actual request body when it sees a PHPUnit callback there,

so that these tests truly validate the constructed Slack payload.

Also applies to: 60-76, 84-103, 105-125, 127-151, 168-197, 199-208, 212-228, 230-252


37-38: Fix array‑shape phpdoc syntax so static analysis tools understand payload structure

Annotations like:

/** @var array {username: string, icon_emoji: string, attachments: array} $payload */

use array {…} instead of the standard shape syntax array{…} expected by PHPStan/Psalm.

Update them to valid array‑shape syntax, for example:

-/** @var array {username: string, icon_emoji: string, attachments: array} $payload */
+/** @var array{username: string, icon_emoji: string, attachments: array} $payload */

(and similarly for the other occurrences). This lets static analysis and IDEs correctly interpret the payload structure.

Also applies to: 66-67, 90-91, 111-112, 136-137

.vortex/tooling/tests/Self/MockMailSelfTest.php (1)

18-30: Load helpers.php in setUp() instead of only in the first test

Currently only testMockMailSuccess() does:

require_once __DIR__ . '/../../src/helpers.php';

while all tests call \DrevOps\VortexTooling\mail(). This breaks test isolation: running any later test alone (or with different ordering) will fail if helpers.php hasn’t been required yet.

Move the helper loading into a setUp() method and drop the inline require:

 class MockMailSelfTest extends UnitTestCase {

+  protected function setUp(): void {
+    parent::setUp();
+    require_once __DIR__ . '/../../src/helpers.php';
+  }
+
   public function testMockMailSuccess(): void {
     $this->mockMail([
       'to' => '[email protected]',
       'subject' => 'Test Subject',
       'message' => 'Test Message',
     ]);
-
-    require_once __DIR__ . '/../../src/helpers.php';
-
     $result = \DrevOps\VortexTooling\mail('[email protected]', 'Test Subject', 'Test Message', []);

This ensures every test has the helpers available, regardless of execution order.

.vortex/tooling/src/helpers.php (3)

35-57: Harden execute_override() against path traversal and unsafe script names

$path is concatenated directly into $full_path and then passed to passthru(). If a caller ever passes something other than a simple file name (e.g. ../bin/evil.sh), you can traverse out of VORTEX_TOOLING_CUSTOM_DIR and execute arbitrary files.

Consider validating $path before use so it’s a plain name with no separators or ..:

 function execute_override(string $path): void {
-  $dir = getenv('VORTEX_TOOLING_CUSTOM_DIR');
-  if ($dir) {
-    if (!is_dir($dir)) {
-      fail('Custom directory specified in VORTEX_TOOLING_CUSTOM_DIR does not exist: %s', $dir);
-    }
-
-    $full_path = $dir . DIRECTORY_SEPARATOR . $path;
-    if (file_exists($full_path) && is_executable($full_path)) {
-      passthru(sprintf('"%s"', $full_path), $exit_code);
-      quit($exit_code);
-    }
-  }
+  $dir = getenv('VORTEX_TOOLING_CUSTOM_DIR');
+  if (!$dir) {
+    return;
+  }
+
+  if (!is_dir($dir)) {
+    fail('Custom directory specified in VORTEX_TOOLING_CUSTOM_DIR does not exist: %s', $dir);
+  }
+
+  // Prevent path traversal by requiring a simple file name.
+  if (basename($path) !== $path || str_contains($path, '..')) {
+    fail('Invalid script name for override: %s', $path);
+  }
+
+  $full_path = $dir . DIRECTORY_SEPARATOR . $path;
+  if (file_exists($full_path) && is_executable($full_path)) {
+    passthru(sprintf('"%s"', $full_path), $exit_code);
+    quit($exit_code);
+  }
 }

283-288: Escape $command in command_exists() to avoid shell‑injection risk

exec(sprintf('command -v %s 2>/dev/null', $command), ...) interpolates $command directly into a shell string. If $command is ever influenced by user input, this can be used for command injection.

Use escapeshellarg() when composing the shell command:

 function command_exists(string $command): void {
-  exec(sprintf('command -v %s 2>/dev/null', $command), $output, $code);
+  exec(sprintf('command -v %s 2>/dev/null', escapeshellarg($command)), $output, $code);
   if ($code !== 0) {
     fail(sprintf("Command '%s' is not available", $command));
   }
 }

403-460: Add error handling for curl_init() and curl_setopt_array() failures in request()

curl_init($url) may return false, and curl_setopt_array() may fail. In both cases the code currently continues, which can lead to a TypeError (passing false as a CurlHandle) or a misconfigured request with confusing downstream errors.

Consider failing early when either step fails:

 function request(string $url, array $options = []): array {
@@
-  $ch = curl_init($url);
+  $ch = curl_init($url);
+  if ($ch === FALSE) {
+    fail('Failed to initialize curl for URL: %s', $url);
+  }
@@
-    curl_setopt_array($ch, $opts);
+    if (curl_setopt_array($ch, $opts) === FALSE) {
+      // Try to surface a useful error message if available.
+      $error_msg = function_exists('curl_error') ? curl_error($ch) : 'unknown cURL error';
+      fail('Failed to set curl options for URL %s: %s', $url, $error_msg);
+    }
@@
-  }
-  finally {
-    // CurlHandle objects are automatically freed when they go out of scope
-    // (PHP 8.0+), so explicit curl_close() is no longer needed.
-    // The unset here ensures the handle goes out of scope immediately.
-    unset($ch);
-  }
+  }
+  finally {
+    // CurlHandle objects are automatically freed when they go out of scope
+    // (PHP 8.0+), so explicit curl_close() is no longer needed.
+    // The unset here ensures the handle goes out of scope immediately.
+    unset($ch);
+  }
 }
.vortex/tooling/tests/Unit/NotifyJiraTest.php (1)

35-131: Body‑inspection callbacks depend on MockTrait support that isn’t implemented yet

Tests like testSuccessfulNotificationCommentOnly() and testSuccessfulNotificationWithTransition() pass $this->callback(...) as the second argument to mockRequestPost() to inspect the JSON payload. However, the current MockTrait::mockRequestPost() implementation ignores its $body parameter and only enqueues URL/method/response, so these callbacks are never actually evaluated.

Until MockTrait::mockRequestPost() / mockRequestMultiple() are extended to capture the request body from curl_setopt_array() and invoke the configured matcher, these tests validate URLs and status codes but not the actual Jira payload structure.

.vortex/tooling/tests/Traits/MockTrait.php (2)

183-195: Align passthru mock docblock with actual response shape (result_code, not exit_code)

The docblock for mockPassthruMultiple() still refers to an exit_code key, but the type and implementation use result_code. This mismatch can confuse callers constructing responses.

Suggest updating the narrative to match the actual shape, e.g.:

- *   Each response should have:
- *   - output: The output to echo
- *   - exit_code: The exit code to set (0 for success).
+ *   Each response should have:
+ *   - cmd: Expected command (required)
+ *   - output: Output to echo (optional)
+ *   - result_code: Exit code to set (optional, defaults to 0)
+ *   - return: Return value (optional, NULL or FALSE)

413-657: mockRequestPost() ignores $body, so request‑payload assertions in tests never run

The HTTP‑request mocking stack works off a per‑request response queue, but it currently never exposes or validates the request body:

  • mockRequestMultiple() stores url, optional method, and normalized response, but no body or matcher.
  • curl_setopt_array’s mock only records CURLOPT_CUSTOMREQUEST into $current_method; it doesn’t capture CURLOPT_POSTFIELDS.
  • curl_exec’s mock reads $mock['response']['body'] and returns it, but never inspects the outgoing payload.
  • mockRequestPost() accepts $body but discards it when calling mockRequestMultiple().

As a result, tests that pass callbacks in $body (e.g., the Jira tests using $this->callback(function ($body) { … })) never have those callbacks invoked, so payload structure/content isn’t actually verified.

Consider wiring body validation through the mock layer, for example:

  • In mockRequestMultiple(), track an optional body / bodyMatcher field alongside url and method.
  • In the mocked curl_setopt_array(), capture CURLOPT_POSTFIELDS into a $current_body variable.
  • In curl_exec()’s callback, after validating URL/method, if a bodyMatcher is present:
    • If it’s a callable, invoke it with $current_body.
    • Or, if it’s a literal expected string, compare it directly to $current_body and throw on mismatch.
  • In mockRequestPost() / mockRequest(), pass the $body (or a matcher closure) into that field instead of dropping it.

That will make the notifier tests’ payload assertions meaningful and avoid a false sense of coverage.

Comment on lines +44 to +49
├── composer.json # Package configuration
├── phpunit.xml # PHPUnit configuration
├── CLAUDE.md # Development guide
├── scripts_conversion_plan.md # This file
├── src/
│ ├── helpers.php # ✅ Core helper functions (17 functions)
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Optional: address markdownlint bare-URL warning

markdownlint is flagging a bare URL (e.g., https://www.vortextemplate.com). If you want a clean lint run, wrap it as a proper Markdown/autolink, for example:

Published to <https://www.vortextemplate.com>

or

Published to [vortextemplate.com](https://www.vortextemplate.com)
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

47-47: Bare URL used

(MD034, no-bare-urls)

🤖 Prompt for AI Agents
In .vortex/tooling/scripts_conversion_plan.md around lines 44 to 49 there is a
bare URL flagged by markdownlint; update the markdown to avoid a bare URL by
converting it to an autolink (<https://www.vortextemplate.com>) or a named link
([vortextemplate.com](https://www.vortextemplate.com)) wherever the URL appears
so the linter no longer flags it.

];

$url = sprintf('%s/applications/%s/deployments.json', $newrelic_endpoint, $newrelic_appid);
$response = request_post($url, json_encode($payload_data, JSON_UNESCAPED_SLASHES), [
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

Add error handling for json_encode() failure.

Similar to the Slack notifier, json_encode() can return false on encoding errors. The result is passed directly to request_post() without validation.

-$response = request_post($url, json_encode($payload_data, JSON_UNESCAPED_SLASHES), [
+$payload = json_encode($payload_data, JSON_UNESCAPED_SLASHES);
+if ($payload === false) {
+  fail('Failed to JSON-encode New Relic payload: %s', json_last_error_msg());
+}
+
+$response = request_post($url, $payload, [
   'Api-Key: ' . $newrelic_user_key,
   'Content-Type: application/json',
 ]);
🤖 Prompt for AI Agents
In .vortex/tooling/src/notify-newrelic around line 150, the code calls
json_encode() inline and passes its result directly to request_post() which can
receive false on encoding failure; change this to first assign json_encode(...)
to a variable, check if the result is === false, and on failure log or handle
the error (include json_last_error_msg() in the log) and abort/return an error
before calling request_post(); only call request_post() with the validated
encoded string when json_encode() succeeded.

Comment on lines +106 to +118
public function testExecuteOverrideWithFileInsteadOfDir(): void {
$custom_dir = self::$tmp . '/custom';
File::dump($custom_dir);
$custom_dir = self::$tmp . '/custom';

$this->envSet('VORTEX_TOOLING_CUSTOM_DIR', $custom_dir);

$script_path = $custom_dir . '/test-override-passing';
file_put_contents($script_path, "#!/usr/bin/env php\n<?php\necho 'override executed';");
chmod($script_path, 0755);

$this->runScriptError('tests/Fixtures/test-override-passing', sprintf('Custom directory specified in VORTEX_TOOLING_CUSTOM_DIR does not exist: %s', $custom_dir));
}
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

Avoid creating a file under a path that is itself a file in testExecuteOverrideWithFileInsteadOfDir()

Here:

$custom_dir = self::$tmp . '/custom';
File::dump($custom_dir);          // creates a file at $custom_dir
$custom_dir = self::$tmp . '/custom';

$this->envSet('VORTEX_TOOLING_CUSTOM_DIR', $custom_dir);

$script_path = $custom_dir . '/test-override-passing';
file_put_contents($script_path, "#!/usr/bin/env php\n<?php\necho 'override executed';");
chmod($script_path, 0755);

File::dump($custom_dir) makes $custom_dir a file. Writing $custom_dir . '/test-override-passing' then targets a child path under a file, which can trigger filesystem warnings and is not needed for this scenario.

Since execute_override() should fail as soon as it sees that VORTEX_TOOLING_CUSTOM_DIR is not a directory, you can safely drop the script creation:

   $custom_dir = self::$tmp . '/custom';
   File::dump($custom_dir);
   $custom_dir = self::$tmp . '/custom';

   $this->envSet('VORTEX_TOOLING_CUSTOM_DIR', $custom_dir);
-
-  $script_path = $custom_dir . '/test-override-passing';
-  file_put_contents($script_path, "#!/usr/bin/env php\n<?php\necho 'override executed';");
-  chmod($script_path, 0755);

This keeps the test focused and avoids spurious I/O warnings.

🤖 Prompt for AI Agents
.vortex/tooling/tests/Unit/OverrideTest.php lines 106-118: the test creates a
file at the custom-dir path with File::dump(), then attempts to create a child
script under that path which is invalid because the parent is a file; remove the
creation of the child script (file_put_contents and chmod for $script_path) so
the test only sets VORTEX_TOOLING_CUSTOM_DIR to a file and asserts
execute_override() fails due to the path not being a directory.

- Documented all 17 helper functions (486 lines)
- Documented completed notify router (80 lines, 20 tests)
- Documented all 6 notification channel scripts (1,097 lines, 130 tests)
- Added current test statistics (247 tests, 1,258 assertions)
- Updated package structure with all completed files
- Added detailed feature lists for each notify script
- Documented test coverage for each component
- Added key learnings and best practices sections
- Updated timeline estimates based on progress
- Included conversion patterns and coding standards
- Added mail mock infrastructure to MockTrait with mockMail(), mockMailMultiple(), and validation
- Converted src/notify-email from system mail/sendmail commands to PHP's native mail() function
- Reduced notify-email from 172 to 133 lines by removing command availability checking
- Rewrote NotifyEmailTest.php with 16 comprehensive tests using the new mail mock
- Fixed helpers.php replace_tokens() to use JSON_UNESCAPED_SLASHES preventing URL escaping
- All 250 tests pass with 1,260 assertions and 100% code coverage maintained
- Added VORTEX_NOTIFY_EMAIL_CC and VORTEX_NOTIFY_EMAIL_BCC environment variables
- Updated mail mock infrastructure to validate headers (both array and string formats)
- Parse CC/BCC recipients with same format as TO recipients (comma-separated with optional names)
- Added Cc and Bcc headers to mail() calls when CC/BCC recipients are specified
- Display CC/BCC in notification summary output
- Added 8 comprehensive tests covering single/multiple CC/BCC scenarios
- All 257 tests pass with 1,283 assertions and 100% code coverage maintained
@AlexSkrypnyk AlexSkrypnyk force-pushed the feature/1192-tooling-package branch from 4272ff9 to fddfa28 Compare November 26, 2025 21:48
@github-actions github-actions bot temporarily deployed to commit November 26, 2025 21:50 Inactive
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: 12

♻️ Duplicate comments (46)
.vortex/tooling/phpcs.xml (1)

34-52: Re‑verify exclude-pattern globs actually match your test files

The exclude-pattern entries like *.Test\.php, *.TestCase\.php, and *.test are intended to relax rules for tests, but PHPCS patterns are glob‑style, not regex, so backslash‑escaping dots will not behave like a regex and may fail to match *Test.php/*TestCase.php as expected. It’s worth double‑checking that these patterns really hit your tests/** files and, if not, switching to plain glob forms such as *Test.php, *TestCase.php, or **/*Test.php.

You can confirm by running PHPCS against a known test file and checking whether the Drupal sniffs are actually skipped there.

.vortex/tooling/scripts_conversion_plan.md (1)

576-618: Keep composer.json snippet in sync with the actual tooling manifest (and optionally clean up markdownlint noise)

The “Package Setup (composer.json)” block is labeled as the “Current configuration” but it no longer matches .vortex/tooling/composer.json (e.g. it omits dealerdirect/phpcodesniffer-composer-installer, ergebnis/composer-normalize, squizlabs/php_codesniffer, and the config.allow-plugins section). Either update the snippet to mirror the real file or explicitly mark it as a trimmed example to avoid confusing readers.

Separately, markdownlint still reports MD0xx issues here (blank lines around headings/fences, ordered list prefixes). If you want a clean lint run, you can re-run the repo’s markdown formatter (e.g., cd .vortex && ahoy lint-markdown-fix) or adjust the headings and fenced blocks manually.

.vortex/tooling/CLAUDE.md (1)

47-99: Sync documented helper API (validation/env helpers) with helpers.php, and optionally tidy markdownlint issues

The “Core Helper Functions” section still documents validate_variable() / validate_command() as the validation API, while the conversion plan and helpers summary describe getenv_default(), getenv_required(), command_exists(), and term_supports_color() as the canonical helpers. To avoid confusion for contributors, I’d align this section with whatever src/helpers.php actually exposes now (including execute_override(), the getenv helpers, and validation helpers), and drop or clearly mark any legacy names if they’re no longer part of the public surface.

Separately, markdownlint still flags a few minor issues here (blank lines around fenced blocks and headings, ordered list numbering, and bare URLs for PHPUnit/php-mock/Vortex links). If CI cares about markdownlint, wrapping those URLs in <...>/links and adding blank lines around code fences will clear the warnings.

.vortex/tooling/.gitattributes (1)

1-2: Remove duplicate .gitattributes entry.

Lines 1 and 2 both contain the identical entry /.gitattributes export-ignore. One of these duplicate lines should be removed.

.vortex/tooling/src/notify-webhook (2)

16-17: Remove blank lines after separator comments to satisfy PHPCS.

The // ----------------------------------------------------------------------------- separators are immediately followed by blank lines, which PHPCS flags (Drupal.Commenting.InlineComment.SpacingAfter). Remove the blank lines on Lines 17 and 64 so the separator comments are directly followed by code or the next comment.

Also applies to: 63-64


98-102: Filter out empty headers when parsing VORTEX_NOTIFY_WEBHOOK_HEADERS.

explode('|', $webhook_headers) can yield empty strings (e.g., trailing or double pipes), which then get passed to request() as blank headers. Cheaply trim and filter out empty values before sending the request.

-$response = request($webhook_url, [
-  'method' => $webhook_method,
-  'headers' => array_map(trim(...), explode('|', $webhook_headers)),
-  'body' => $webhook_payload,
-]);
+$response = request($webhook_url, [
+  'method' => $webhook_method,
+  'headers' => array_filter(
+    array_map(trim(...), explode('|', $webhook_headers)),
+    static fn(string $header): bool => $header !== ''
+  ),
+  'body' => $webhook_payload,
+]);
.vortex/tooling/src/notify-github (2)

64-65: Consider relaxing the upper bound on deployment ID length.

The hard limit of 11 digits may reject future larger GitHub deployment IDs. While the numeric check is valuable, consider removing the upper bound constraint or making it more generous.


85-88: Missing Content-Type header in JSON POST requests.

Both deployment creation (line 85) and status update (line 147) POST requests send JSON bodies but omit the Content-Type: application/json header. While GitHub's API may handle this, setting it explicitly is best practice.

Also applies to: 147-150

.github/workflows/vortex-test-tooling.yml (1)

57-57: GITHUB_TOKEN environment variable uses PACKAGE_TOKEN secret.

The workflow maps secrets.PACKAGE_TOKEN to GITHUB_TOKEN, which may be confusing. Consider documenting why elevated privileges are needed or renaming for clarity.

.vortex/tooling/tests/Self/MockQuitSelfTest.php (1)

61-107: Output assertions after expectException are unreachable.

In script-based tests (lines 61-107), assertions on $output (lines 69-70, 81-82, 93-94, 105-106) will never execute because PHPUnit's expectException causes the test to terminate when the exception is thrown from runScript().

While a past comment indicated this was addressed in commit 2cde55e, the pattern still appears present. To validate both the exception and output, wrap runScript() in a manual try/catch block instead of using expectException.

.vortex/tooling/playground/env.php (1)

74-91: Consider expanding test coverage for additional variable types.

The demo could verify additional variable types like PLAYGROUND_MULTIWORD, PLAYGROUND_NUMERIC, and PLAYGROUND_BOOL_LIKE to demonstrate quoting, numeric, and boolean-like parsing. This would strengthen the demo coverage.

.vortex/tooling/tests/Unit/TokenTest.php (1)

32-55: Add test cases for JSON-escaping edge values.

The data provider lacks scenarios testing special characters that require JSON escaping (quotes, backslashes, newlines). Adding these would help catch regressions in the json_encode() escaping logic.

.vortex/tooling/src/notify (2)

54-54: First-class callable syntax requires PHP 8.1+

This uses trim(...) first-class callable syntax introduced in PHP 8.1. Ensure the package's composer.json declares "php": ">=8.1" in the require section to prevent installation on incompatible runtimes.


66-66: Consider using DIRECTORY_SEPARATOR for cross-platform compatibility.

The script path uses a hardcoded forward slash which may cause issues on Windows.

-  $script = __DIR__ . ('/notify-' . $channel);
+  $script = __DIR__ . DIRECTORY_SEPARATOR . 'notify-' . $channel;
.vortex/tooling/src/notify-jira (4)

168-168: Logging user email may expose PII.

note('User email : ' . $jira_user_email); logs a concrete user identifier. Depending on compliance requirements (GDPR, etc.), consider masking or omitting this from standard output.


220-227: Transition request ignores API response.

The request_post call for transitioning the issue doesn't check the response. If Jira returns a 4xx/5xx error, the script will incorrectly report success. Consider validating $response['ok'] before logging success.


257-268: Assignee PUT request ignores API response.

Similar to the transition request, the assignee update via request() doesn't validate the response. The script will report success even if the API call failed.


155-159: ADF content must be an array of block nodes.

The Jira ADF schema requires content at the document level to be an array of block nodes. Currently, $jira_message_adf (a single paragraph node) is assigned directly, which violates the schema and may cause API rejections.

 $comment_body = [
   'type' => 'doc',
   'version' => 1,
-  'content' => $jira_message_adf,
+  'content' => [$jira_message_adf],
 ];
.vortex/tooling/rector.php (1)

38-79: Past review concerns about Rector paths and PHP version remain unaddressed.

The earlier review identified two issues:

  1. withPaths() excludes src/helpers.php, which exists and should be modernized
  2. Comments reference PHP 8.3 while the configuration uses php82: TRUE

These concerns were marked as addressed but the code has not changed. Please either:

  • Update withPaths() to [__DIR__ . '/src', __DIR__ . '/tests'] to include all source files
  • Align the PHP version by updating the comment to say "PHP 8.2" or changing the config to php83: TRUE
.vortex/tooling/tests/Unit/NotifyGithubTest.php (1)

24-24: Test token still triggers Gitleaks secret detection.

The hardcoded value 'ghp_test123456' continues to match GitHub's PAT prefix pattern and is flagged by static analysis. The earlier review suggested using a clearly fake value (e.g., 'test-token' or 'ghp_TEST_TOKEN') to avoid tripping secret scanners while keeping the test valid.

.vortex/tooling/tests/Unit/FormatterTest.php (5)

35-35: Direct putenv() call bypasses EnvTrait cleanup.

The earlier review identified that putenv('TERM=dumb') should be replaced with $this->envSet('TERM', 'dumb') to ensure proper cleanup via EnvTrait. This change was marked as addressed but the code still uses putenv() directly.


64-64: Direct putenv() call bypasses EnvTrait cleanup.

Similar to line 35, this putenv('TERM=xterm-256color') should use $this->envSet('TERM', 'xterm-256color') instead.


102-107: Direct putenv() calls bypass EnvTrait cleanup.

Lines 103 and 106 use putenv('TERM') and putenv('TERM=' . $term_value) directly. Replace with:

  • $this->envUnset('TERM') when $term_value === FALSE
  • $this->envSet('TERM', $term_value) otherwise

77-82: Missing test coverage for fail_no_exit with color support.

The data provider dataProviderOutputFormattersWithColor omits the fail_no_exit formatter, which supports colored output (red ANSI codes). This was noted in an earlier review as incomplete test coverage.


113-117: Missing positive test case for color support.

The data provider dataProviderTermSupportsColor only includes cases where color support is FALSE (dumb terminal, no terminal). A positive case (e.g., 'xterm-256color' => [..., 'expected' => TRUE]) is needed to verify that term_supports_color() correctly returns TRUE for color-capable terminals.

.vortex/tooling/tests/Self/CheckNoExitSelfTest.php (3)

29-36: Fixed temporary directory path causes mkdir warnings and test isolation issues.

The earlier review identified that setUp() reuses a fixed $testDir path, leading to:

  1. mkdir() warnings on subsequent test runs (which fail tests due to failOnWarning="true")
  2. File persistence between tests causing order-dependency

The suggested fix was to use uniqid() and recursive mkdir():

$this->testDir = self::$tmp . '/check-exit-test-' . uniqid('', true);
mkdir($this->testDir . '/src', 0777, true);
mkdir($this->testDir . '/tests', 0777, true);

47-50: Shell arguments not escaped in exec() calls.

The exec() command builds a shell string without escaping $this->testDir or $this->scriptPath. The earlier review suggested creating a helper method using escapeshellarg() to avoid issues with paths containing spaces or special characters. This pattern repeats at lines 65-68, 82-84, 103-105, 120-122, and 133-135.


113-126: Missing exit code assertion in line-number test.

The test testReportsCorrectLineNumbers() verifies that the output contains 'line-test.php:5' but doesn't assert that $exit_code is non-zero. The earlier review noted this should match the pattern used in other "exit detected" tests.

.vortex/tooling/tests/Unit/OverrideTest.php (1)

106-118: Avoid creating a child path under a file.

The earlier review identified that after File::dump($custom_dir) makes $custom_dir a file, attempting to create $custom_dir . '/test-override-passing' targets a child path under a file, which triggers filesystem warnings. Since execute_override() should fail when it detects the custom dir is not a directory, the script creation (lines 113-115) can be removed.

.vortex/tooling/src/notify-newrelic (2)

103-107: Missing error handling for json_decode().

The earlier review noted that line 104 calls json_decode() without validating the result. If the API response contains invalid JSON, $data could be null, leading to issues when accessing $data['applications'][0]['id']. The suggested fix was to check $data && isset($data['applications'][0]['id']) before accessing array keys.


150-153: Missing error handling for json_encode().

Line 150 calls json_encode() inline and passes the result directly to request_post(). If encoding fails, json_encode() returns false, which would be passed as the request body. The earlier review suggested assigning the result to a variable, checking for === false, and handling the error with json_last_error_msg() before calling request_post().

.vortex/tooling/tests/Unit/NotifyNewrelicTest.php (2)

25-25: Test New Relic keys trigger static analysis warnings.

The hardcoded value 'NRAK-TEST123456' matches New Relic API key patterns and is flagged by Gitleaks. The earlier review suggested using clearly non-secret placeholder patterns (e.g., 'NRAK-XXXX-PLACEHOLDER' or 'NRAK-00000000000000000000') or adding a targeted allowlist rule to prevent CI noise.


232-232: Test New Relic keys trigger static analysis warnings.

Similar to line 25, 'NRAK-FALLBACK456' matches New Relic API key patterns. Consider using a non-matching placeholder pattern.

.vortex/tooling/tests/Unit/NotifyWebhookTest.php (1)

51-63: Payload/token-focused tests don't assert on the actual webhook body.

These tests (testSuccessfulNotificationWithCustomPayload, testTokenReplacementWithSpecialCharacters, testAllTokenReplacements) only verify successful script completion but don't validate that the payload content or token replacements are correct. Consider extending mockRequest() to capture and verify the outgoing request body.

Also applies to: 175-191, 209-221

.vortex/tooling/tests/Unit/NotifyRouterTest.php (3)

48-62: Event type coverage is good, but the "default is post_deployment" test only asserts success.

The invalid event type data provider (Lines 55-62) covers a good range, but testDefaultEventTypeIsPostDeployment() (Lines 219-232) doesn't verify that the default event used is actually post_deployment. Consider asserting on the resolved event value in the output.


87-97: Consider asserting realpath() success before use.

If notify-email script were missing or misnamed, realpath() would return false, causing the mock to be set up with an empty string command. This could lead to confusing test failures.


234-264: Fallback/login tests don't validate the actual fallback behavior.

testProjectFallbackToGenericVariable() and testDefaultLoginUrlGeneration() only assert that notifications "Finished dispatching" but don't verify that the router actually used the generic project variable or derived login URL. Consider asserting on output containing the expected fallback values.

.vortex/tooling/tests/Unit/NotifySlackTest.php (2)

33-58: The mockRequestPost() $body callback is not consumed by the mock implementation.

The MockTrait::mockRequestPost() method (shown in relevant snippets) only forwards url, method, and response to mockRequestMultiple(). The $body callback argument (Lines 36-46) is never invoked, so these payload assertions do not actually run. The mocking layer needs to be updated to propagate and invoke the body callback.


36-48: Fix array-shape phpdoc syntax for static analysis compatibility.

The annotations use array {…} instead of array{…}. Tools like PHPStan/Psalm expect the latter syntax without a space.

Also applies to: 65-72, 88-93, 110-114, 135-140

.vortex/tooling/tests/Self/MockMailSelfTest.php (1)

18-30: Helper loading breaks test isolation

The require_once for helpers.php is only in the first test method (line 25). Other tests like testMockMailWithHeadersSuccess call \DrevOps\VortexTooling\mail() without loading the helpers first. If tests run in isolation or randomized order, they will fail because the mail() function won't be defined.

Move the helper loading to setUp():

+  protected function setUp(): void {
+    parent::setUp();
+    require_once __DIR__ . '/../../src/helpers.php';
+  }
+
   public function testMockMailSuccess(): void {
     $this->mockMail([
       'to' => '[email protected]',
       'subject' => 'Test Subject',
       'message' => 'Test Message',
     ]);
 
-    require_once __DIR__ . '/../../src/helpers.php';
-
     $result = \DrevOps\VortexTooling\mail('[email protected]', 'Test Subject', 'Test Message', []);
.vortex/tooling/tests/Unit/NotifyJiraTest.php (2)

35-73: Callbacks passed to mockRequestPost() are not validated by the mock layer

The callback passed as $body argument (lines 50-56) is intended to validate the request payload, but based on the MockTrait::mockRequestPost() implementation shown in the relevant code snippets, the $body parameter is not forwarded to mockRequestMultiple(). The mock only stores url, method, and response.

As a result, the payload assertions inside the callback (lines 52-54) never execute, and the test doesn't actually validate the comment body structure.

Consider either:

  1. Extending mockRequestPost() to support a body matcher/validator
  2. Removing the callbacks since they provide false confidence
  3. Using a different approach to validate request payloads

333-362: Custom message test doesn't verify the rendered message

This test sets VORTEX_NOTIFY_JIRA_MESSAGE to a templated string but only asserts that the notification finished successfully. It doesn't verify that the custom message was actually rendered with the tokens replaced and used in the Jira comment.

Consider asserting on the output or extending the mock to capture and validate the rendered message content.

.vortex/tooling/src/helpers.php (3)

44-57: Add path traversal protection to execute_override()

The $path parameter is concatenated directly into a file path without validation. While current usage may pass safe values, this creates a potential path traversal vulnerability if $path ever comes from less trusted sources.

Consider validating that $path doesn't contain path traversal patterns:

 function execute_override(string $path): void {
+  // Prevent path traversal.
+  if (str_contains($path, '..') || str_starts_with($path, '/') || str_starts_with($path, '\\')) {
+    fail('Invalid script path for override: %s', $path);
+  }
+
   $dir = getenv('VORTEX_TOOLING_CUSTOM_DIR');

283-288: Shell-escape $command in command_exists()

The $command parameter is interpolated directly into a shell string without escaping. While current usage may pass only safe constants, this is a command injection risk if the function is ever called with user-influenced input.

 function command_exists(string $command): void {
-  exec(sprintf('command -v %s 2>/dev/null', $command), $output, $code);
+  exec(sprintf('command -v %s 2>/dev/null', escapeshellarg($command)), $output, $code);
   if ($code !== 0) {
     fail(sprintf("Command '%s' is not available", $command));
   }
 }

409-431: Check curl_init() and curl_setopt_array() return values

curl_init() can return false on failure (line 409), and passing false to subsequent curl functions would cause a TypeError. Additionally, curl_setopt_array() (line 431) can return false if setting options fails.

   $ch = curl_init($url);
+  if ($ch === FALSE) {
+    fail('Failed to initialize curl for URL: %s', $url);
+  }
 
   try {
     /** @var array<int, mixed> $opts */
     $opts = [
       // ...
     ];
 
-    curl_setopt_array($ch, $opts);
+    if (!curl_setopt_array($ch, $opts)) {
+      fail('Failed to set curl options: %s', curl_error($ch));
+    }
.vortex/tooling/tests/Traits/MockTrait.php (1)

182-195: Docblock mentions 'exit_code' but actual key is 'result_code'

The docblock narrative at line 189 says "exit_code: The exit code to set" but the actual array shape at line 185 shows result_code. This inconsistency could confuse developers.

   * @param array<int, array{cmd:string, output?: string, result_code?: int, return?: NULL|FALSE}> $responses
   *   Array of responses to return for each passthru call.
   *   Each response should have:
-  *   - output: The output to echo
-  *   - exit_code: The exit code to set (0 for success).
+  *   - cmd: Expected command (required)
+  *   - output: The output to echo (optional)
+  *   - result_code: The exit code to set, 0 for success (optional)
+  *   - return: Return value, NULL or FALSE (optional)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4272ff9 and fddfa28.

📒 Files selected for processing (64)
  • .github/workflows/vortex-test-tooling.yml (1 hunks)
  • .vortex/CLAUDE.md (9 hunks)
  • .vortex/tooling/.gitattributes (1 hunks)
  • .vortex/tooling/.gitignore (1 hunks)
  • .vortex/tooling/CLAUDE.md (1 hunks)
  • .vortex/tooling/check-no-exit.php (1 hunks)
  • .vortex/tooling/composer.json (1 hunks)
  • .vortex/tooling/phpcs.xml (1 hunks)
  • .vortex/tooling/phpstan.neon (1 hunks)
  • .vortex/tooling/phpunit.xml (1 hunks)
  • .vortex/tooling/playground/.env (1 hunks)
  • .vortex/tooling/playground/.env.local (1 hunks)
  • .vortex/tooling/playground/env.php (1 hunks)
  • .vortex/tooling/playground/formatters.php (1 hunks)
  • .vortex/tooling/playground/request.php (1 hunks)
  • .vortex/tooling/rector.php (1 hunks)
  • .vortex/tooling/scripts_conversion_plan.md (1 hunks)
  • .vortex/tooling/src/helpers.php (1 hunks)
  • .vortex/tooling/src/notify (1 hunks)
  • .vortex/tooling/src/notify-email (1 hunks)
  • .vortex/tooling/src/notify-github (1 hunks)
  • .vortex/tooling/src/notify-jira (1 hunks)
  • .vortex/tooling/src/notify-newrelic (1 hunks)
  • .vortex/tooling/src/notify-slack (1 hunks)
  • .vortex/tooling/src/notify-webhook (1 hunks)
  • .vortex/tooling/tests/Exceptions/QuitErrorException.php (1 hunks)
  • .vortex/tooling/tests/Exceptions/QuitSuccessException.php (1 hunks)
  • .vortex/tooling/tests/Fixtures/check-exit/with-exit-in-comments.php (1 hunks)
  • .vortex/tooling/tests/Fixtures/check-exit/with-exit.php (1 hunks)
  • .vortex/tooling/tests/Fixtures/check-exit/with-quit.php (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-override-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-override-passing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-passthru-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-passthru-multiple (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-passthru-passing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-quit-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-quit-passing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-get-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-get-passing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-multiple (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-post-failing (1 hunks)
  • .vortex/tooling/tests/Fixtures/test-request-post-passing (1 hunks)
  • .vortex/tooling/tests/Self/CheckNoExitSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockInfrastructureSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockMailSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockPassthruSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockQuitSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php (1 hunks)
  • .vortex/tooling/tests/Traits/MockTrait.php (1 hunks)
  • .vortex/tooling/tests/Unit/CommandExistsTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/DotenvTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/FormatterTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/GetenvTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyEmailTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyGithubTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyJiraTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyNewrelicTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyRouterTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifySlackTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/NotifyWebhookTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/OverrideTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/RequestTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/TokenTest.php (1 hunks)
  • .vortex/tooling/tests/Unit/UnitTestCase.php (1 hunks)
🧰 Additional context used
🧠 Learnings (12)
📚 Learning: 2025-09-13T04:14:41.765Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 2011
File: .vortex/tests/phpunit/Traits/Steps/StepAhoyTrait.php:20-23
Timestamp: 2025-09-13T04:14:41.765Z
Learning: In .vortex/tests/phpunit PHPUnit tests using cmd/cmdFail helpers, patterns in the expected output arrays support '*' markers for positive assertions (text must be present) and '!' markers for negative assertions (text must NOT be present). The '*' markers are NOT literal text matches but rather indicate expected presence.

Applied to files:

  • .vortex/tooling/phpcs.xml
  • .vortex/tooling/tests/Exceptions/QuitSuccessException.php
  • .vortex/tooling/tests/Fixtures/test-passthru-multiple
  • .vortex/tooling/tests/Self/MockQuitSelfTest.php
  • .vortex/tooling/tests/Fixtures/check-exit/with-exit-in-comments.php
  • .vortex/tooling/tests/Exceptions/QuitErrorException.php
  • .vortex/tooling/tests/Unit/CommandExistsTest.php
  • .vortex/tooling/tests/Fixtures/check-exit/with-exit.php
  • .vortex/tooling/tests/Self/MockInfrastructureSelfTest.php
  • .vortex/tooling/tests/Unit/TokenTest.php
  • .vortex/tooling/tests/Fixtures/test-request-get-failing
  • .vortex/tooling/tests/Fixtures/test-passthru-failing
  • .vortex/tooling/tests/Fixtures/test-request-multiple
  • .vortex/tooling/tests/Unit/OverrideTest.php
  • .vortex/tooling/tests/Unit/GetenvTest.php
  • .vortex/tooling/tests/Unit/FormatterTest.php
  • .vortex/tooling/CLAUDE.md
  • .vortex/tooling/tests/Unit/NotifySlackTest.php
  • .vortex/tooling/tests/Fixtures/test-request-get-passing
  • .vortex/tooling/tests/Fixtures/test-override-failing
  • .vortex/tooling/tests/Fixtures/test-quit-passing
  • .vortex/tooling/tests/Unit/NotifyJiraTest.php
  • .vortex/tooling/tests/Unit/DotenvTest.php
  • .vortex/tooling/tests/Unit/NotifyRouterTest.php
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
  • .vortex/tooling/tests/Fixtures/test-request-post-failing
  • .vortex/tooling/tests/Fixtures/test-request-post-passing
  • .vortex/CLAUDE.md
  • .vortex/tooling/phpunit.xml
  • .vortex/tooling/tests/Self/MockMailSelfTest.php
  • .vortex/tooling/tests/Unit/UnitTestCase.php
  • .vortex/tooling/tests/Self/CheckNoExitSelfTest.php
  • .vortex/tooling/tests/Fixtures/test-passthru-passing
  • .vortex/tooling/tests/Self/MockPassthruSelfTest.php
  • .vortex/tooling/tests/Fixtures/test-quit-failing
  • .vortex/tooling/tests/Fixtures/check-exit/with-quit.php
  • .vortex/tooling/tests/Fixtures/test-override-passing
  • .vortex/tooling/tests/Unit/NotifyEmailTest.php
📚 Learning: 2025-06-01T08:09:18.993Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1693
File: .vortex/installer/tests/Fixtures/install/hosting_acquia/docs/deployment.md:6-8
Timestamp: 2025-06-01T08:09:18.993Z
Learning: Do not review files in test fixture directories (e.g., files under paths containing `/tests/Fixtures/` or similar test fixture patterns). These files are used for testing purposes and should remain as-is.

Applied to files:

  • .vortex/tooling/phpcs.xml
  • .vortex/tooling/tests/Unit/OverrideTest.php
  • .vortex/tooling/tests/Self/CheckNoExitSelfTest.php
📚 Learning: 2025-06-01T08:08:59.071Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1693
File: .vortex/installer/tests/Fixtures/install/deploy_type_none_gha/.github/workflows/build-test-deploy.yml:1-4
Timestamp: 2025-06-01T08:08:59.071Z
Learning: Test fixture files (typically located in directories containing "fixture" or "test") should not be reviewed for code quality issues as they may intentionally contain incomplete, invalid, or malformed data for testing purposes.

Applied to files:

  • .vortex/tooling/phpcs.xml
  • .vortex/tooling/tests/Fixtures/test-override-failing
  • .vortex/tooling/tests/Fixtures/test-quit-passing
  • .vortex/tooling/tests/Fixtures/test-request-post-failing
  • .vortex/tooling/tests/Fixtures/test-quit-failing
  • .vortex/tooling/tests/Fixtures/check-exit/with-quit.php
  • .vortex/tooling/tests/Fixtures/test-override-passing
  • .vortex/tooling/tests/Unit/NotifyEmailTest.php
📚 Learning: 2025-05-29T12:15:32.188Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 0
File: :0-0
Timestamp: 2025-05-29T12:15:32.188Z
Learning: Do not review files in `.vortex/installer/tests/Fixtures/install` directory as they are test fixtures.

Applied to files:

  • .vortex/tooling/.gitignore
  • .vortex/tooling/tests/Fixtures/test-passthru-failing
  • .vortex/tooling/tests/Unit/OverrideTest.php
  • .vortex/tooling/tests/Fixtures/test-override-failing
  • .vortex/tooling/tests/Fixtures/test-quit-passing
  • .vortex/CLAUDE.md
  • .vortex/tooling/tests/Fixtures/test-override-passing
📚 Learning: 2025-08-08T12:02:24.652Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1896
File: .vortex/tests/bats/unit/download-db-lagoon.bats:24-25
Timestamp: 2025-08-08T12:02:24.652Z
Learning: In .vortex/tests/bats/unit Bats tests using ../_helper.bash (run_steps), prefixing a STEPS entry with "- " denotes a negative assertion (the substring must NOT appear in output). Unprefixed entries are positive assertions. Example: "- Database dump refresh requested. Will create a new dump." asserts absence; "Database dump refresh requested. Will create a new dump." asserts presence.

Applied to files:

  • .vortex/tooling/tests/Fixtures/test-request-get-failing
  • .vortex/tooling/tests/Fixtures/test-passthru-failing
  • .vortex/tooling/tests/Fixtures/test-override-failing
  • .vortex/tooling/tests/Fixtures/test-quit-passing
  • .vortex/tooling/tests/Self/MockRequestSelfTest.php
  • .vortex/CLAUDE.md
  • .vortex/tooling/tests/Fixtures/test-passthru-passing
  • .vortex/tooling/tests/Fixtures/test-quit-failing
📚 Learning: 2025-06-02T00:41:36.321Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1697
File: .vortex/installer/tests/Fixtures/install/provision_database_lagoon/docs/onboarding.md:16-17
Timestamp: 2025-06-02T00:41:36.321Z
Learning: DO NOT EVER REVIEW FILES IN .vortex/installer/tests/Fixtures/ directory - these are test fixtures and should not be reviewed under any circumstances.

Applied to files:

  • .vortex/tooling/tests/Fixtures/test-passthru-failing
  • .vortex/tooling/tests/Fixtures/test-override-failing
  • .vortex/tooling/tests/Fixtures/test-quit-passing
  • .vortex/CLAUDE.md
  • .vortex/tooling/tests/Fixtures/test-override-passing
📚 Learning: 2025-07-23T01:16:30.963Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1816
File: .docker/cli.dockerfile:72-76
Timestamp: 2025-07-23T01:16:30.963Z
Learning: In the Vortex project, unauthenticated Composer installs should be allowed, so GitHub token secrets should not be marked as `required=true` in Docker build mounts. The conditional check `if [ -s /run/secrets/github_token ]` is the preferred approach to allow builds to proceed without a token when needed.

Applied to files:

  • .github/workflows/vortex-test-tooling.yml
  • .vortex/tooling/tests/Unit/NotifyGithubTest.php
📚 Learning: 2025-11-24T21:29:10.896Z
Learnt from: CR
Repo: drevops/vortex PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T21:29:10.896Z
Learning: Applies to tests/behat/features/**/*.feature : Always prefix test content with [TEST] to distinguish test data from real content

Applied to files:

  • .vortex/tooling/tests/Unit/FormatterTest.php
📚 Learning: 2025-06-03T03:15:29.849Z
Learnt from: AlexSkrypnyk
Repo: drevops/vortex PR: 1714
File: scripts/vortex/doctor.sh:69-70
Timestamp: 2025-06-03T03:15:29.849Z
Learning: In the Vortex project, logging functions (task(), info(), note(), pass(), fail(), warn()) are intentionally duplicated across all script files to make each script self-contained and independent, rather than sharing them through a common library. This design choice prioritizes script independence over reducing code duplication.

Applied to files:

  • .vortex/CLAUDE.md
📚 Learning: 2025-11-24T21:29:10.896Z
Learnt from: CR
Repo: drevops/vortex PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T21:29:10.896Z
Learning: Use 'ahoy build' for initial site setup, 'ahoy up/down' for environment management, and 'ahoy provision' to re-provision with fresh database

Applied to files:

  • .vortex/CLAUDE.md
📚 Learning: 2025-11-24T21:29:10.896Z
Learnt from: CR
Repo: drevops/vortex PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T21:29:10.896Z
Learning: Use 'ahoy' command prefix for all local development commands instead of running commands directly

Applied to files:

  • .vortex/CLAUDE.md
📚 Learning: 2025-11-24T21:29:10.896Z
Learnt from: CR
Repo: drevops/vortex PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T21:29:10.896Z
Learning: Applies to tests/behat/features/**/*.feature : Use descriptive step definitions and discover available steps via 'ahoy test-bdd -- --definitions=l'

Applied to files:

  • .vortex/CLAUDE.md
🧬 Code graph analysis (22)
.vortex/tooling/tests/Fixtures/check-exit/with-exit-in-comments.php (1)
.vortex/tooling/src/helpers.php (1)
  • quit (481-483)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (2)
  • __construct (22-31)
  • getOutput (39-41)
.vortex/tooling/tests/Unit/CommandExistsTest.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/src/helpers.php (2)
  • command_exists (283-288)
  • fail (265-268)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockQuit (279-298)
.vortex/tooling/tests/Self/MockInfrastructureSelfTest.php (1)
.vortex/tooling/tests/Traits/MockTrait.php (3)
  • mockPassthruAssertAllMocksConsumed (263-265)
  • mockMailAssertAllMocksConsumed (409-411)
  • mockRequestAssertAllMocksConsumed (666-668)
.vortex/tooling/playground/request.php (1)
.vortex/tooling/src/helpers.php (3)
  • request_get (346-352)
  • request_post (374-381)
  • request (403-460)
.vortex/tooling/tests/Unit/TokenTest.php (1)
.vortex/tooling/src/helpers.php (1)
  • replace_tokens (301-317)
.vortex/tooling/tests/Unit/OverrideTest.php (1)
.vortex/tooling/src/helpers.php (1)
  • execute_override (44-57)
.vortex/tooling/tests/Unit/FormatterTest.php (2)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/src/helpers.php (2)
  • fail (265-268)
  • term_supports_color (273-275)
.vortex/tooling/tests/Unit/NotifySlackTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (4)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockRequestPost (644-658)
.vortex/tooling/tests/Unit/NotifyWebhookTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockRequest (581-597)
.vortex/tooling/tests/Unit/NotifyJiraTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (4)
  • UnitTestCase (16-122)
  • runScript (38-92)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (3)
  • mockRequestGet (613-626)
  • mockRequestPost (644-658)
  • mockRequest (581-597)
.vortex/tooling/tests/Unit/DotenvTest.php (1)
.vortex/tooling/src/helpers.php (1)
  • load_dotenv (65-99)
.vortex/tooling/tests/Unit/NotifyRouterTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
  • runScript (38-92)
.vortex/tooling/tests/Traits/MockTrait.php (1)
  • mockPassthru (253-255)
.vortex/tooling/tests/Unit/RequestTest.php (1)
.vortex/tooling/src/helpers.php (3)
  • request_get (346-352)
  • request_post (374-381)
  • request (403-460)
.vortex/tooling/playground/formatters.php (1)
.vortex/tooling/src/helpers.php (1)
  • fail_no_exit (251-255)
.vortex/tooling/tests/Self/MockMailSelfTest.php (2)
.vortex/tooling/tests/Self/MockInfrastructureSelfTest.php (1)
  • CoversClass (15-27)
.vortex/tooling/tests/Traits/MockTrait.php (3)
  • mockMail (399-401)
  • mockMailMultiple (316-389)
  • mockMailAssertAllMocksConsumed (409-411)
.vortex/tooling/tests/Self/CheckNoExitSelfTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (2)
  • UnitTestCase (16-122)
  • setUp (21-28)
.vortex/tooling/tests/Unit/GetenvTest.php (1)
  • setUp (20-23)
.vortex/tooling/tests/Unit/NotifyGithubTest.php (2)
.vortex/tooling/tests/Unit/UnitTestCase.php (3)
  • UnitTestCase (16-122)
  • runScript (38-92)
  • runScriptError (108-120)
.vortex/tooling/tests/Traits/MockTrait.php (2)
  • mockRequestPost (644-658)
  • mockRequestGet (613-626)
.vortex/tooling/tests/Fixtures/check-exit/with-quit.php (2)
.vortex/tooling/tests/Fixtures/check-exit/with-exit.php (2)
  • some_condition (22-24)
  • another_condition (26-28)
.vortex/tooling/src/helpers.php (1)
  • quit (481-483)
.vortex/tooling/tests/Traits/MockTrait.php (3)
.vortex/tooling/tests/Exceptions/QuitErrorException.php (1)
  • QuitErrorException (12-43)
.vortex/tooling/tests/Exceptions/QuitSuccessException.php (1)
  • QuitSuccessException (12-43)
.vortex/tooling/src/helpers.php (2)
  • fail (265-268)
  • quit (481-483)
.vortex/tooling/tests/Unit/NotifyEmailTest.php (1)
.vortex/tooling/tests/Unit/UnitTestCase.php (5)
  • UnitTestCase (16-122)
  • setUp (21-28)
  • runScript (38-92)
  • runScriptEarlyPass (94-106)
  • runScriptError (108-120)
.vortex/tooling/src/helpers.php (1)
.vortex/installer/src/Utils/Tui.php (3)
  • line (57-59)
  • output (38-43)
  • error (53-55)
🪛 dotenv-linter (4.0.0)
.vortex/tooling/playground/.env

[warning] 25-25: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 29-29: [UnorderedKey] The PLAYGROUND_FLOAT key should go before the PLAYGROUND_NUMBER key

(UnorderedKey)


[warning] 33-33: [UnorderedKey] The PLAYGROUND_BOOL_FALSE key should go before the PLAYGROUND_BOOL_TRUE key

(UnorderedKey)

🪛 Gitleaks (8.29.1)
.vortex/tooling/tests/Unit/NotifyGithubTest.php

[high] 24-24: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 LanguageTool
.vortex/tooling/CLAUDE.md

[grammar] ~230-~230: Ensure spelling is correct
Context: ...ssertAllMocksConsumed(): void #### Passthru Mocking **Response Structure**:php...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
.vortex/tooling/scripts_conversion_plan.md

42-42: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


83-83: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


97-97: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


102-102: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


113-113: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


122-122: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


144-144: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


145-145: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


147-147: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


150-150: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


151-151: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


153-153: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


156-156: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


157-157: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


160-160: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


163-163: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


164-164: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


178-178: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


179-179: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


184-184: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


185-185: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


192-192: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


202-202: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


203-203: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


239-239: Spaces inside code span elements

(MD038, no-space-in-code)


402-402: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


409-409: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


410-410: Ordered list item prefix
Expected: 1; Actual: 6; Style: 1/2/3

(MD029, ol-prefix)


411-411: Ordered list item prefix
Expected: 2; Actual: 7; Style: 1/2/3

(MD029, ol-prefix)


412-412: Ordered list item prefix
Expected: 3; Actual: 8; Style: 1/2/3

(MD029, ol-prefix)


413-413: Ordered list item prefix
Expected: 4; Actual: 9; Style: 1/2/3

(MD029, ol-prefix)


414-414: Ordered list item prefix
Expected: 5; Actual: 10; Style: 1/2/3

(MD029, ol-prefix)


415-415: Ordered list item prefix
Expected: 6; Actual: 11; Style: 1/2/3

(MD029, ol-prefix)


417-417: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


418-418: Ordered list item prefix
Expected: 1; Actual: 12; Style: 1/2/3

(MD029, ol-prefix)


419-419: Ordered list item prefix
Expected: 2; Actual: 13; Style: 1/2/3

(MD029, ol-prefix)


420-420: Ordered list item prefix
Expected: 3; Actual: 14; Style: 1/2/3

(MD029, ol-prefix)


421-421: Ordered list item prefix
Expected: 4; Actual: 15; Style: 1/2/3

(MD029, ol-prefix)


422-422: Ordered list item prefix
Expected: 5; Actual: 16; Style: 1/2/3

(MD029, ol-prefix)


452-452: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


453-453: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


461-461: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


462-462: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


470-470: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


471-471: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


479-479: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


480-480: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


492-492: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


493-493: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


505-505: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


506-506: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


514-514: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


515-515: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


526-526: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


527-527: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


539-539: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


548-548: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


557-557: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


568-568: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


632-632: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


644-644: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


649-649: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


658-658: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


666-666: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

.vortex/tooling/CLAUDE.md

53-53: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


83-83: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


129-129: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


160-160: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


218-218: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


233-233: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


243-243: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


261-261: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


306-306: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


400-400: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


407-407: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


412-412: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


480-480: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


512-512: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


527-527: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


528-528: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


537-537: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


538-538: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


545-545: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


573-573: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


574-574: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


587-587: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


592-592: Ordered list item prefix
Expected: 1; Actual: 4; Style: 1/1/1

(MD029, ol-prefix)


736-736: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


817-817: Bare URL used

(MD034, no-bare-urls)


818-818: Bare URL used

(MD034, no-bare-urls)


819-819: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: build (0)
  • GitHub Check: build (1)
  • GitHub Check: vortex-test-workflow (3)
  • GitHub Check: vortex-test-workflow (4)
  • GitHub Check: vortex-test-common
  • GitHub Check: vortex-test-workflow (1)
  • GitHub Check: vortex-test-workflow (0)
  • GitHub Check: vortex-test-workflow (2)
  • GitHub Check: vortex-test-installer (8.4)
  • GitHub Check: vortex-test-installer (8.3)
  • GitHub Check: vortex-test-docs

## .vortex Directory Structure

The `.vortex/` directory contains **three distinct subsystems**, each with its own purpose and technology stack:
The `.vortex/` directory contains **four distinct subsystems**, each with its own purpose and technology stack:
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:

#!/bin/bash
# Search for any remaining references to old subsystem/testing counts that should be updated
echo "=== Checking for 'three distinct' patterns ==="
rg -n 'three\s+(distinct|independent|subsystem)' .vortex/CLAUDE.md

echo -e "\n=== Checking for 'four independent testing' patterns ==="
rg -n 'four\s+independent\s+testing' .vortex/CLAUDE.md

echo -e "\n=== Checking for section numbering gaps ==="
rg -n '^###\s+[0-9]' .vortex/CLAUDE.md | tail -20

Repository: drevops/vortex


<shell_result>
Repository: drevops/vortex
Exit code: 0

stdout:

=== Checking for 'three distinct' patterns ===

=== Checking for 'four independent testing' patterns ===

=== Checking for section numbering gaps ===
32:### 1. .vortex/docs/ - Documentation Website
61:### 2. .vortex/installer/ - Template Installer
92:### 3. .vortex/tooling/ - Notification & Helper Scripts
107:### 4. .vortex/tests/ - Template Testing Harness
143:### 1. Documentation Tests (.vortex/docs/)
162:### 2. Installer Tests (.vortex/installer/)
181:### 3. Tooling Tests (.vortex/tooling/)
185:### 4. Template Tests (.vortex/tests/)
203:### 5. Manual Integration Tests (.vortex/tests/manual/)
272:### 1. Documentation Tests (.vortex/docs/)
297:### 2. Installer Tests (.vortex/installer/)
322:### 3. Template Tests (.vortex/tests/)
351:### 4. Manual Integration Tests (.vortex/tests/manual/)
816:### 1. Documentation System (.vortex/docs/)
831:### 2. Installer System (.vortex/installer/)
848:### 3. Template Testing System (.vortex/tests/)
879:### 1. Documentation Issues (.vortex/docs/)
896:### 2. Installer Issues (.vortex/installer/)
920:### 3. Template Testing Issues (.vortex/tests/)
940:### 4. Manual Integration Test Issues (.vortex/tests/manual/)

</shell_result>


Script executed:

#!/bin/bash
# Read sections around the flagged lines to verify count references
echo "=== Line 30 context (subsystems) ==="
sed -n '28,32p' .vortex/CLAUDE.md

echo -e "\n=== Line 141 context ==="
sed -n '139,143p' .vortex/CLAUDE.md

echo -e "\n=== Line 569 context ==="
sed -n '567,571p' .vortex/CLAUDE.md

echo -e "\n=== Line 575 context ==="
sed -n '573,577p' .vortex/CLAUDE.md

echo -e "\n=== Line 584 context ==="
sed -n '582,586p' .vortex/CLAUDE.md

echo -e "\n=== Line 591 context ==="
sed -n '589,593p' .vortex/CLAUDE.md

echo -e "\n=== Line 1607 context ==="
sed -n '1605,1609p' .vortex/CLAUDE.md

Repository: drevops/vortex


<shell_result>
Repository: drevops/vortex
Exit code: 0

stdout:

=== Line 30 context (subsystems) ===
## .vortex Directory Structure

The `.vortex/` directory contains **four distinct subsystems**, each with its own purpose and technology stack:

### 1. .vortex/docs/ - Documentation Website

=== Line 141 context ===
## Testing Architecture Overview

Vortex uses **five independent testing systems**, each serving different parts of the codebase:

### 1. Documentation Tests (.vortex/docs/)

=== Line 569 context ===
1. **Documentation** (`.vortex/docs/`): Requires Node.js/Yarn
2. **Installer** (`.vortex/installer/`): Requires PHP/Composer
3. **Tooling** (`.vortex/tooling/`): Requires PHP/Composer
4. **Template** (`.vortex/tests/`): Requires PHP/Composer + Node.js + BATS


=== Line 575 context ===

```bash
ahoy install        # Installs dependencies for all four systems
```


=== Line 584 context ===
```bash
# From .vortex/ root
ahoy install        # Install all dependencies (docs, installer, tooling, template)
ahoy lint           # Code linting across all systems
ahoy test           # Run all template tests

=== Line 591 context ===
cd docs && yarn test                    # Documentation tests only
cd installer && composer test           # Installer tests only
cd tooling && ./vendor/bin/phpunit     # Tooling tests only
cd tests && ./vendor/bin/phpunit       # Template PHPUnit tests only
ahoy test-bats -- tests/bats/          # Template BATS tests only

=== Line 1607 context ===

- Each system can be modified independently
- Changes to template (outside `.vortex/`) may require updates across all four systems
- Always run system-specific tests after making changes
- Consider impact on user workflows when modifying any system

</shell_result>


Script executed:

#!/bin/bash
# Read the testing systems section to verify all 5 are listed
echo "=== Testing Architecture Overview section (lines 141-210) ==="
sed -n '141,210p' .vortex/CLAUDE.md

echo -e "\n=== Search for any remaining count references ==="
rg -n '(three|four|five|six)\s+' .vortex/CLAUDE.md | head -30

Repository: drevops/vortex


<shell_result>
Repository: drevops/vortex
Exit code: 0

stdout:

=== Testing Architecture Overview section (lines 141-210) ===
Vortex uses **five independent testing systems**, each serving different parts of the codebase:

### 1. Documentation Tests (.vortex/docs/)

**Scope**: Testing the documentation website components and content

**Technology**: Jest + React Testing Library + cspell

**What it Tests**:

- React component functionality and interactions
- MDX content rendering and navigation
- American English spelling consistency
- Documentation build processes

**Test Types**:

- **Unit tests**: Component behavior (`tests/unit/`)
- **Spellcheck**: Content validation (`cspell.json`)
- **Coverage reporting**: Multiple formats (text, lcov, HTML, Cobertura)

### 2. Installer Tests (.vortex/installer/)

**Scope**: Testing the template installation and customization logic

**Technology**: PHPUnit + Fixture System

**What it Tests**:

- User prompt handling and validation
- Template file modifications and token replacement
- Installation scenario outcomes
- Baseline vs customized template differences

**Test Types**:

- **Functional tests**: Complete installation scenarios
- **Handler tests**: Individual prompt and modification logic
- **Fixture tests**: Expected vs actual template output

### 3. Tooling Tests (.vortex/tooling/)

> **→ See `.vortex/tooling/CLAUDE.md`** for all tooling test documentation

### 4. Template Tests (.vortex/tests/)

**Scope**: Testing the actual Drupal template functionality

**Technology**: PHPUnit + BATS

**What it Tests**:

- Complete Drupal project workflows (build, provision, deploy)
- Individual shell script functionality
- Docker container interactions
- Real-world usage scenarios

**Test Types**:

- **PHPUnit Functional**: End-to-end workflow testing
- **BATS Unit**: Individual shell script testing with mocking

### 5. Manual Integration Tests (.vortex/tests/manual/)

**Scope**: Manual verification of notification integrations with real external services

**Technology**: Bash scripts + Real service APIs (Slack, JIRA, etc.)

**What it Tests**:


=== Search for any remaining count references ===
30:The `.vortex/` directory contains **four distinct subsystems**, each with its own purpose and technology stack:
141:Vortex uses **five independent testing systems**, each serving different parts of the codebase:
575:ahoy install        # Installs dependencies for all four systems
761:**Critical Understanding**: The `.vortex/` directory contains three **completely independent subsystems**:
1607:- Changes to template (outside `.vortex/`) may require updates across all four systems
1613:*This knowledge base should be updated whenever significant changes are made to any of the four Vortex subsystems or their maintenance procedures.*

</shell_result>


Script executed:

#!/bin/bash
# Read context around the problematic line 761
echo "=== Line 761 context (inconsistency found) ==="
sed -n '759,765p' .vortex/CLAUDE.md

# Double-check for any other "three" count references
echo -e "\n=== All count references in document ==="
rg -n '(three|four|five)\s+(distinct|independent|subsystem|system|testing)' .vortex/CLAUDE.md

Repository: drevops/vortex


<shell_result>
Repository: drevops/vortex
Exit code: 0

stdout:

=== Line 761 context (inconsistency found) ===
### Test Harness (.vortex/) - Three Separate Systems

**Critical Understanding**: The `.vortex/` directory contains three **completely independent subsystems**:

1. **`.vortex/docs/`** - Docusaurus website (Node.js/React)
2. **`.vortex/installer/`** - PHP installer application (Symfony Console)
3. **`.vortex/tests/`** - Template testing harness (PHPUnit + BATS)

=== All count references in document ===
30:The `.vortex/` directory contains **four distinct subsystems**, each with its own purpose and technology stack:
141:Vortex uses **five independent testing systems**, each serving different parts of the codebase:
575:ahoy install        # Installs dependencies for all four systems
1607:- Changes to template (outside `.vortex/`) may require updates across all four systems

</shell_result>


Line 761 contains an outdated count that contradicts the document's established architecture. The section header and description reference "three completely independent subsystems" but omit .vortex/tooling/, which is explicitly included as the third subsystem in the main architecture overview (line 30). The section should list all four subsystems: docs, installer, tooling, and tests—and update the count from three to four.

The remaining references (lines 30, 141, 575, 1607) are consistent: four subsystems and five testing systems.

🤖 Prompt for AI Agents
.vortex/CLAUDE.md around line 761: the header and sentence on this line
incorrectly state "three completely independent subsystems" and omit
`.vortex/tooling/`; update the count to "four" and revise the list to explicitly
name all four subsystems as "docs, installer, tooling, and tests" so the
sentence matches the main architecture overview and other references.

Comment on lines +24 to +35
# Multi-word value without quotes
PLAYGROUND_MULTIWORD=this has spaces and should work

# Numbers
PLAYGROUND_NUMBER=12345
PLAYGROUND_FLOAT=123.45

# Booleans (as strings)
PLAYGROUND_BOOL_TRUE=true
PLAYGROUND_BOOL_FALSE=false

# URLs
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Decide whether to satisfy dotenv-linter or keep intentional “gotcha” examples

PLAYGROUND_MULTIWORD=this has spaces and should work and the bool/number ordering trigger dotenv-linter warnings, but the surrounding comments explicitly demonstrate unquoted multi-word values and ordering is irrelevant for behavior. If you want a clean dotenv-linter run, quote the multi-word value and reorder the BOOL/NUMBER keys; otherwise, consider disabling those specific checks for this playground file.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 25-25: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 29-29: [UnorderedKey] The PLAYGROUND_FLOAT key should go before the PLAYGROUND_NUMBER key

(UnorderedKey)


[warning] 33-33: [UnorderedKey] The PLAYGROUND_BOOL_FALSE key should go before the PLAYGROUND_BOOL_TRUE key

(UnorderedKey)

🤖 Prompt for AI Agents
.vortex/tooling/playground/.env around lines 24 to 35: the dotenv-linter flags
are raised by the unquoted multi-word value and the ordering of numeric and
boolean keys; either quote the multi-word value and reorder the keys to satisfy
the linter (e.g., change PLAYGROUND_MULTIWORD=this has spaces and should work to
PLAYGROUND_MULTIWORD="this has spaces and should work" and place
PLAYGROUND_NUMBER/PLAYGROUND_FLOAT before the BOOL entries), or keep the
examples as-is and suppress the linter for this file by adding the appropriate
dotenv-linter disable directive for these lines; apply one of these fixes so the
repository linter run is intentional.

Comment on lines +19 to +122
// Test 1: Simple GET request.
echo "Test 1: GET request to httpbin.org/get\n";
$result = request_get('https://httpbin.org/get');
echo " Status: " . $result['status'] . "\n";
echo " OK: " . ($result['ok'] ? 'true' : 'false') . "\n";
echo " Body preview: " . substr((string) $result['body'], 0, 100) . "...\n";
echo " Result: " . ($result['ok'] ? "✓ SUCCESS" : "✗ FAILED") . "\n";
echo "\n";

// Test 2: GET with headers.
echo "Test 2: GET request with custom headers\n";
$result = request_get(
'https://httpbin.org/headers',
['X-Custom-Header: TestValue', 'X-Another-Header: AnotherValue']
);
echo " Status: " . $result['status'] . "\n";
echo " OK: " . ($result['ok'] ? 'true' : 'false') . "\n";
if ($result['ok']) {
echo " Check response body for custom headers:\n";
echo " " . substr((string) $result['body'], 0, 200) . "...\n";
}
echo " Result: " . ($result['ok'] ? "✓ SUCCESS" : "✗ FAILED") . "\n";
echo "\n";

// Test 3: POST request with JSON.
echo "Test 3: POST request with JSON body\n";
$result = request_post(
'https://httpbin.org/post',
'{"test": "data", "number": 123, "array": [1, 2, 3]}',
['Content-Type: application/json']
);
echo " Status: " . $result['status'] . "\n";
echo " OK: " . ($result['ok'] ? 'true' : 'false') . "\n";
if ($result['ok']) {
echo " Response preview:\n";
echo " " . substr((string) $result['body'], 0, 200) . "...\n";
}
echo " Result: " . ($result['ok'] ? "✓ SUCCESS" : "✗ FAILED") . "\n";
echo "\n";

// Test 4: Testing error handling (404).
echo "Test 4: Testing error handling (404)\n";
$result = request_get('https://httpbin.org/status/404');
echo " Status: " . $result['status'] . "\n";
echo " OK: " . ($result['ok'] ? 'true' : 'false') . "\n";
$expected_fail = !$result['ok'];
echo " Result: " . ($expected_fail ? "✓ SUCCESS (404 correctly detected as error)" : "✗ FAILED (404 should be detected as error)") . "\n";
echo "\n";

// Test 5: Testing 500 error.
echo "Test 5: Testing server error (500)\n";
$result = request_get('https://httpbin.org/status/500');
echo " Status: " . $result['status'] . "\n";
echo " OK: " . ($result['ok'] ? 'true' : 'false') . "\n";
$expected_fail = !$result['ok'];
echo " Result: " . ($expected_fail ? "✓ SUCCESS (500 correctly detected as error)" : "✗ FAILED (500 should be detected as error)") . "\n";
echo "\n";

// Test 6: Custom method (PUT).
echo "Test 6: Custom method (PUT) using request()\n";
$result = request(
'https://httpbin.org/put',
[
'method' => 'PUT',
'body' => '{"updated": true, "timestamp": 1234567890}',
'headers' => ['Content-Type: application/json'],
]
);
echo " Status: " . $result['status'] . "\n";
echo " OK: " . ($result['ok'] ? 'true' : 'false') . "\n";
echo " Result: " . ($result['ok'] ? "✓ SUCCESS" : "✗ FAILED") . "\n";
echo "\n";

// Test 7: DELETE method.
echo "Test 7: Custom method (DELETE) using request()\n";
$result = request(
'https://httpbin.org/delete',
['method' => 'DELETE']
);
echo " Status: " . $result['status'] . "\n";
echo " OK: " . ($result['ok'] ? 'true' : 'false') . "\n";
echo " Result: " . ($result['ok'] ? "✓ SUCCESS" : "✗ FAILED") . "\n";
echo "\n";

// Test 8: Timeout handling.
echo "Test 8: Testing timeout (3 second delay with 1 second timeout)\n";
echo " This should trigger a timeout error...\n";
$result = request_get('https://httpbin.org/delay/3', [], 1);
if (!empty($result['error'])) {
echo " Error: " . $result['error'] . "\n";
echo " Result: ✓ SUCCESS (Timeout correctly detected)\n";
}
else {
echo " Result: ✗ FAILED (Timeout not detected)\n";
}
echo "\n";

// Test 9: Successful request with custom timeout.
echo "Test 9: Successful request with longer timeout\n";
$result = request_get('https://httpbin.org/delay/1', [], 5);
echo " Status: " . $result['status'] . "\n";
echo " OK: " . ($result['ok'] ? 'true' : 'false') . "\n";
echo " Result: " . ($result['ok'] ? "✓ SUCCESS" : "✗ FAILED") . "\n";

Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding a fallback or note about external dependency.

The demo relies on httpbin.org being available. Consider adding a brief note at the top about this dependency, or a check that the service is reachable before running all tests.

🤖 Prompt for AI Agents
In .vortex/tooling/playground/request.php around lines 19-122 the test suite
assumes httpbin.org is reachable; add either a short preflight check (e.g., a
quick GET to https://httpbin.org/get with a 1–2s timeout) and if it fails print
a clear note and skip the tests (or exit non-fatally), or add a top-of-file
comment explaining the external dependency and how to override/skip tests (for
example via an environment variable). Implement the preflight so the script
cleanly reports "httpbin.org unreachable — skipping tests" and avoids running
the long sequence when the service is down.

Comment on lines +103 to +114
// Test 8: Timeout handling.
echo "Test 8: Testing timeout (3 second delay with 1 second timeout)\n";
echo " This should trigger a timeout error...\n";
$result = request_get('https://httpbin.org/delay/3', [], 1);
if (!empty($result['error'])) {
echo " Error: " . $result['error'] . "\n";
echo " Result: ✓ SUCCESS (Timeout correctly detected)\n";
}
else {
echo " Result: ✗ FAILED (Timeout not detected)\n";
}
echo "\n";
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Timeout test result check could be more robust.

Test 8 assumes a timeout will occur and only checks for $result['error'], but doesn't verify the ok flag is false. Also, the timeout test relies on httpbin.org's /delay/3 endpoint which may occasionally behave differently.

-if (!empty($result['error'])) {
+if (!$result['ok'] && !empty($result['error'])) {
   echo "  Error: " . $result['error'] . "\n";
   echo "  Result: ✓ SUCCESS (Timeout correctly detected)\n";
 }
🤖 Prompt for AI Agents
.vortex/tooling/playground/request.php around lines 103 to 114: the timeout test
only checks for $result['error'] and assumes httpbin.org/delay/3 will always
trigger a timeout; update the check to assert that $result['ok'] is false in
addition to $result['error'], and make the test more resilient by either using a
deterministic local/mock endpoint or by validating the error text contains a
timeout indication (e.g. "timeout" or cURL timeout code) so intermittent remote
behavior won't produce false positives; change the messages to reflect both
conditions (error present AND ok === false) to mark the test as success,
otherwise mark as failure.

Comment on lines +72 to +77
$exit_code = 0;
passthru(sprintf('"%s"', $script), $exit_code);

if ($exit_code !== 0) {
fail('Notification to %s failed with exit code %s', $channel, $exit_code);
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Script path quoting may break if path contains special characters.

The sprintf('"%s"', $script) approach could fail if $script contains double quotes. While unlikely for controlled paths, consider using escapeshellarg() for robustness:

-  passthru(sprintf('"%s"', $script), $exit_code);
+  passthru(escapeshellarg($script), $exit_code);
🤖 Prompt for AI Agents
In .vortex/tooling/src/notify around lines 72 to 77, the current sprintf('"%s"',
$script) can break when $script contains embedded double quotes or other
shell-sensitive characters; replace the manual quoting with
escapeshellarg($script) when building the command passed to passthru (i.e., call
passthru(escapeshellarg($script), $exit_code)) so the script path is safely
escaped for the shell and keeps the existing exit-code handling and fail()
behavior unchanged.

Comment on lines +613 to +626
protected function mockRequestGet(
string $url,
array $headers = [],
int $timeout = 10,
?array $response = NULL,
string $namespace = 'DrevOps\\VortexTooling',
): void {
$this->mockRequestMultiple([[
'url' => $url,
'method' => 'GET',
'response' => $response ?? [],
],
], $namespace);
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

mockRequestGet() ignores $headers and $timeout parameters

Similar to mockRequestPost(), the $headers and $timeout parameters are accepted but not forwarded to mockRequestMultiple(). While less critical than body validation, this could mislead developers into thinking these parameters affect mock matching.

Either forward the parameters or remove them from the signature to avoid confusion.

🤖 Prompt for AI Agents
.vortex/tooling/tests/Traits/MockTrait.php around lines 613 to 626:
mockRequestGet currently accepts $headers and $timeout but doesn't pass them to
mockRequestMultiple; update the call to mockRequestMultiple so the single
request entry includes 'headers' => $headers and 'timeout' => $timeout (matching
the structure used by mockRequestPost), or if headers/timeout are intentionally
unused, remove them from the method signature and any callers; ensure the
request array keys exactly match what mockRequestMultiple expects so mock
matching works as intended.

Comment on lines +644 to +658
protected function mockRequestPost(
string $url,
$body = NULL,
array $headers = [],
int $timeout = 10,
?array $response = NULL,
string $namespace = 'DrevOps\\VortexTooling',
): void {
$this->mockRequestMultiple([[
'url' => $url,
'method' => 'POST',
'response' => $response ?? [],
],
], $namespace);
}
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

mockRequestPost() ignores $body, $headers, and $timeout parameters

The method signature accepts $body, $headers, and $timeout parameters to mirror the real request_post() function, but none of these are forwarded to mockRequestMultiple(). Only url, method, and response are used.

This causes issues in tests (like NotifyJiraTest) where callbacks are passed as $body expecting validation, but they're silently ignored. The test appears to pass while the payload assertions never run.

Consider either:

  1. Forward $body as a matcher/validator to the mock infrastructure:
   protected function mockRequestPost(
     string $url,
     $body = NULL,
     array $headers = [],
     int $timeout = 10,
     ?array $response = NULL,
     string $namespace = 'DrevOps\\VortexTooling',
   ): void {
     $this->mockRequestMultiple([[
       'url' => $url,
       'method' => 'POST',
+      'body' => $body,
+      'headers' => $headers,
       'response' => $response ?? [],
     ],
     ], $namespace);
   }
  1. Remove unused parameters to avoid misleading API
  2. Document that these parameters are intentionally ignored

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
.vortex/tooling/tests/Traits/MockTrait.php around lines 644 to 658:
mockRequestPost currently accepts $body, $headers, and $timeout but does not
forward them to mockRequestMultiple, so tests that pass callbacks or expect
header/timeout checks are ignored; update the inner array passed to
mockRequestMultiple to include 'body' => $body, 'headers' => $headers, and
'timeout' => $timeout (ensuring null/defaults are preserved) so the mock
infrastructure can run validators or matchers, and keep 'response' => $response
?? []; alternatively, if you prefer the simpler option, remove the unused
parameters from the signature and update callers — but the recommended fix is to
forward these parameters so callbacks/validators are executed by the mock.

Comment on lines +26 to +30
public function testCommandExistsAvailable(): void {
$this->expectNotToPerformAssertions();

\DrevOps\VortexTooling\command_exists('php');
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding a positive assertion for the available command test.

Using expectNotToPerformAssertions() only verifies that no exception is thrown but doesn't confirm the function actually validated the command. While this may be intentional (since command_exists is void), adding a comment explaining the intent would improve clarity.

🤖 Prompt for AI Agents
In .vortex/tooling/tests/Unit/CommandExistsTest.php around lines 26-30 the test
uses expectNotToPerformAssertions() which only ensures no exception is thrown
and doesn't clearly document intent; either replace the no-assertion placeholder
with a minimal positive assertion (for example incrementing the assertion count)
or add a concise comment above the call explaining that command_exists is void
and the test only verifies it does not throw, so future readers understand this
is intentional.

Comment on lines +26 to +40
#[DataProvider('dataProviderLoadDotenv')]
public function testLoadDotenv(array $env_content, array $expected): void {
$env_file = self::$tmp . '/.env.test';
$lines = [];
foreach ($env_content as $key => $value) {
$lines[] = sprintf('%s=%s', $key, $value);
}
file_put_contents($env_file, implode("\n", $lines));

\DrevOps\VortexTooling\load_dotenv([$env_file]);

foreach ($expected as $key => $expected_value) {
$this->assertEquals($expected_value, getenv($key));
}
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Environment variables are not cleaned up after tests.

The test sets environment variables via load_dotenv() which calls putenv(), but there's no tearDown to remove them. This can cause test pollution if these variable names conflict with other tests or if test order matters.

Consider adding cleanup in tearDown or tracking and removing set variables:

protected function tearDown(): void {
    // Clean up any test environment variables
    foreach (['TEST_VAR_1', 'TEST_VAR_2', 'TEST_QUOTED_DOUBLE', 'TEST_QUOTED_SINGLE', 
              'TEST_EQUALS', 'TEST_SPACES', 'TEST_VAR', 'TEST_VAR2'] as $var) {
        putenv($var);
    }
    parent::tearDown();
}
🤖 Prompt for AI Agents
In .vortex/tooling/tests/Unit/DotenvTest.php around lines 26-40, the test sets
environment variables via load_dotenv() but does not remove them afterwards,
risking test pollution; add a protected tearDown(): void method that iterates
the exact keys set by the data provider and unsets each (use putenv("$key=") or
equivalent to remove the variable) and then calls parent::tearDown() so the
environment is cleaned between tests.

Comment on lines +34 to +51
public function testSuccessfulNotificationSingleRecipient(): void {
$this->mockMail([
'to' => '[email protected]',
'subject' => 'test-project deployment notification of main',
'message' => "## This is an automated message ##\nSite test-project main has been deployed at " . date('d/m/Y H:i:s T') . " and is available at https://example.com.\nLogin at: https://example.com/login",
'result' => TRUE,
]);

$output = $this->runScript('src/notify-email');

$this->assertStringContainsString('Started email notification', $output);
$this->assertStringContainsString('Project : test-project', $output);
$this->assertStringContainsString('From : [email protected]', $output);
$this->assertStringContainsString('Recipients : [email protected]', $output);
$this->assertStringContainsString('Sending email notification', $output);
$this->assertStringContainsString('Email notification sent successfully to 1 recipient(s)', $output);
$this->assertStringContainsString('Finished email notification', $output);
}
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

Potential timestamp flakiness in mail message assertions

The expected message includes date('d/m/Y H:i:s T') which captures the timestamp when the mock is set up. If the script execution crosses a second boundary, the timestamp in the actual message won't match, causing a spurious test failure.

Consider either:

  1. Using a more lenient assertion that ignores the timestamp portion
  2. Mocking the time function
  3. Using a regex pattern to match the message structure

This applies to multiple tests using the same pattern (lines 38, 59, 66, 83, 151, 168, 184, 214, etc.).

🤖 Prompt for AI Agents
.vortex/tooling/tests/Unit/NotifyEmailTest.php around lines 34 to 51: the test
builds an exact timestamp with date('d/m/Y H:i:s T') into the mocked mail
message which can drift by a second between setup and script execution causing
flaky failures; update the test to avoid asserting the exact timestamp — either
mock the time function so both mock and script use the same fixed time, or
change the assertion to use a regex or substring match that verifies the message
structure and static parts but ignores or loosely matches the timestamp portion
(e.g. assert that the message contains the fixed prefix/suffix and a
timestamp-like pattern) and apply the same change to the other tests listed.

@AlexSkrypnyk AlexSkrypnyk modified the milestones: 25.12.0, 2.0 Dec 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DO NOT MERGE Do not merge this pull request Do not review Do not review this pull request

Projects

Status: BACKLOG

Development

Successfully merging this pull request may close these issues.

2 participants