Skip to content

fix: address 17 open issues across auth, isolation, idempotency, and docs#52

Merged
nficano merged 1 commit into
mainfrom
fix/multiple-open-issues
May 25, 2026
Merged

fix: address 17 open issues across auth, isolation, idempotency, and docs#52
nficano merged 1 commit into
mainfrom
fix/multiple-open-issues

Conversation

@nficano
Copy link
Copy Markdown
Contributor

@nficano nficano commented May 25, 2026

Summary

Closes #34, #35, #36, #37, #38, #39, #40, #41, #42, #44, #45, #46, #47, #48, #49, #50.

Security & session isolation

Correctness

Documentation

Test plan

  • composer test — 360 tests, 803 assertions, OK
  • composer lint — clean
  • composer stan — no errors
  • composer psalm — no errors
  • New regression tests cover each fix (bearer principal, cross-session resume/subscribe/artifact/lease, sha256 mismatch, contiguous chunks, small-float costs, durable revocation gate, terminal-job retention, stdio EOF, idempotent replay).

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added SHA-256 digest support for artifact upload to ensure data integrity.
    • Added new artifact release operation.
    • Completed jobs now remain visible within a configurable retention window.
  • Security Improvements

    • Enforced session-based isolation for artifact operations—artifacts are scoped to their owning session.
    • Bearer token authentication now always resolves the authenticated principal from server-side token mappings, ignoring any client-supplied values.
    • Subscription filtering and event replay now respect session boundaries, preventing cross-session data leakage.
  • Bug Fixes

    • Fixed idempotent replay to re-emit the original response instead of a generic acknowledgment.
    • Improved transport handling for JSON frames without trailing delimiters.
    • Aligned credential store behavior expectations for durable revocation support.
  • Documentation

    • Updated authentication guide to clarify bearer token principal resolution behavior.

…docs

