From 2a5033ae78e78483a1a800b055226aa2b52718ec Mon Sep 17 00:00:00 2001 From: Dumea Alexandru Date: Fri, 2 Oct 2026 11:59:55 +0300 Subject: [PATCH 1/4] feat: run the integration suite in lane smoke --- .github/workflows/ci.yml | 6 +- AGENTS.md | 6 +- bin/ci-smoke.sh | 33 +++++--- docs/qa.md | 2 +- .../Ucp/UcpRequestContextGuardTest.php | 81 ++++++++++++++++++- 5 files changed, 109 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12c80068..1e4847cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -449,11 +449,11 @@ jobs: - run: agentic-commerce/bin/ci-smoke.sh "$GITHUB_WORKSPACE/shopware" env: SHOPWARE_REF: ${{ matrix.lane }} - # Run the functional suite on every shopware-matrix lane. ci-smoke installs - # Shopware's dev deps so the suite runs on the lane's own phpunit + # Run the integration and functional suites on every shopware-matrix lane. ci-smoke + # installs Shopware's dev deps so the suites run on the lane's own phpunit # (6.5->9.x, 6.6->10.x, trunk->11.x; Shopware's test base classes are coupled # to the phpunit major), covering lane-specific behavior everywhere the smoke runs. - CI_SMOKE_RUN_FUNCTIONAL: '1' + CI_SMOKE_RUN_PHPUNIT: '1' CI_COMPOSER_AUDIT_OUTPUT: ${{ github.workspace }}/composer-audit.json - name: Upload Composer advisory report if: always() diff --git a/AGENTS.md b/AGENTS.md index ec95b5f9..dc12259a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,7 +68,7 @@ deployed HTTP stack: suite assumes a booted kernel (no per-test skip-guards) — run it via `composer test:functional` against a configured lane (e.g. the `shopware-6-6-branch-web` container), never under the fast-path bootstrap. It gates in CI on **every** `shopware-matrix` lane - (`CI_SMOKE_RUN_FUNCTIONAL=1`). + (`CI_SMOKE_RUN_PHPUNIT=1`). The flow tests share `UcpFlowTestBehaviour`, which reproduces the SDK request-context handshake offline: it sets the sales-channel config the smoke sets (`active`, `signaturePolicy=log`, @@ -142,9 +142,9 @@ The `bin/` smoke scripts share helpers from `bin/lib/`: banner, so a failure names the area. Stages share the orchestrator's shell scope (they are sourced, not subprocesses); add a new check by adding a `smoke_` module and calling it from the orchestrator. Before adding a smoke check, confirm it cannot be a `functional` test (see _Test - layering_ above) — smoke is for deployed-stack concerns only. With `CI_SMOKE_RUN_FUNCTIONAL=1` + layering_ above) — smoke is for deployed-stack concerns only. With `CI_SMOKE_RUN_PHPUNIT=1` (set on every `shopware-matrix` lane) the orchestrator installs Shopware's dev deps and runs the - functional suite on the lane's own phpunit after the HTTP smoke. + integration and functional suites on the lane's own phpunit after the HTTP smoke. Lint every shell script with `shellcheck -x bin/*.sh bin/lib/*.sh` (the CI `shell-lint` job; `.shellcheckrc` disables `SC2016` for jq filters). `-x` follows the `# shellcheck source=` diff --git a/bin/ci-smoke.sh b/bin/ci-smoke.sh index 5fd011f6..736ca58d 100755 --- a/bin/ci-smoke.sh +++ b/bin/ci-smoke.sh @@ -544,23 +544,36 @@ smoke_discovery smoke_identity smoke_checkout -# Optionally run the functional suite inside the already-booted stack. These tests -# boot a real Shopware test kernel (SHOPWARE_PROJECT_DIR unset + APP_ENV=test) and drive UCP +# Optionally run the integration and functional suites inside the already-booted stack. These +# tests boot a real Shopware test kernel (SHOPWARE_PROJECT_DIR unset + APP_ENV=test) and drive UCP # routes through a real Symfony browser, rather than hitting the deployed HTTP stack. They use # Shopware core's test base classes, which are coupled to the lane's phpunit major # (6.5->9, 6.6->10, trunk->11), so they must run on the lane's OWN phpunit, not the plugin's # pinned .tools 10.5. The smoke stack installs Shopware --no-dev, so pull in the dev deps here: # bin/run.php then prefers the platform phpunit and tests/bootstrap.php registers the plugin's # src + Tests namespaces on the platform autoloader. -if [[ "${CI_SMOKE_RUN_FUNCTIONAL:-0}" == "1" ]]; then - echo "Installing Shopware dev dependencies so the functional suite runs on the lane's own phpunit." +if [[ "${CI_SMOKE_RUN_PHPUNIT:-0}" == "1" ]]; then + # Lane-level check that ucp:config:set persists strict; PHPUnit below uses the *_test database. + # Restored to log because the unsigned e2e step after this script reuses the stack. + echo "Persisting signature-policy=strict for sales channel ${sales_channel_id} via ucp:config:set." + web php /var/www/html/bin/console ucp:config:set --sales-channel="${sales_channel_id}" --signature-policy=strict + persisted_signature_policy="$(db_query "SELECT JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.signaturePolicy')) FROM swag_agentic_commerce_ucp_config WHERE sales_channel_id = UNHEX('${sales_channel_id}');")" + if [[ "${persisted_signature_policy}" != "strict" ]]; then + echo "Expected ucp:config:set to persist signaturePolicy=strict, got '${persisted_signature_policy}'." >&2 + exit 1 + fi + web php /var/www/html/bin/console ucp:config:set --sales-channel="${sales_channel_id}" --signature-policy=log + + echo "Installing Shopware dev dependencies so the PHPUnit suites run on the lane's own phpunit." web composer install -d /var/www/html --no-interaction --no-progress --no-scripts - echo "Running functional suite (lane phpunit, booting test kernel)." - "${compose[@]}" exec -T \ - -e SHOPWARE_PROJECT_DIR= \ - -e APP_ENV=test \ - -w /var/www/html/custom/plugins/SwagAgenticCommerce \ - web php bin/run.php phpunit --testsuite functional + for suite in integration functional; do + echo "Running ${suite} suite (lane phpunit, booting test kernel)." + "${compose[@]}" exec -T \ + -e SHOPWARE_PROJECT_DIR= \ + -e APP_ENV=test \ + -w /var/www/html/custom/plugins/SwagAgenticCommerce \ + web php bin/run.php phpunit --testsuite "${suite}" + done fi echo "Smoke test passed for ${SHOPWARE_DIR}." diff --git a/docs/qa.md b/docs/qa.md index f51fc9c5..7cb0d719 100644 --- a/docs/qa.md +++ b/docs/qa.md @@ -26,7 +26,7 @@ including completing a checkout into a **real Shopware order** and reading it ba token (via the shared `UcpFlowTestBehaviour`). It requires the booting bootstrap (`SHOPWARE_PROJECT_DIR` unset + `APP_ENV=test`) and, like core, assumes a booted kernel — run it against a configured lane with `composer test:functional`. In CI it gates on **every** -`shopware-matrix` lane (`CI_SMOKE_RUN_FUNCTIONAL=1`). +`shopware-matrix` lane (`CI_SMOKE_RUN_PHPUNIT=1`). It runs on the **lane's own** phpunit: Shopware core's test base classes are coupled to each lane's phpunit major (6.5→9.x, 6.6→10.x, trunk→11.x), so a single pinned phpunit can't span lanes. diff --git a/tests/Functional/Ucp/UcpRequestContextGuardTest.php b/tests/Functional/Ucp/UcpRequestContextGuardTest.php index 34134894..e8440ea5 100644 --- a/tests/Functional/Ucp/UcpRequestContextGuardTest.php +++ b/tests/Functional/Ucp/UcpRequestContextGuardTest.php @@ -7,8 +7,8 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Shopware\Core\DevOps\Environment\EnvironmentHelper; -use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour; use Shopware\Core\Framework\Test\TestCaseBase\KernelLifecycleManager; +use Swag\AgenticCommerce\Ucp\Config\UcpConfigService; use Symfony\Component\HttpFoundation\Response; /** @@ -18,6 +18,10 @@ * a 422, replacing the equivalent shell-smoke assertion with a readable PHP test, and that the * OAuth-metadata endpoint stays a 501 stub until identity linking is enabled. * + * The strict-policy tests persist `signaturePolicy=strict` through UcpConfigService (the service behind + * `ucp:config:set`), overriding the `log` system config from {@see UcpFlowTestBehaviour::configureUcpRuntime()}. + * A2A and embedded stay incomplete while the SDK's RequestContextListener::isUcpRequest() exempts those paths. + * * Requests target `APP_URL` — the test database's default storefront sales-channel domain — exactly * as Shopware's own functional tests do. * @@ -25,7 +29,7 @@ */ final class UcpRequestContextGuardTest extends TestCase { - use IntegrationTestBehaviour; + use UcpFlowTestBehaviour; #[Test] public function testRuntimeRequestWithoutUcpAgentHeaderIsRejected(): void @@ -54,6 +58,79 @@ public function testOAuthMetadataStaysUnsupportedUntilIdentityLinkingIsEnabled() self::assertSame(Response::HTTP_NOT_IMPLEMENTED, $browser->getResponse()->getStatusCode()); } + #[Test] + public function testUnsignedCatalogSearchIsRejectedUnderStrictSignaturePolicy(): void + { + $this->configureUcpRuntime(); + $this->enforceStrictSignaturePolicy(); + + $response = $this->ucpRequest('POST', '/ucp/v1/catalog/search', ['query' => 'Kernel', 'limit' => 1]); + + $this->assertMissingSignatureRejection($response); + } + + #[Test] + public function testUnsignedA2aRequestIsRejectedUnderStrictSignaturePolicy(): void + { + $this->configureUcpRuntime(); + $this->enforceStrictSignaturePolicy(); + + $response = $this->ucpRequest('POST', '/ucp/a2a', [ + 'jsonrpc' => '2.0', + 'id' => 1, + 'method' => 'catalog.search', + 'params' => ['query' => 'Kernel', 'limit' => 1], + ]); + + // JSON-RPC errors are also HTTP 200, so only a served result counts as the known gap. + if (Response::HTTP_OK === $response->getStatusCode() && isset($this->decode($response)['result'])) { + self::markTestIncomplete('ucp-php-sdk 0.0.7 RequestContextListener::isUcpRequest() excludes /ucp/a2a from request-context handling, so signaturePolicy=strict is not enforced on A2A.'); + } + + $this->assertMissingSignatureRejection($response); + } + + #[Test] + public function testUnsignedEmbeddedCartPageIsRejectedUnderStrictSignaturePolicy(): void + { + $this->configureUcpRuntime(); + $productId = $this->seedStorefrontProduct('Kernel Test Album'); + $create = $this->ucpRequest('POST', '/ucp/v1/carts', [ + 'line_items' => [['item' => ['id' => $productId, 'title' => 'Kernel Test Album', 'price' => 19.99], 'quantity' => 1]], + ]); + self::assertSame(Response::HTTP_CREATED, $create->getStatusCode()); + $cartId = $this->decode($create)['id']; + $this->enforceStrictSignaturePolicy(); + + $response = $this->ucpRequest('GET', '/ucp/embedded/cart/'.$cartId); + + if (Response::HTTP_OK === $response->getStatusCode() && str_starts_with((string) $response->headers->get('Content-Type'), 'text/html')) { + self::markTestIncomplete('ucp-php-sdk 0.0.7 RequestContextListener::isUcpRequest() excludes /ucp/embedded from request-context handling, so signaturePolicy=strict is not enforced on the embedded page.'); + } + + $this->assertMissingSignatureRejection($response); + } + + private function enforceStrictSignaturePolicy(): void + { + $config = static::getContainer()->get(UcpConfigService::class)->saveConfig([ + 'signaturePolicy' => 'strict', + 'enabledTransports' => ['rest', 'a2a', 'embedded'], + // EmbeddedResponseListener answers 403 before routing while no origin is allowlisted. + 'embeddedAllowedOrigins' => [$this->ucpDomain], + ], $this->ucpSalesChannelId); + + self::assertSame('strict', $config->signaturePolicy); + } + + private function assertMissingSignatureRejection(Response $response): void + { + self::assertSame(Response::HTTP_UNAUTHORIZED, $response->getStatusCode()); + $message = $this->decode($response)['messages'][0] ?? []; + self::assertSame('signature_invalid', $message['code'] ?? null); + self::assertSame('Missing signature headers.', $message['content'] ?? null); + } + private function appUrl(): string { return rtrim((string) EnvironmentHelper::getVariable('APP_URL'), '/'); From 70840446d869d91eb991f4edeb040c47486d8eef Mon Sep 17 00:00:00 2001 From: Dumea Alexandru Date: Fri, 2 Oct 2026 12:05:06 +0300 Subject: [PATCH 2/4] feat: fix lines in md file --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index dc12259a..105d36d7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -142,8 +142,8 @@ The `bin/` smoke scripts share helpers from `bin/lib/`: banner, so a failure names the area. Stages share the orchestrator's shell scope (they are sourced, not subprocesses); add a new check by adding a `smoke_` module and calling it from the orchestrator. Before adding a smoke check, confirm it cannot be a `functional` test (see _Test - layering_ above) — smoke is for deployed-stack concerns only. With `CI_SMOKE_RUN_PHPUNIT=1` - (set on every `shopware-matrix` lane) the orchestrator installs Shopware's dev deps and runs the + layering_ above) — smoke is for deployed-stack concerns only. With `CI_SMOKE_RUN_PHPUNIT=1` (set + on every `shopware-matrix` lane) the orchestrator installs Shopware's dev deps and runs the integration and functional suites on the lane's own phpunit after the HTTP smoke. Lint every shell script with `shellcheck -x bin/*.sh bin/lib/*.sh` (the CI `shell-lint` job; From 0706b0245c375745e16713b621f5d70608cac5a8 Mon Sep 17 00:00:00 2001 From: Dumea Alexandru Date: Tue, 6 Oct 2026 10:47:41 +0300 Subject: [PATCH 3/4] fix: Fix review --- AGENTS.md | 18 ++++++------ bin/ci-smoke.sh | 14 ++-------- bin/lib/smoke/identity.sh | 6 ---- bin/lib/smoke/signature.sh | 28 +++++++++++++++++++ .../Ucp/UcpRequestContextGuardTest.php | 13 ++++----- 5 files changed, 47 insertions(+), 32 deletions(-) create mode 100644 bin/lib/smoke/signature.sh diff --git a/AGENTS.md b/AGENTS.md index 105d36d7..3f982d9e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,8 @@ deployed HTTP stack: regardless of how many collaborators it stubs. 2. **`integration`** (`tests/Integration`, `composer test:integration`) — DB-backed tests that use a real Doctrine connection through the kernel (e.g. the migration tests). Reserve this tier for - tests that genuinely touch the database/kernel; mock-only tests belong in `unit`. + tests that genuinely touch the database/kernel; mock-only tests belong in `unit`. It gates in CI + on **every** `shopware-matrix` lane (`CI_SMOKE_RUN_PHPUNIT=1`). 3. **`functional`** (`tests/Functional`, `composer test:functional`) — boots a real Shopware test kernel and drives UCP runtime routes end-to-end through a real Symfony `KernelBrowser` (the full HttpKernel request/response cycle, kernel events included), against `APP_URL` — the test @@ -138,13 +139,14 @@ The `bin/` smoke scripts share helpers from `bin/lib/`: - `bin/lib/lane.sh` — container helpers (`web`, `db_query`, …) operating on the sourcing script's `compose` array and `container_runtime`. - `bin/lib/smoke/*.sh` — `bin/ci-smoke.sh` is a thin orchestrator that, after bootstrap, sources and - runs named stage modules (`discovery`, `identity`, `checkout`). Each prints a `>>> smoke: ` - banner, so a failure names the area. Stages share the orchestrator's shell scope (they are - sourced, not subprocesses); add a new check by adding a `smoke_` module and calling it from - the orchestrator. Before adding a smoke check, confirm it cannot be a `functional` test (see _Test - layering_ above) — smoke is for deployed-stack concerns only. With `CI_SMOKE_RUN_PHPUNIT=1` (set - on every `shopware-matrix` lane) the orchestrator installs Shopware's dev deps and runs the - integration and functional suites on the lane's own phpunit after the HTTP smoke. + runs named stage modules (`discovery`, `identity`, `checkout`, `signature`). Each prints a + `>>> smoke: ` banner, so a failure names the area. Stages share the orchestrator's shell + scope (they are sourced, not subprocesses); add a new check by adding a `smoke_` module and + calling it from the orchestrator. Before adding a smoke check, confirm it cannot be a `functional` + test (see _Test layering_ above) — smoke is for deployed-stack concerns only. With + `CI_SMOKE_RUN_PHPUNIT=1` (set on every `shopware-matrix` lane) the orchestrator installs + Shopware's dev deps and runs the integration and functional suites on the lane's own phpunit after + the HTTP smoke. Lint every shell script with `shellcheck -x bin/*.sh bin/lib/*.sh` (the CI `shell-lint` job; `.shellcheckrc` disables `SC2016` for jq filters). `-x` follows the `# shellcheck source=` diff --git a/bin/ci-smoke.sh b/bin/ci-smoke.sh index 736ca58d..32256618 100755 --- a/bin/ci-smoke.sh +++ b/bin/ci-smoke.sh @@ -539,10 +539,13 @@ source "${PLUGIN_ROOT}/bin/lib/smoke/discovery.sh" source "${PLUGIN_ROOT}/bin/lib/smoke/identity.sh" # shellcheck source=bin/lib/smoke/checkout.sh source "${PLUGIN_ROOT}/bin/lib/smoke/checkout.sh" +# shellcheck source=bin/lib/smoke/signature.sh +source "${PLUGIN_ROOT}/bin/lib/smoke/signature.sh" smoke_discovery smoke_identity smoke_checkout +smoke_signature # Optionally run the integration and functional suites inside the already-booted stack. These # tests boot a real Shopware test kernel (SHOPWARE_PROJECT_DIR unset + APP_ENV=test) and drive UCP @@ -553,17 +556,6 @@ smoke_checkout # bin/run.php then prefers the platform phpunit and tests/bootstrap.php registers the plugin's # src + Tests namespaces on the platform autoloader. if [[ "${CI_SMOKE_RUN_PHPUNIT:-0}" == "1" ]]; then - # Lane-level check that ucp:config:set persists strict; PHPUnit below uses the *_test database. - # Restored to log because the unsigned e2e step after this script reuses the stack. - echo "Persisting signature-policy=strict for sales channel ${sales_channel_id} via ucp:config:set." - web php /var/www/html/bin/console ucp:config:set --sales-channel="${sales_channel_id}" --signature-policy=strict - persisted_signature_policy="$(db_query "SELECT JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.signaturePolicy')) FROM swag_agentic_commerce_ucp_config WHERE sales_channel_id = UNHEX('${sales_channel_id}');")" - if [[ "${persisted_signature_policy}" != "strict" ]]; then - echo "Expected ucp:config:set to persist signaturePolicy=strict, got '${persisted_signature_policy}'." >&2 - exit 1 - fi - web php /var/www/html/bin/console ucp:config:set --sales-channel="${sales_channel_id}" --signature-policy=log - echo "Installing Shopware dev dependencies so the PHPUnit suites run on the lane's own phpunit." web composer install -d /var/www/html --no-interaction --no-progress --no-scripts for suite in integration functional; do diff --git a/bin/lib/smoke/identity.sh b/bin/lib/smoke/identity.sh index 4cd0f36c..65a029b7 100644 --- a/bin/lib/smoke/identity.sh +++ b/bin/lib/smoke/identity.sh @@ -20,10 +20,4 @@ smoke_identity() { fi rm -f "${tokenize_body_file}" - - # NOTE: strict-signature acceptance/rejection is intentionally NOT asserted here. This smoke - # sends unsigned requests, and flipping signaturePolicy=strict at runtime did not reject the - # no-signature request (the SDK rejects bad signatures, not absent ones, on this path). Signed - # request verification is covered by the conformance suite (bin/validate-ucp-store.sh - # conformance) and the manual-testing doc. } diff --git a/bin/lib/smoke/signature.sh b/bin/lib/smoke/signature.sh new file mode 100644 index 00000000..56d955fa --- /dev/null +++ b/bin/lib/smoke/signature.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +# bin/lib/smoke/signature.sh — strict signature policy over the deployed HTTP stack. +# ucp:config:set writes the stored config row, which is what the runtime reads; system config +# alone cannot switch the policy. The body check pins the RFC 9421 path, since a disallowed +# profile host also answers 401. Sourced by ci-smoke.sh. + +smoke_signature() { + echo ">>> smoke: signature" + + web php /var/www/html/bin/console ucp:config:set --sales-channel="${sales_channel_id}" --signature-policy=strict + + local strict_body_file strict_status + strict_body_file="$(mktemp)" + + strict_status="$(curl -sS -o "${strict_body_file}" -w '%{http_code}' -X POST "${BASE_URL}/ucp/v1/catalog/search" -H "${ucp_agent_header}" -H 'content-type: application/json' -d '{"query":"smoke","limit":1}')" + if [[ "${strict_status}" != "401" ]] || ! grep -q 'Missing signature headers.' "${strict_body_file}"; then + echo "Expected an unsigned catalog search to be rejected with 401 'Missing signature headers.' under strict, got ${strict_status}." >&2 + cat "${strict_body_file}" >&2 + rm -f "${strict_body_file}" + exit 1 + fi + + rm -f "${strict_body_file}" + + # The unsigned e2e step after this script reuses the stack. + web php /var/www/html/bin/console ucp:config:set --sales-channel="${sales_channel_id}" --signature-policy=log +} \ No newline at end of file diff --git a/tests/Functional/Ucp/UcpRequestContextGuardTest.php b/tests/Functional/Ucp/UcpRequestContextGuardTest.php index e8440ea5..4c5e7330 100644 --- a/tests/Functional/Ucp/UcpRequestContextGuardTest.php +++ b/tests/Functional/Ucp/UcpRequestContextGuardTest.php @@ -20,7 +20,9 @@ * * The strict-policy tests persist `signaturePolicy=strict` through UcpConfigService (the service behind * `ucp:config:set`), overriding the `log` system config from {@see UcpFlowTestBehaviour::configureUcpRuntime()}. - * A2A and embedded stay incomplete while the SDK's RequestContextListener::isUcpRequest() exempts those paths. + * A2A stays incomplete while the SDK's RequestContextListener::isUcpRequest() exempts that path. The + * embedded page must still be served: browsers cannot sign an iframe load, and the Embedded Checkout + * Protocol authorizes it through the token in the URL. * * Requests target `APP_URL` — the test database's default storefront sales-channel domain — exactly * as Shopware's own functional tests do. @@ -91,7 +93,7 @@ public function testUnsignedA2aRequestIsRejectedUnderStrictSignaturePolicy(): vo } #[Test] - public function testUnsignedEmbeddedCartPageIsRejectedUnderStrictSignaturePolicy(): void + public function testUnsignedEmbeddedCartPageIsServedUnderStrictSignaturePolicy(): void { $this->configureUcpRuntime(); $productId = $this->seedStorefrontProduct('Kernel Test Album'); @@ -104,11 +106,8 @@ public function testUnsignedEmbeddedCartPageIsRejectedUnderStrictSignaturePolicy $response = $this->ucpRequest('GET', '/ucp/embedded/cart/'.$cartId); - if (Response::HTTP_OK === $response->getStatusCode() && str_starts_with((string) $response->headers->get('Content-Type'), 'text/html')) { - self::markTestIncomplete('ucp-php-sdk 0.0.7 RequestContextListener::isUcpRequest() excludes /ucp/embedded from request-context handling, so signaturePolicy=strict is not enforced on the embedded page.'); - } - - $this->assertMissingSignatureRejection($response); + self::assertSame(Response::HTTP_OK, $response->getStatusCode()); + self::assertStringStartsWith('text/html', (string) $response->headers->get('Content-Type')); } private function enforceStrictSignaturePolicy(): void From 0100b09f1daf7a159b85bf57e1b3ef83834be8fe Mon Sep 17 00:00:00 2001 From: Dumea Alexandru Date: Tue, 6 Oct 2026 11:44:54 +0300 Subject: [PATCH 4/4] fix: Add issue into the skipped test --- tests/Functional/Ucp/UcpRequestContextGuardTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Functional/Ucp/UcpRequestContextGuardTest.php b/tests/Functional/Ucp/UcpRequestContextGuardTest.php index 4c5e7330..bda60a0d 100644 --- a/tests/Functional/Ucp/UcpRequestContextGuardTest.php +++ b/tests/Functional/Ucp/UcpRequestContextGuardTest.php @@ -86,7 +86,7 @@ public function testUnsignedA2aRequestIsRejectedUnderStrictSignaturePolicy(): vo // JSON-RPC errors are also HTTP 200, so only a served result counts as the known gap. if (Response::HTTP_OK === $response->getStatusCode() && isset($this->decode($response)['result'])) { - self::markTestIncomplete('ucp-php-sdk 0.0.7 RequestContextListener::isUcpRequest() excludes /ucp/a2a from request-context handling, so signaturePolicy=strict is not enforced on A2A.'); + self::markTestIncomplete('A2A skips the signature check under strict, see agentic-commerce-alliance/ucp-php-sdk#206.'); } $this->assertMissingSignatureRejection($response);