Security & isolation:
- BearerAuth ignores client-supplied principal; only the server-side
  token mapping is authoritative (#34).
- Resume replay is scoped to the calling session via
  EventLog::replayAfterForSession (#44).
- Subscriptions with no session_id filter default to the calling
  session instead of fanning out across sessions (#45).
- Artifact fetch/release reject cross-session artifact ids with
  PERMISSION_DENIED (#46).
- LeaseManager records the granting session; tool.invoke rejects
  leases granted to another session (#47).

Correctness:
- Idempotent retries now replay the original tool.result/tool.error
  instead of returning a bare Ack (#35).
- ResultChunkAssembler enforces sequence contiguity, terminal-chunk
  delivery, and duplicate consistency (#36).
- CostBudget tolerates small float costs (scientific notation),
  rejects NaN/Inf, and rejects over-precise budget patterns (#37).
- ArtifactPut validates the supplied sha256 digest against payload
  bytes; client putArtifact() accepts and surfaces digest errors (#38).
- ARCPClient ping/putArtifact map Nack responses through ErrorMapper
  consistently with the other helpers (#39).
- StdioTransport drains and decodes a final unterminated frame on
  EOF instead of dropping it (#48).
- InMemoryCredentialStore now reports supportsDurableRevocation()
  as false; runtime correctly refuses to pair it with a
  CredentialProvisioner (#49).
- JobManager retains terminal jobs for a bounded retention window so
  session.list_jobs can surface recent history (#50).

Documentation:
- README event type names match the actual catalog (#40).
- Auth guide notes server-authoritative bearer principal (#41).
- @throws docs added to ARCPClient, JobContext, ARCPRuntime, and
  Transport public APIs (#42).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

Walkthrough

This PR enforces session isolation across artifact, lease, and event replay operations; upgrades bearer authentication to ignore client-supplied principals; validates artifact SHA-256 digests and result chunk streams; extends job retention; and improves cost budget float handling.

Changes

Session isolation, authentication security, and artifact management

Layer / File(s) Summary
Bearer authentication principal server-authority
src/Auth/BearerAuth.php, docs/guides/auth.md, tests/Integration/HandshakeTest.php, tests/Unit/AuthTest.php
Bearer token verification now always returns the server-side token-mapped principal, ignoring any client-supplied PeerInfo principal. Documentation clarifies Session::$principal is server-authoritative and derived from the auth scheme. Tests verify token mapping wins over client input.
Artifact session-scoped access and SHA-256 validation
src/Runtime/ArtifactStore.php, src/Internal/Runtime/ArtifactDispatcher.php, src/Client/ARCPClient.php, tests/Integration/ArtifactTest.php
ArtifactStore enforces session ownership via assertOwnership checks on fetch/ref/release. ArtifactDispatcher validates incoming sha256 digests and uses session-aware artifact operations. ARCPClient adds optional sha256 parameter to putArtifact and new releaseArtifact method. Cross-session access is rejected with PermissionDeniedException.
Result chunk stream contiguity and duplicate validation
src/Client/ResultChunkAssembler.php, tests/Unit/Client/ResultChunkAssemblerTest.php
ResultChunkAssembler enforces sequence contiguity from 0, terminal-chunk delivery, and duplicate consistency. Conflicting duplicate chunks throw InvalidArgumentException. New test suite covers happy path, out-of-order assembly, missing chunks, and base64 encoding.
Event log session-scoped replay and point lookup
src/Store/EventLog.php, src/Internal/Runtime/LifecycleHandler.php, src/Internal/Runtime/SubscriptionRouter.php, tests/Integration/ResumeTest.php
EventLog gains findByMessageId for point lookup and replayAfterForSession for session-scoped replay. LifecycleHandler and SubscriptionRouter use session-scoped replay to ensure clients see only their own session's events during resume and backfill. Cross-session resume is rejected.
Lease registration with session ownership
src/Runtime/LeaseManager.php, tests/Unit/LeaseManagerTest.php
LeaseManager::register now accepts optional SessionId to track lease ownership. New getForSession method enforces ownership, throwing PermissionDeniedException when querying session doesn't match. Tests verify owner is allowed and foreign sessions are rejected.
Subscription session filtering
src/Runtime/SubscriptionManager.php, tests/Integration/SubscriptionTest.php
SubscriptionManager defaults empty session_id filters to the calling session, preventing subscriptions from accidentally observing other sessions. Integration test verifies empty filter respects session boundaries.
Tool invocation with session-aware lease and idempotency
src/Internal/Runtime/ToolInvocationHandler.php, tests/Integration/JobLifecycleTest.php
ToolInvocationHandler passes Session to leaseFromArguments for ownership enforcement, wraps PermissionDeniedException to emit early ToolError, and re-emits original outcome messages for idempotent duplicates instead of emitting Ack('replay'). Idempotency records now store outcome message ids and skip retryable failures.
Credential lifecycle with session-aware lease lookup
src/Internal/Runtime/CredentialLifecycle.php, src/Runtime/JobContext.php, tests/Integration/CredentialLifecycleTest.php
CredentialLifecycle::leaseFromArguments accepts optional Session and uses session-scoped lookup via LeaseManager::getForSession. JobContext passes sessionId when registering leases. Runtime rejects InMemoryCredentialStore with CredentialProvisioner.
Job terminal retention with clock injection
src/Runtime/JobManager.php, src/Runtime/Job.php, tests/Integration/JobLifecycleTest.php
JobManager injects ClockInterface, records terminatedAt on terminal transitions, retains jobs for configurable window (default 900s), and sweeps expired entries. Job adds public terminatedAt property. Integration test verifies completed jobs remain queryable within retention window.
Cost budget float validation and six-decimal precision
src/Runtime/CostBudget.php, tests/Unit/Runtime/CostBudgetTest.php
CostBudget::consume validates float metrics are finite/not NaN; decimalToScaled enforces six-decimal precision and rejects inputs exceeding that threshold. Tests verify small floats, scientific notation, INF/NAN rejection, and precision rules.
Transport EOF handling and infrastructure
src/Transport/StdioTransport.php, src/Transport/Transport.php, tests/Integration/StdioTransportTest.php, tests/Support/FakeDurableCredentialStore.php, README.md, docs/guides/auth.md
StdioTransport drains residual unterminated frames from buffer at close. Transport interface clarifies exception behavior. FakeDurableCredentialStore test double provides durable revocation support. README event names and auth guide principal behavior updated.
Runtime clock initialization and client API
src/Runtime/ARCPRuntime.php, src/Client/ARCPClient.php
ARCPRuntime passes clock to JobManager. ARCPClient extends putArtifact with sha256 parameter, adds releaseArtifact method, and expands @throws documentation across open/invokeTool/subscribe/listJobs/ping.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: address 17 open issues across auth, isolation, idempotency, and docs' accurately reflects the main changes in the PR, which cover multiple categories of fixes including security/auth, session isolation, idempotency, and documentation updates.
Linked Issues check ✅ Passed The PR satisfies the primary objective from issue #34: BearerAuth now ignores client-supplied PeerInfo.principal and returns the server-side token-mapped principal, with regression tests added (testBearerTokenIgnoresClientSuppliedPrincipal in HandshakeTest and testBearerIgnoresClientSuppliedPrincipal in AuthTest).
Out of Scope Changes check ✅ Passed All code changes are directly aligned with the 17 issues being addressed: auth fixes (BearerAuth), session isolation (resume replay scoping, subscription filtering, artifact/lease access control), idempotency (replay of original outcomes), and supporting infrastructure (ResultChunkAssembler validation, CostBudget precision, StdioTransport EOF handling, JobManager retention).

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/multiple-open-issues

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.1.54)

PHPStan was skipped because the config uses disallowed bootstrapFiles, bootstrapFile, or includes directives.


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

Copy link
Copy Markdown

@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: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Internal/Runtime/ArtifactDispatcher.php`:
- Around line 45-51: The error message in the SHA-256 validation is misleading
because $normalized is derived with strtolower(), so uppercase hex is accepted;
update the lifecycle->nack call (the error string passed in the artifact.put
SHA-256 validation block using preg_match('/^[0-9a-f]{64}$/', $normalized)) to a
neutral message such as "artifact.put sha256 must be 64 hexadecimal characters"
(or similar) that does not claim lowercase-only hex.

In `@src/Internal/Runtime/CredentialLifecycle.php`:
- Around line 33-37: The code currently allows a null Session to fall back to
leases->get(...) which bypasses session ownership checks; update
leaseFromArguments (and the other method(s) that call leases->get around the
102–113 area) to require a non-null Session: if $session is null throw an
InvalidArgumentException (or specific domain exception) instead of calling
leases->get; and when retrieving a lease use a session-aware lookup or enforce
ownership by verifying the returned LeaseGranted's session id matches
$session->id (e.g. replace leases->get(...) with a session-aware call or add a
post-check that LeaseGranted->getSessionId() === $session->id and return
null/throw if it does not). Ensure you reference leaseFromArguments and the code
paths that currently call leases->get to locate and harden all fallbacks.

In `@src/Runtime/JobContext.php`:
- Line 326: Replace the nullable session reference with the authoritative
JobContext session id when registering leases: in JobContext (where the call to
$this->runtime->leases->register(...) occurs), use $this->sessionId instead of
$this->session->sessionId so the register call uses the non-null JobContext
session identifier; update the register invocation in that method (and any
nearby calls in the same class) to pass $this->sessionId.

In `@src/Transport/Transport.php`:
- Around line 33-35: Update the Transport class/interface header to match the
receive() method contract: change the top-level wording that currently says
connection drops are returned as null to state that clean closes return null
while unexpected transport failures throw \Arcp\Errors\TransportClosedException;
ensure this aligns with the receive() docblock which already documents returning
null on clean close and throwing TransportClosedException on unexpected failure,
and keep the \Amp\CancelledException note for `$cancellation` consistent.

In `@tests/Integration/ResumeTest.php`:
- Around line 137-166: The test currently has two consumers reading from the
same transport (ARCPClient's internal read-loop started by ARCPClient::open and
the direct $clientTA->receive() calls), which causes nondeterministic races; fix
by choosing one consumer path and removing the other: either stop using
ARCPClient::open and drive the test via the raw transport client ($clientTA)
only (send the Resume envelope with $clientTA->send and assert/read all replies
with $clientTA->receive), or stop using $clientTA->receive() and assert
replay/ack via the ARCPClient instance (use ARCPClient methods/events or its
receive/queueing API) so only ARCPClient reads the transport; adjust the code
around Envelope/Resume handling and checks that reference
$clientA->session->sessionId, $clientTA->send, and the receive loop to use the
single chosen consumer.

In `@tests/Integration/SubscriptionTest.php`:
- Around line 149-154: The test only performs negative checks and can pass when
$aObservedSessions is empty; add a positive assertion that A actually observed
its own session by asserting $aSessionId is present in $aObservedSessions (e.g.
call self::assertArrayHasKey($aSessionId, $aObservedSessions) or
self::assertContains($aSessionId, array_keys($aObservedSessions))) before
computing $foreign so the test verifies the subscription delivered at least one
envelope for A.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dffff26a-ed36-493a-8e2a-53a9fb08ab7c

📥 Commits

Reviewing files that changed from the base of the PR and between 09d40da and d2c4b0e.

📒 Files selected for processing (35)
  • README.md
  • docs/guides/auth.md
  • src/Auth/BearerAuth.php
  • src/Client/ARCPClient.php
  • src/Client/ResultChunkAssembler.php
  • src/Internal/Runtime/ArtifactDispatcher.php
  • src/Internal/Runtime/CredentialLifecycle.php
  • src/Internal/Runtime/LifecycleHandler.php
  • src/Internal/Runtime/SubscriptionRouter.php
  • src/Internal/Runtime/ToolInvocationHandler.php
  • src/Runtime/ARCPRuntime.php
  • src/Runtime/ArtifactStore.php
  • src/Runtime/CostBudget.php
  • src/Runtime/Credentials/InMemoryCredentialStore.php
  • src/Runtime/Job.php
  • src/Runtime/JobContext.php
  • src/Runtime/JobManager.php
  • src/Runtime/LeaseManager.php
  • src/Runtime/SubscriptionManager.php
  • src/Store/EventLog.php
  • src/Transport/StdioTransport.php
  • src/Transport/Transport.php
  • tests/Integration/ArtifactTest.php
  • tests/Integration/CredentialLifecycleTest.php
  • tests/Integration/HandshakeTest.php
  • tests/Integration/JobLifecycleTest.php
  • tests/Integration/ResumeTest.php
  • tests/Integration/StdioTransportTest.php
  • tests/Integration/SubscriptionTest.php
  • tests/Support/FakeDurableCredentialStore.php
  • tests/Unit/AuthTest.php
  • tests/Unit/Client/ResultChunkAssemblerTest.php
  • tests/Unit/LeaseManagerTest.php
  • tests/Unit/Runtime/CostBudgetTest.php
  • tests/Unit/Runtime/Credentials/InMemoryCredentialStoreTest.php

Comment on lines +45 to +51
if (preg_match('/^[0-9a-f]{64}$/', $normalized) !== 1) {
$this->lifecycle->nack(
$session,
$env,
'INVALID_ARGUMENT',
'artifact.put sha256 must be 64 lowercase hex chars',
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Adjust SHA-256 validation error text to match actual behavior.

Line 45 normalizes with strtolower(...), so uppercase hex is accepted. The current message says “must be 64 lowercase hex chars,” which is misleading.

Suggested fix
-                    'artifact.put sha256 must be 64 lowercase hex chars',
+                    'artifact.put sha256 must be 64 hex characters',
📝 Committable suggestion

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

Suggested change
if (preg_match('/^[0-9a-f]{64}$/', $normalized) !== 1) {
$this->lifecycle->nack(
$session,
$env,
'INVALID_ARGUMENT',
'artifact.put sha256 must be 64 lowercase hex chars',
);
if (preg_match('/^[0-9a-f]{64}$/', $normalized) !== 1) {
$this->lifecycle->nack(
$session,
$env,
'INVALID_ARGUMENT',
'artifact.put sha256 must be 64 hex characters',
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Internal/Runtime/ArtifactDispatcher.php` around lines 45 - 51, The error
message in the SHA-256 validation is misleading because $normalized is derived
with strtolower(), so uppercase hex is accepted; update the lifecycle->nack call
(the error string passed in the artifact.put SHA-256 validation block using
preg_match('/^[0-9a-f]{64}$/', $normalized)) to a neutral message such as
"artifact.put sha256 must be 64 hexadecimal characters" (or similar) that does
not claim lowercase-only hex.

Comment on lines +33 to +37
public function leaseFromArguments(
array $arguments,
ResolvedTool $resolved,
?Session $session = null,
): ?LeaseGranted {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Null-session fallback weakens lease isolation guarantees.

At Line 36 and Line 109–Line 113, a missing session falls back to leases->get(...), which bypasses session ownership enforcement. This leaves a reachable unsafe path if a caller omits the session argument.

Suggested hardening
-    public function leaseFromArguments(
+    public function leaseFromArguments(
         array $arguments,
         ResolvedTool $resolved,
-        ?Session $session = null,
+        Session $session,
     ): ?LeaseGranted {
@@
-    private function baseLease(mixed $leaseArg, ?Session $session): ?LeaseGranted
+    private function baseLease(mixed $leaseArg, Session $session): ?LeaseGranted
     {
         $leaseId = $this->extractLeaseId($leaseArg);
         if ($leaseId === null) {
             return null;
         }
-        $sessionId = $session?->sessionId;
-        if ($sessionId !== null) {
-            return $this->runtime->leases->getForSession($leaseId, $sessionId);
-        }
-        return $this->runtime->leases->get($leaseId);
+        $sessionId = $session->sessionId
+            ?? throw new InvalidArgumentException('lease lookup requires authenticated session');
+        return $this->runtime->leases->getForSession($leaseId, $sessionId);
     }

Also applies to: 102-113

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Internal/Runtime/CredentialLifecycle.php` around lines 33 - 37, The code
currently allows a null Session to fall back to leases->get(...) which bypasses
session ownership checks; update leaseFromArguments (and the other method(s)
that call leases->get around the 102–113 area) to require a non-null Session: if
$session is null throw an InvalidArgumentException (or specific domain
exception) instead of calling leases->get; and when retrieving a lease use a
session-aware lookup or enforce ownership by verifying the returned
LeaseGranted's session id matches $session->id (e.g. replace leases->get(...)
with a session-aware call or add a post-check that LeaseGranted->getSessionId()
=== $session->id and return null/throw if it does not). Ensure you reference
leaseFromArguments and the code paths that currently call leases->get to locate
and harden all fallbacks.

expiresAt: $expiresAt,
);
$this->runtime->leases->register($granted);
$this->runtime->leases->register($granted, $this->session->sessionId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the authoritative JobContext session id when registering granted leases.

Line 326 currently pulls from $this->session->sessionId, which can be nullable. This weakens the new session-ownership guarantee if a nullable session object slips through. Use the non-null $this->sessionId already carried by JobContext.

Suggested fix
-        $this->runtime->leases->register($granted, $this->session->sessionId);
+        $this->runtime->leases->register($granted, $this->sessionId);
📝 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
$this->runtime->leases->register($granted, $this->session->sessionId);
$this->runtime->leases->register($granted, $this->sessionId);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Runtime/JobContext.php` at line 326, Replace the nullable session
reference with the authoritative JobContext session id when registering leases:
in JobContext (where the call to $this->runtime->leases->register(...) occurs),
use $this->sessionId instead of $this->session->sessionId so the register call
uses the non-null JobContext session identifier; update the register invocation
in that method (and any nearby calls in the same class) to pass
$this->sessionId.

Comment on lines +33 to +35
* @throws \Arcp\Errors\TransportClosedException on unexpected
* transport failure (distinct from a clean close, which returns null).
* @throws \Amp\CancelledException when `$cancellation` fires.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align top-level transport close semantics with this method contract.

The receive() doc now correctly distinguishes clean close (null) from unexpected failure (TransportClosedException), but the interface header still says connection drops are seen as null. Please update that header text to avoid contradictory API expectations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Transport/Transport.php` around lines 33 - 35, Update the Transport
class/interface header to match the receive() method contract: change the
top-level wording that currently says connection drops are returned as null to
state that clean closes return null while unexpected transport failures throw
\Arcp\Errors\TransportClosedException; ensure this aligns with the receive()
docblock which already documents returning null on clean close and throwing
TransportClosedException on unexpected failure, and keep the
\Amp\CancelledException note for `$cancellation` consistent.

Comment on lines +137 to +166
$clientA = new ARCPClient($clientTA);
$clientB = new ARCPClient($clientTB);
$clientA->open(Auth::none(), new PeerInfo('cli-a', '0.1'), new Capabilities(anonymous: true));
$clientB->open(Auth::none(), new PeerInfo('cli-b', '0.1'), new Capabilities(anonymous: true));

// Both sessions push some events that get recorded in the event log.
$clientA->ping();
$clientB->ping();
$clientA->ping();

// Session A asks for a full replay from the beginning. The bug
// pre-fix would forward session B's envelopes too.
$resumeEnv = new Envelope(
id: MessageId::random(),
payload: new Resume(afterMessageId: ''),
timestamp: new \DateTimeImmutable('now'),
sessionId: $clientA->session->sessionId,
);
$clientTA->send($resumeEnv);

$foreignSessionId = (string) $clientB->session->sessionId;
$sawForeign = false;
$sawAck = false;
$loops = 0;
while (!$sawAck && $loops < 200) {
++$loops;
$env = $clientTA->receive();
if (!$env instanceof Envelope) {
break;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid dual consumers on the same transport in this test.

After open(), the ARCPClient read-loop is already consuming envelopes, but Line 163 also reads directly from $clientTA. That race makes this test nondeterministic (ack/replay envelopes may be consumed by either reader, causing flakes/hangs). Please drive assertions through a single consumer path (e.g., raw transport-only test client, or ARCPClient-managed pending/router path exclusively).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Integration/ResumeTest.php` around lines 137 - 166, The test currently
has two consumers reading from the same transport (ARCPClient's internal
read-loop started by ARCPClient::open and the direct $clientTA->receive()
calls), which causes nondeterministic races; fix by choosing one consumer path
and removing the other: either stop using ARCPClient::open and drive the test
via the raw transport client ($clientTA) only (send the Resume envelope with
$clientTA->send and assert/read all replies with $clientTA->receive), or stop
using $clientTA->receive() and assert replay/ack via the ARCPClient instance
(use ARCPClient methods/events or its receive/queueing API) so only ARCPClient
reads the transport; adjust the code around Envelope/Resume handling and checks
that reference $clientA->session->sessionId, $clientTA->send, and the receive
loop to use the single chosen consumer.

Comment on lines +149 to +154
$aSessionId = (string) $clientA->session->sessionId;
$bSessionId = (string) $clientB->session->sessionId;
self::assertArrayNotHasKey($bSessionId, $aObservedSessions);
// Allow A to see its own envelopes (e.g. backfill marker).
$foreign = array_diff(array_keys($aObservedSessions), [$aSessionId]);
self::assertEmpty($foreign);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test can pass without proving the subscription actually delivered any envelope.

At Line 151–Line 154, the assertions are only negative checks. If $aObservedSessions is empty, this still passes. Add a positive assertion that A observed at least its own session id.

Suggested patch
         $aSessionId = (string) $clientA->session->sessionId;
         $bSessionId = (string) $clientB->session->sessionId;
+        self::assertArrayHasKey($aSessionId, $aObservedSessions, 'expected at least one envelope for subscriber session');
         self::assertArrayNotHasKey($bSessionId, $aObservedSessions);
         // Allow A to see its own envelopes (e.g. backfill marker).
         $foreign = array_diff(array_keys($aObservedSessions), [$aSessionId]);
         self::assertEmpty($foreign);
📝 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
$aSessionId = (string) $clientA->session->sessionId;
$bSessionId = (string) $clientB->session->sessionId;
self::assertArrayNotHasKey($bSessionId, $aObservedSessions);
// Allow A to see its own envelopes (e.g. backfill marker).
$foreign = array_diff(array_keys($aObservedSessions), [$aSessionId]);
self::assertEmpty($foreign);
$aSessionId = (string) $clientA->session->sessionId;
$bSessionId = (string) $clientB->session->sessionId;
self::assertArrayHasKey($aSessionId, $aObservedSessions, 'expected at least one envelope for subscriber session');
self::assertArrayNotHasKey($bSessionId, $aObservedSessions);
// Allow A to see its own envelopes (e.g. backfill marker).
$foreign = array_diff(array_keys($aObservedSessions), [$aSessionId]);
self::assertEmpty($foreign);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Integration/SubscriptionTest.php` around lines 149 - 154, The test only
performs negative checks and can pass when $aObservedSessions is empty; add a
positive assertion that A actually observed its own session by asserting
$aSessionId is present in $aObservedSessions (e.g. call
self::assertArrayHasKey($aSessionId, $aObservedSessions) or
self::assertContains($aSessionId, array_keys($aObservedSessions))) before
computing $foreign so the test verifies the subscription delivered at least one
envelope for A.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bearer authentication lets clients override the token principal

1 participant