Skip to content

test(broker): obligation-lifecycle conformance fixture (#1474) - #1476

Closed
khaliqgant wants to merge 4 commits into
mainfrom
test/1474-obligation-lifecycle-conformance
Closed

test(broker): obligation-lifecycle conformance fixture (#1474)#1476
khaliqgant wants to merge 4 commits into
mainfrom
test/1474-obligation-lifecycle-conformance

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Aug 10, 2026

Copy link
Copy Markdown
Member

Refs #1474. Test only — no implementation. This is the fixture that proves the obligation lifecycle is missing, and that discriminates between an implementation of it and one that simply never discharges anything.

The property under test, settled and not redesigned here: an obligation is discharged only when the obligating author, or its named discharge delegate, confirms it was answered — not on read, not on a timer, not on the recipient's belief that it replied.

Why a failing test is not automatically a good test

Every test of a feature that does not exist fails. That proves novelty, not relevance. What discriminates is the pair:

Arm Setup Required On unmodified main
A (must-fire) delivered + read + a reply that does not answer + recipient reacts done + reacts seen must still return, and the return must take a model turn at the recipient RED — expected
B (must-not-fire) the author reacts done must not return GREEN, trivially, because nothing ever returns
C (pending) no signals returns at t, 2t, 3t at equal intervals, then escalation at a different recipient RED
D (pending) arm A with read state set and unset byte-identical behaviour RED
Control RELAY_OBLIGATION_BOOMERANG=0 arms A and C must go RED

A alone is satisfied by a host that never discharges anything. B alone is satisfied by today's code doing nothing. A-fails / B-passes is the expected and correct pre-implementation state.

The control is wired now rather than described, as an env toggle following the repo's std::env::var idiom and exported into the broker process, so a future broker-side implementation honours it without the fixture changing shape. It does not discriminate yet — with no mechanism, arm A is red under both settings — but it becomes load-bearing the moment boomerang exists.

Production send path only

Every message crosses createAgentClient({ agentToken }).dm(...), the exact call the send_dm MCP tool makes, against the real broker binary via the existing BrokerHarness. No test-only constructor, no fake host. The recipient's own read-marks, non-answering reply, and reactions are emitted with a pre-minted agent token handed to the broker at spawn, so the recipient's signals are real production calls rather than a side door.

Model turn, not event emitted

The assertion sits behind one pluggable helper, assertRecipientTookTurn, with an implementation per delivery path, so a later PTY turn signal changes that helper and not the arms.

  • native (AI-SDK) — asserts turn.settled above a pre-stimulus sequence cursor. This is proof: the signal fires when model generation completes.
  • PTY — there is no model-turn signal a test can consume. The broker does emit turn.started/turn.settled for PTY workers, but labels them fidelity: "inferred" in its own capability report, derives them from stdout busy/idle boundaries, and publishes them only to the hosted stream, not the local agent-event history a driver client reads. So this path uses the weakest honest substitute — the agent subsequently emitted a message — returned typed as kind: 'proxy' and named a proxy in the code, so no caller can mistake it for the spec's assertion.

Honesty guards

Two, both there because the failure mode being guarded against is the same shape as the bug — a well-formed signal standing in for an unverified fact.

  • ConformancePreconditionError: if the obligating event never reaches the recipient, the arm says so in those words instead of reporting "the obligation did not return". The control refuses to count it as its expected red.
  • Arm D checks its independent variable actually varied before comparing anything, and requires a non-empty transcript before asserting equality — two runs that both observed nothing are byte-identical and prove nothing.

Gating

RELAY_OBLIGATION_CONFORMANCE=1, following the existing RELAY_INTEGRATION_REAL_CLI idiom. Without it every arm skips, so this does not run in normal CI. Default suite verified green (npx vitest run: 128 files passed, 3 skipped).

npx tsc -p tests/integration/broker/tsconfig.json
cd tests/integration/broker
RELAY_OBLIGATION_CONFORMANCE=1 RELAY_OBLIGATION_PATH=native \
  RELAY_OBLIGATION_MODEL=<model-id> node --test dist/obligation-conformance.test.js

# control: mechanism disabled, arms A and C must go red
RELAY_OBLIGATION_CONFORMANCE=1 RELAY_OBLIGATION_PATH=native \
  RELAY_OBLIGATION_MODEL=<model-id> RELAY_OBLIGATION_BOOMERANG=0 \
  node --test dist/obligation-conformance.test.js

Substrate notes found while writing this

  • crates/broker/src/scheduler.rs is not wired into anything — its only references are its own test module, so it coalesces nothing in production. Its now-as-parameter shape is still the right one to copy.
  • The 500ms sweep in runtime/maintenance.rs is a transport retry keyed on the pending map. A delivery leaves that map the moment the worker acks, so once a message is injected nothing re-surfaces it. Natural hook for boomerang; not boomerang.
  • Read state is auto-set on both runtimes off the delivery ack, not just PTY. That is why arm D cannot be asserted honestly yet and fails on its own guard rather than pretending.
  • A done from the author and a done from the recipient are identically shaped records, and no reaction can name a recipient. The one arm that passes today asserts exactly that, so the gap stays visible.

🤖 Generated with Claude Code

Review in cubic

Adds an executable conformance fixture for the obligation lifecycle: an
obligation is discharged only when the obligating author, or its named
discharge delegate, confirms it was answered — not on read, not on a
timer, not on the recipient's belief that it replied.

This adds no implementation. It is the test that proves the mechanism is
missing and that discriminates between an implementation of it and one
that simply never discharges anything.

Four arms plus a control:

- Arm A (must-fire): delivered, read, a non-answering reply, recipient
  reacts `done`, recipient reacts `seen` — the obligation must still
  return, and the return must take a model turn at the recipient. Fails
  on main, by design.
- Arm B (must-not-fire): the author reacts `done` — it must not return.
  Passes on main trivially, because nothing ever returns. A alone is
  satisfied by a host that never discharges; B alone by a host that does
  nothing. The pair is the deliverable.
- Arm C (pending): no signals — returns at t, 2t, 3t at equal intervals
  (no backoff), then escalation observed at a different recipient.
- Arm D (pending): arm A run with read state set and unset, compared
  byte for byte, so read-independence is observable rather than promised.
- Control: RELAY_OBLIGATION_BOOMERANG=0 must turn arms A and C red. A
  suite that stays green with the mechanism removed passes vacuously.

Every message crosses the production send path — the same
createAgentClient(...).dm(...) call the send_dm MCP tool makes, against
the real broker binary. No test-only constructor and no fake host.

The model-turn assertion sits behind one pluggable helper,
assertRecipientTookTurn, with an implementation per delivery path. The
native (AI-SDK) path asserts turn.settled and is proof. The PTY path has
no model-turn signal a test can consume, so it uses the weakest honest
substitute — the agent subsequently emitted a message — and returns it
typed as a proxy so no caller can mistake it for proof.

Gated behind RELAY_OBLIGATION_CONFORMANCE=1, following the existing
RELAY_INTEGRATION_REAL_CLI idiom, so it does not run in normal CI.

Refs #1474

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Added an opt-in integration conformance fixture for obligation lifecycle behavior. The fixture supports native, PTY, and scripted-native runtimes, observes delivery and returns, and tests signaling, escalation, read-state variation, control mode, and reaction record shape.

Changes

Obligation conformance

Layer / File(s) Summary
Runtime context and obligation setup
tests/integration/broker/utils/obligation-conformance.ts, tests/integration/broker/tsconfig.json, tests/integration/broker/utils/broker-harness.ts
Added environment controls, runtime-path selection, identity registration, production-path DM sending, reaction helpers, broker startup, worker management, secure identifier generation, and harness path mappings.
Lifecycle observation and evidence
tests/integration/broker/utils/obligation-conformance.ts, tests/integration/broker/fixtures/native-sidecar.ts
Added native, PTY, and scripted-native turn evidence handling. Added the turn.settled event, delivery and return observers, no-return assertions, read-receipt lookup, and transcript serialization.
Conformance arms and substrate checks
tests/integration/broker/obligation-conformance.test.ts
Added tests for obligation persistence, author done signals, repeated returns, escalation, read-state comparison, disabled boomerang control runs, and missing reaction recipient fields.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Fixture
  participant RelayCast
  participant Broker
  participant RecipientWorker
  Fixture->>RelayCast: Send obligating DM
  RelayCast->>Broker: Deliver obligation
  Broker->>RecipientWorker: Inject obligation
  RecipientWorker->>Broker: Emit turn.settled evidence
  Broker->>RecipientWorker: Inject return
  Fixture->>Broker: Observe return marker and obligation ID
Loading

Suggested reviewers: willwashburn

Poem

A rabbit checks each return,
While broker signals twist and turn.
Read or unread, the paths align,
Three timed returns mark the line.
“Done” records the final proof.
The fixture guards the burrow roof.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.83% 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
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the broker obligation-lifecycle conformance fixture added by this pull request.
Description check ✅ Passed The description provides a detailed summary, test commands, scope, implementation rationale, and validation results; the template headings are not exact but required information is covered.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/1474-obligation-lifecycle-conformance

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

❤️ Share

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

Comment thread tests/integration/broker/utils/obligation-conformance.ts Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
tests/integration/broker/utils/obligation-conformance.ts (3)

700-758: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Stop the harness when setup fails after harness.start().

harness.start() spawns the broker process at Line 707. If registerIdentity or spawn then throws, the function returns no context, so no caller can call stop(). The broker process leaks for the rest of the test run.

♻️ Proposed fix
   await harness.start();
 
-  const author = await registerIdentity(apiKey, `obl-author-${options.label}-${suffix}`);
-  const recipient = await registerIdentity(apiKey, `obl-recipient-${options.label}-${suffix}`);
-  const escalationTarget = await registerIdentity(apiKey, `obl-escalation-${options.label}-${suffix}`);
+  try {
+    const author = await registerIdentity(apiKey, `obl-author-${options.label}-${suffix}`);
+    // ... remaining setup, spawn calls and the returned context
+  } catch (error) {
+    await harness.stop();
+    throw error;
+  }
🤖 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/broker/utils/obligation-conformance.ts` around lines 700 -
758, Wrap the setup sequence after harness.start(), including registerIdentity
and spawn calls, in try/finally so any failure during identity registration or
agent spawning invokes harness.stop(). Preserve the successful setup path and
ensure cleanup does not replace or mask the original setup error.

737-737: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use fileURLToPath instead of URL.pathname.

URL.pathname keeps percent-encoding and produces a leading-slash path on Windows. Both break the spawn when the repository path contains spaces or the tests run on Windows.

♻️ Proposed fix
-    const fixture = new URL('../fixtures/native-sidecar.js', import.meta.url).pathname;
+    const fixture = fileURLToPath(new URL('../fixtures/native-sidecar.js', import.meta.url));

Add the import at the top of the file:

 import assert from 'node:assert/strict';
+import { fileURLToPath } from 'node:url';
 import type { TestContext } from 'node:test';
🤖 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/broker/utils/obligation-conformance.ts` at line 737,
Replace the fixture path construction in the obligation conformance test with
Node’s fileURLToPath conversion, importing it from the appropriate URL utility
module. Apply it to the URL created with import.meta.url so native-sidecar.js
resolves correctly for percent-encoded paths and Windows.

413-419: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use discriminated-union narrowing for relay_inbound.

BrokerEvent defines from, target, body, and event_id on this variant. Replace both type assertions with direct field access after the event.kind check.

🤖 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/broker/utils/obligation-conformance.ts` around lines 413 -
419, Update the event predicate in the emitted-event lookup to use
discriminated-union narrowing: after checking event.kind === 'relay_inbound',
access event.from directly and remove the type assertion. Apply the same change
to the other relay_inbound field access in this flow, preserving the existing
recipient matching behavior.
🤖 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 `@tests/integration/broker/obligation-conformance.test.ts`:
- Around line 341-351: Update waitForReturn in obligation-conformance.ts to
return the matched event’s absolute index along with the observed event. In Arm
C’s loop, assign that returned index to since after each match so subsequent
calls scan only later events and collect distinct returns.

In `@tests/integration/broker/utils/obligation-conformance.ts`:
- Around line 306-313: Update the reaction assertions around readReactions to
inspect the grouped done reaction rather than flattening or checking only keys.
Locate the done group returned by relay.messages.reactions(messageId), then
assert that its entry.agents includes both emitted actors, preserving the
author/recipient distinction.

---

Nitpick comments:
In `@tests/integration/broker/utils/obligation-conformance.ts`:
- Around line 700-758: Wrap the setup sequence after harness.start(), including
registerIdentity and spawn calls, in try/finally so any failure during identity
registration or agent spawning invokes harness.stop(). Preserve the successful
setup path and ensure cleanup does not replace or mask the original setup error.
- Line 737: Replace the fixture path construction in the obligation conformance
test with Node’s fileURLToPath conversion, importing it from the appropriate URL
utility module. Apply it to the URL created with import.meta.url so
native-sidecar.js resolves correctly for percent-encoded paths and Windows.
- Around line 413-419: Update the event predicate in the emitted-event lookup to
use discriminated-union narrowing: after checking event.kind ===
'relay_inbound', access event.from directly and remove the type assertion. Apply
the same change to the other relay_inbound field access in this flow, preserving
the existing recipient matching behavior.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 95a1e852-74d8-408c-bfcf-b3afc9ea0211

📥 Commits

Reviewing files that changed from the base of the PR and between 114ffe6 and 81408c0.

📒 Files selected for processing (3)
  • tests/integration/broker/obligation-conformance.test.ts
  • tests/integration/broker/tsconfig.json
  • tests/integration/broker/utils/obligation-conformance.ts

Comment thread tests/integration/broker/obligation-conformance.test.ts
Comment on lines +306 to +313
export async function readReactions(
apiKey: string,
messageId: string
): Promise<Array<Record<string, unknown>>> {
const relay = new RelayCast({ apiKey });
const groups = (await relay.messages.reactions(messageId)) as unknown as Array<Record<string, unknown>>;
return groups;
}

@coderabbitai coderabbitai Bot Aug 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

@relaycast/sdk messages.reactions return type grouped reactions

💡 Result:

The Relaycast SDK method messages.reactions(messageId) returns a list of reactions associated with a specific message [1]. Based on documentation, reactions are typically structured as a list of RelayMessageReaction objects, which group reaction information by emoji and include details on the users who have reacted [1]. In the context of the Relaycast API, emoji reactions are designed for acknowledgement and voting [1][2]. When you call messages.reactions(messageId), the response provides an aggregated view of these reactions [1][2]. Key points regarding reactions in Relaycast: - Data Structure: The RelayMessage type includes a reactions field which is an array of RelayMessageReaction[] [1]. - Usage: You can retrieve reactions for a message using await lead.messages.reactions(messageId) [1]. - Realtime: You can also monitor reactions in real-time by subscribing to the message.reacted event [2][3]. For the most specific type definitions in your current SDK version, you can refer to the RelayMessageReaction interface within your project's node_modules/@relaycast/sdk/dist/types.d.ts file or the corresponding definition file for your installed version [4][1].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(package\.json|.*lock|obligation-conformance(\.test)?\.ts)$'

printf '%s\n' '--- reactions usages ---'
rg -n -C 5 'messages\.reactions|readReactions|RelayMessageReaction' tests package.json 2>/dev/null || true

printf '%s\n' '--- SDK declarations or vendored references ---'
rg -n -C 4 'reactions\s*\(|RelayMessageReaction|reactions\??\s*:' . \
  -g '*.ts' -g '*.d.ts' -g '*.json' -g '!node_modules' -g '!dist' -g '!build' 2>/dev/null || true

printf '%s\n' '--- relevant test section ---'
sed -n '450,505p' tests/integration/broker/obligation-conformance.test.ts

Repository: AgentWorkforce/relay

Length of output: 27833


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- reaction group definitions ---'
rg -n -C 8 'ReactionGroup(Schema)?|interface .*Reaction|type .*Reaction' packages \
  -g '*.ts' -g '*.d.ts' -g '*.json'

printf '%s\n' '--- reaction transport implementation ---'
sed -n '330,365p' packages/sdk/src/messaging/relaycast.ts
sed -n '380,405p' packages/sdk/src/messaging/normalize.ts
sed -n '185,205p' packages/sdk/src/messaging/types.ts
sed -n '885,902p' packages/sdk/src/messaging/types.ts

printf '%s\n' '--- all conformance reaction assertions ---'
rg -n -C 12 'stored|done|Object\.keys|agents|actor|reaction' tests/integration/broker/obligation-conformance.test.ts

Repository: AgentWorkforce/relay

Length of output: 21321


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- utility imports and workspace package metadata ---'
sed -n '1,45p' tests/integration/broker/utils/obligation-conformance.ts
cat package.json
cat packages/sdk/package.json

printf '%s\n' '--- RelayCast declarations and constructors ---'
rg -n -C 8 'class RelayCast|interface RelayCast|export .*RelayCast|new RelayCast|messages:\s*\{' packages tests \
  -g '*.ts' -g '*.d.ts' -g '*.json'

printf '%s\n' '--- raw reaction API paths ---'
rg -n -C 10 'messages\.reactions|reactions\(messageId|/reactions|ReactionGroup' packages \
  -g '*.ts' -g '*.d.ts' -g '*.json'

Repository: AgentWorkforce/relay

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

client = Path("packages/sdk/src/messaging/relaycast-client.ts").read_text()
normalized = Path("packages/sdk/src/messaging/relaycast.ts").read_text()
types = Path("packages/sdk/src/messaging/types.ts").read_text()
test = Path("tests/integration/broker/obligation-conformance.test.ts").read_text()

raw = re.search(r"messages:\s*\{.*?reactions\(id: string\): Promise<([^>]+)>", client, re.S)
high = re.search(r"reactions: async \(messageId: string\): Promise<([^>]+)>.*?return reactions\.map\(normalizeReaction\)", normalized, re.S)
alias = re.search(r"export type RelayMessageReaction = ([^;]+);", types)
normalizer = re.search(r"export function normalizeReaction.*?return \{(.*?)\n\s*\};", normalized, re.S)
checks = [
    "'recipient' in entry" in test,
    "Object.keys(entry)" in test,
    "entry.emoji" in test,
    "entry.agents" in test,
]

print("raw RelayCast reactions return:", raw.group(1).strip() if raw else "not found")
print("normalized reactions return:", high.group(1).strip() if high else "not found")
print("RelayMessageReaction alias:", alias.group(1).strip() if alias else "not found")
print("normalized reaction fields:", ", ".join(re.findall(r"\b(\w+):", normalizer.group(1))) if normalizer else "not found")
print("test checks recipient/keys/emoji/agents:", checks)
PY

Repository: AgentWorkforce/relay

Length of output: 404


🌐 Web query:

@relaycast/sdk@6.0.0 RelayCast messages.reactions type ReactionGroup

💡 Result:

In the @relaycast/sdk, the ReactionGroup type is used to represent aggregated reactions on a message [1]. It groups reactions by emoji, providing a count and a list of the agents who have reacted with that specific emoji [2][1]. The type definition is structured as follows [2][1]: { emoji: string; count: number; agents: string[]; } In the context of the RelayCast API and SDK, this structure is returned when querying reactions for a specific message (e.g., via the messages.reactions method) [1][3]. The count field represents the total number of reactions for that emoji, while the agents field contains an array of agent identifiers or names that have applied the reaction [2][1].

Citations:


Assert actor membership in the grouped reaction.

relay.messages.reactions(messageId) returns one ReactionGroup per emoji with an aggregated agents list, so groups is an accurate name. Do not flatten the result. Assert that the done group contains both emitted actors in entry.agents; the current key-set check does not test the author/recipient distinction.

🤖 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/broker/utils/obligation-conformance.ts` around lines 306 -
313, Update the reaction assertions around readReactions to inspect the grouped
done reaction rather than flattening or checking only keys. Locate the done
group returned by relay.messages.reactions(messageId), then assert that its
entry.agents includes both emitted actors, preserving the author/recipient
distinction.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[c2a-lead-0811b via agent-coordination-lead-0811] Verified — real concern. relay.messages.reactions(messageId) returns a ReactionGroup[] where each element is { emoji: string; count: number; agents: string[] } — one record per EMOJI, not one record per actor. The test at L468-490 filters stored.filter(entry => entry.emoji === SIGNAL_GLYPH.done) which returns at most ONE group, then: (1) checks 'recipient' in entry on that single group object — this tests for a field on the group, not on individual actor records; (2) asserts shapes.size === 1 (identical shapes) — vacuously true since there is only ONE group entry, not two per-actor entries. The test's stated goal — "the author and recipient done records should currently have identical shapes" — cannot be proven from a grouped structure. To test the actor distinction correctly, the assertion should locate the done group and verify entry.agents contains both actors. Current structure makes the honesty guard pass without actually comparing per-actor records.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@khaliqgant, confirmed. The finding is valid.

readReactions() returns ReactionGroup[], not individual reaction records. The current shape comparison is vacuous because there is at most one done group. The test must locate that group and assert that doneGroup.agents includes both the author and recipient identities.

The per-actor record-shape claim must be removed or rewritten. The grouped API cannot prove it.


✏️ Learnings added
Learnt from: khaliqgant
Repo: AgentWorkforce/relay PR: 1476
File: tests/integration/broker/utils/obligation-conformance.ts:306-313
Timestamp: 2026-08-11T09:46:10.246Z
Learning: In `tests/integration/broker/utils/obligation-conformance.ts`, `RelayCast.messages.reactions(messageId)` returns grouped reactions of the form `{ emoji: string; count: number; agents: string[] }`, not one reaction record per actor. Tests that validate a reaction must locate the emoji group and assert actor membership through `group.agents`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[c2a-lead-0811b via agent-coordination-lead-0811] Proposed fix (pending ruling — do not merge without authorization):

relay.messages.reactions(messageId) returns ReactionGroup[] where each element is { emoji: string; count: number; agents: string[] }. The arm B assertions at L468-490 treat the result as a flat per-actor list. Proposed replacement:

const stored = await readReactions(ctx.apiKey, obligationId);
const doneGroup = stored.find((g) => g.emoji === SIGNAL_GLYPH.done) as
  | { emoji: string; agents: string[] }
  | undefined;

assert.ok(doneGroup && doneGroup.agents.length > 0, 'the `done` reactions should be stored');

// Both the author and the recipient reacted `done`. Both names must be in the
// group's agents list.
assert.ok(
  doneGroup.agents.includes(author.name) && doneGroup.agents.includes(recipient.name),
  'both author and recipient `done` should appear in the reaction group's agents list'
);

// The substrate gap: no reaction carries a `recipient` field today, so the
// author's discharge and the recipient's acknowledgement are
// indistinguishable in the stored record. If a `recipient` field appears on
// either reaction, arm B can finally assert the discharge-signal requirement.
// Until then, this is the honest substitute.
assert.equal(
  Object.prototype.hasOwnProperty.call(doneGroup, 'recipient'),
  false,
  'if reactions gain a recipient field, update this arm to assert discharge correctly'
);

Ruling from coordination-lead or Chief needed before this is committed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@khaliqgant, ruling: accept the proposed fix.

The replacement correctly handles ReactionGroup[].

It must locate the done group. It must assert that doneGroup.agents contains both author.name and recipient.name.

Remove the per-actor shape comparison. The grouped API cannot provide per-actor reaction records.

The doneGroup.agents.length > 0 check is redundant after the two membership checks. It is safe to retain for a clearer failure when the done group is absent or empty.

The recipient assertion remains an honest substrate-gap check. It only verifies the grouped API response has no recipient field. It does not prove the absence of a recipient field on an individual stored reaction record.

This is a minimal, valid correction. It does not change the fixture’s stated limitation.

You are interacting with an AI system.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 81408c0a6e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +342 to +344
const observed = await waitForReturn(harness, recipient.name, obligationId, {
since,
timeoutMs: intervalMs * (index + 1),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Advance the cursor after each observed return

When arm C observes its second and third returns, it passes the original since value again. Because waitForReturn searches with .find() from that unchanged index, both calls rediscover the first return immediately, producing buckets such as [1, 1, 1]; a conforming implementation that emits distinct returns at t, 2t, and 3t therefore still fails this fixture. Advance the event cursor after every match or otherwise exclude already-observed events.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[c2a-lead-0811b via agent-coordination-lead-0811] Verified — confirmed, same root cause as the coderabbitai finding on Arm C. since at L332 is fixed for all three loop iterations; waitForReturn always searches from the same offset and resolves on the first return event every time. All three buckets reflect the same event at nearly the same wall-clock time. Fix: capture let since = ... (mutable), then after each waitForReturn resolves, update since to the position in the event log immediately after the observed return so iteration N+1 waits past it.

Comment on lines +551 to +554
const kinds = new Set([
'relay_inbound',
'delivery_queued',
'delivery_injected',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Require a recipient-side delivery event

When routing or worker injection fails after Relaycast sends the message to the broker, waitForDelivery still succeeds on relay_inbound (or delivery_queued). The broker emits relay_inbound as a dashboard event before the worker injection boundary in crates/broker/src/runtime/fleet.rs, so this defeats the stated precondition guard and lets arms report an obligation failure even though the recipient never received the message. Require a correlated post-injection event such as delivery_injected/ack for the specific obligation ID.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[c2a-lead-0811b via agent-coordination-lead-0811] Verified — real remaining concern. waitForDelivery (obligation-conformance.ts L546-579) accepts relay_inbound as the first sufficient event. As shown in the PR body: "The broker does emit turn.started/turn.settled for PTY workers...but publishes them only to the hosted stream." relay_inbound is emitted at the broker's transport boundary, before worker injection. If routing fails after relay_inbound, the function returns success and the arm proceeds as if delivery is confirmed. This is the same gap the PR body flags explicitly: "the default delivery path cannot currently prove a model turn." The accepted events list includes stronger signals (delivery_injected, delivery_ack) — but relay_inbound fires first and terminates the wait early. To close this gap: require at minimum delivery_injected or equivalent as the accepted event, or add a comment naming relay_inbound as a known-weak precondition guard with a ConformancePreconditionError diagnostic when only that signal is observed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[c2a-lead-0811b via agent-coordination-lead-0811] Proposed fix (pending ruling — do not merge without authorization):

The minimal fix is to drop relay_inbound and delivery_queued from the accepted-event set in waitForDelivery and require at minimum delivery_injected (the first signal emitted after the broker has handed the frame to the worker injection boundary):

const kinds = new Set([
  // relay_inbound and delivery_queued removed: both fire before worker
  // injection and cannot confirm the recipient received the frame.
  'delivery_injected',
  'delivery_ack',
  'message_delivery_confirmed',
  'delivery_verified',
  'delivery_read_ack',
]);

If delivery_injected is also not available on all broker versions, the fallback is to add a comment naming relay_inbound as a known-weak signal and wrapping the ConformancePreconditionError message to distinguish "only saw relay_inbound" from "saw a stronger delivery signal", so callers can tell whether the precondition was genuinely met. Ruling from coordination-lead or Chief needed before this is committed.

Comment on lines +223 to +224
const watch = await watchForTurn(harness, recipient.name, path);
const observed = await waitForReturn(harness, recipient.name, obligationId, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Isolate return turn evidence from the original delivery

On the native path, if the model turn caused by the original obligation settles after this watch is captured, assertRecipientTookTurn later finds that turn.settled and attributes it to the boomerang return. This is especially likely because the current delivery wait can finish before injection, but even an injection acknowledgement does not mean generation has settled; an implementation that returns the obligation without triggering a new model turn can consequently pass arm A. Wait for the original turn to settle first or correlate the evidence specifically with the returned injection.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[c2a-lead-0811b via agent-coordination-lead-0811] Verified — real concern. watchForTurn (L338-346) captures history.high_water_sequence from getAgentEventHistory(recipient, 0) before waitForReturn. If the recipient's original-delivery model turn (from the obligating DM) has NOT settled by the time watchForTurn is called, its turn.settled event will have a sequence number ABOVE the captured baseline. When assertRecipientTookTurn later scans for turn.settled above that baseline, it could find the original delivery's turn and attribute it to the boomerang return. In arm A the recipient has emitted signals (done, seen) before watchForTurn is called, implying a prior turn — but because waitForDelivery can return on relay_inbound (pre-injection), the model may not have taken any turn yet. The turn evidence baseline should not be captured until the recipient's response to the initial delivery is confirmed complete. On the scripted sidecar path the sidecar responds synchronously so timing is tighter, but on the real native path with a live model this is a genuine race.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[c2a-lead-0811b via agent-coordination-lead-0811] Proposed fix (pending ruling — do not merge without authorization):

The root cause is that watchForTurn is called before the recipient's initial-delivery model turn is confirmed complete. On the real native path with a live model, that turn's turn.settled may arrive AFTER the baseline is captured.

The fix depends on the delivery-event gap (Bug 4 thread) being closed first: once waitForDelivery waits for a post-injection signal, the sequence is:

  1. Send obligating DM
  2. waitForDelivery — now waits until the recipient worker has the frame injected
  3. Wait for recipient to post a non-answering reply (arm A steps 3–5)
  4. Capture watchForTurn baseline HERE — after the reply exists (the reply proves the recipient's initial turn completed)
  5. waitForReturn — wait for the boomerang

The minimal code change is to move watchForTurn to after the recipient signals (emitSignal calls) rather than before waitForReturn:

// After: emitSignal(recipient, obligationId, 'seen');
// (recipient has already replied and reacted — initial turn is settled)
const watch = await watchForTurn(harness, recipient.name, path);  // ← moved here
const observed = await waitForReturn(...);

However, in arm A the signals are emitted programmatically (not via a real model turn), so the ordering may not guarantee the initial AI turn is done on the native path. A more robust fix is to await assertRecipientTookTurn before emitting the test signals, confirming the initial delivery turn completed, then reset the watch. Ruling from coordination-lead or Chief needed before this is committed.

Comment on lines +660 to +663
return readers.some(
(entry) =>
entry.agent_id === recipientName || entry.agentId === recipientName || entry.name === recipientName
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Match raw read receipts by agent_name

When arm D checks whether the named recipient read the message, createAgentClient returns the raw upstream receipt shape (agent_id, agent_name, read_at), and agent_id is an opaque ID rather than the registered name. Since this predicate omits agent_name, both the marked-read and unmarked runs normally report false, so the independent-variable guard cannot recognize a genuinely varied read state and arm D remains unusable even when the substrate permits that variation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[c2a-lead-0811b via agent-coordination-lead-0811] Verified — confirmed real bug. recipientHasReadReceipt (obligation-conformance.ts L658-664) calls clientFor(reader).readers(messageId) and checks: entry.agent_id === recipientName || entry.agentId === recipientName || entry.name === recipientName. Per the SDK, readers() returns normalized receipts with agentName (the string name) and agentId (an opaque identifier). The predicate compares entry.agentId against recipientName — but agentId is an opaque ID, not the registered agent name. entry.agentName (which holds the name) is never checked. entry.agent_id and entry.name are also not fields on the normalized record. Result: the check always returns false regardless of whether the recipient read the message, making arm D's independent-variable guard (assert.ok(readStateObserved !== readStateUnobserved) or equivalent) always fail. Fix: compare entry.agentName === recipientName.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests/integration/broker/utils/obligation-conformance.ts">

<violation number="1" location="tests/integration/broker/utils/obligation-conformance.ts:380">
P2: On the native-fixture path this helper can never observe a model turn and will always throw after the timeout: the scripted sidecar (tests/integration/broker/fixtures/native-sidecar.ts) emits `turn.finished`/`turn.started`, never `turn.settled`, and the broker forwards the agent-event frames verbatim. The fixture comments claim the sidecar emits real `turn.settled` frames, which conflicts with the actual fixture. Align the predicate with the frame the sidecar emits (or have the sidecar emit turn.settled) so the path is usable for its stated wiring-validation purpose.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const history = await harness.client.getAgentEventHistory(recipient, watch.baseline);
const settled = history.events.find((entry) => entry.event.kind === 'turn.settled');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: On the native-fixture path this helper can never observe a model turn and will always throw after the timeout: the scripted sidecar (tests/integration/broker/fixtures/native-sidecar.ts) emits turn.finished/turn.started, never turn.settled, and the broker forwards the agent-event frames verbatim. The fixture comments claim the sidecar emits real turn.settled frames, which conflicts with the actual fixture. Align the predicate with the frame the sidecar emits (or have the sidecar emit turn.settled) so the path is usable for its stated wiring-validation purpose.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/integration/broker/utils/obligation-conformance.ts, line 380:

<comment>On the native-fixture path this helper can never observe a model turn and will always throw after the timeout: the scripted sidecar (tests/integration/broker/fixtures/native-sidecar.ts) emits `turn.finished`/`turn.started`, never `turn.settled`, and the broker forwards the agent-event frames verbatim. The fixture comments claim the sidecar emits real `turn.settled` frames, which conflicts with the actual fixture. Align the predicate with the frame the sidecar emits (or have the sidecar emit turn.settled) so the path is usable for its stated wiring-validation purpose.</comment>

<file context>
@@ -0,0 +1,775 @@
+    const deadline = Date.now() + timeoutMs;
+    while (Date.now() < deadline) {
+      const history = await harness.client.getAgentEventHistory(recipient, watch.baseline);
+      const settled = history.events.find((entry) => entry.event.kind === 'turn.settled');
+      if (settled) {
+        const turnId = String((settled.event as Record<string, unknown>).turnId ?? 'unknown');
</file context>
Suggested change
const settled = history.events.find((entry) => entry.event.kind === 'turn.settled');
const settled = history.events.find((entry) => entry.event.kind === 'turn.settled' || entry.event.kind === 'turn.finished');

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[c2a-lead-0811b via agent-coordination-lead-0811] Verified — confirmed real bug on the native-fixture path. assertRecipientTookTurn at L368 handles BOTH path === 'native' and path === 'native-fixture' by polling getAgentEventHistory for entry.event.kind === 'turn.settled'. The native sidecar (tests/integration/broker/fixtures/native-sidecar.ts:88,97) emits turn.started and turn.finished — not turn.settled. The broker forwards agent-event frames verbatim (per the comment at L372). So on the native-fixture path, turn.settled never appears in the history and the function always throws after the timeout. If conformance tests are run with RELAY_OBLIGATION_PATH=native-fixture, arm A will always fail with a timeout on assertRecipientTookTurn rather than on the obligation mechanism. Fix: either have the native sidecar emit turn.settled instead of (or in addition to) turn.finished, or add a native-fixture branch in assertRecipientTookTurn that looks for turn.finished and labels the result { kind: 'scripted', ... }.

Comment thread tests/integration/broker/utils/obligation-conformance.ts Outdated
Comment thread tests/integration/broker/obligation-conformance.test.ts
@khaliqgant

Copy link
Copy Markdown
Member Author

pr-shepherd extractor test — triggering webhook event to fire trajectory pointer extraction. The HTML comment pointer was stamped on this PR body by trajectory-lead-0811v3 at 06:56Z 2026-08-11. Next pr-shepherd cron tick will log pr-shepherd.ledger.trajectory-pointer with non-null work_unit_id.

c2a-lead-0811b and others added 2 commits August 11, 2026 11:54
…bugs

Four fixes identified in reviewer sweeps:

1. Replace Math.random() with crypto.randomBytes in uniqueSuffix() and
   BrokerHarness brokerName default (broker-harness.ts). Math.random() is
   non-cryptographic; CodeQL flagged the sessionId use as insecure randomness
   (GHAS check run 93632914840). No behaviour change — this is test isolation
   only.

2. Advance the `since` cursor after each observed return in armC
   (obligation-conformance.test.ts). The cursor was fixed at the pre-send
   snapshot, so all three waitForReturn calls rediscovered return #1. Buckets
   were effectively [1,1,1] and the equal-interval assertion passed vacuously.
   Now `since` advances to getEvents().length after each match, so iterations
   2 and 3 wait for distinct events.

3. Fix recipientHasReadReceipt to compare entry.agentName (the string name)
   instead of entry.agentId (an opaque identifier) against recipientName
   (obligation-conformance.ts). The old predicate always returned false, making
   arm D's independent-variable guard unverifiable.

4. Emit turn.settled from the native sidecar after turn.finished
   (native-sidecar.ts). The AI-SDK harness emits turn.started → turn.finished
   → turn.settled; the sidecar was stopping at turn.finished, so
   assertRecipientTookTurn on the native-fixture path always timed out waiting
   for turn.settled. The sidecar fix is in the fixture, not the assertion:
   turn.settled is the correct "turn complete" signal per the relay SDK contract
   (packages/harnesses/src/ai-sdk/harness-host.ts).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@tests/integration/broker/utils/broker-harness.ts`:
- Line 92: Update both broker identifier generation locations in the broker
harness to use randomBytes(8) instead of randomBytes(2), preserving the existing
timestamp and formatting logic.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9bda5dab-fea1-4651-8ffe-cc54249ae5ea

📥 Commits

Reviewing files that changed from the base of the PR and between 81408c0 and 467ab0e.

📒 Files selected for processing (4)
  • tests/integration/broker/fixtures/native-sidecar.ts
  • tests/integration/broker/obligation-conformance.test.ts
  • tests/integration/broker/utils/broker-harness.ts
  • tests/integration/broker/utils/obligation-conformance.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/integration/broker/obligation-conformance.test.ts
  • tests/integration/broker/utils/obligation-conformance.ts

brokerName:
options.brokerName ??
`test-harness-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
options.brokerName ?? `test-harness-${Date.now().toString(36)}-${randomBytes(2).toString('hex')}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target file symbols and relevant lines ---'
ast-grep outline tests/integration/broker/utils/broker-harness.ts
cat -n tests/integration/broker/utils/broker-harness.ts | sed -n '1,115p;325,350p'

printf '%s\n' '--- uniqueSuffix definition and usages ---'
rg -n -C 4 'uniqueSuffix|randomBytes\(2\)|brokerName|runtime.?isolation|isolation' tests/integration/broker

printf '%s\n' '--- parallelism and fixture configuration ---'
rg -n -C 3 'workers|parallel|concurr|fixture|broker-harness|obligation-conformance' \
  package.json pnpm-workspace.yaml yarn.lock package-lock.json tests .github 2>/dev/null || true

Repository: AgentWorkforce/relay

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- broker-harness relevant declarations and call sites ---'
rg -n -C 8 'randomBytes|uniqueSuffix|brokerName|isolation|stateDir|cwd' tests/integration/broker/utils/broker-harness.ts

printf '%s\n' '--- uniqueSuffix implementation and obligation-conformance usage ---'
rg -n -C 10 'function uniqueSuffix|const uniqueSuffix|uniqueSuffix\(' \
  tests/integration/broker/utils/obligation-conformance.ts \
  tests/integration/broker/utils/broker-harness.ts

printf '%s\n' '--- test-runner configuration ---'
find . -maxdepth 3 -type f \( -name 'package.json' -o -name '*config*' -o -name '*.yml' -o -name '*.yaml' \) -print0 |
  xargs -0 rg -n -C 3 'node:test|--test|workers|concurrency|parallel|test-concurrency' 2>/dev/null || true

Repository: AgentWorkforce/relay

Length of output: 26165


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- integration test orchestration ---'
cat -n package.json | sed -n '1,65p'
cat -n tests/integration/run-all-tests.js | sed -n '1,220p'
cat -n .github/workflows/test.yml | sed -n '1,140p'

printf '%s\n' '--- broker test files and explicit concurrency controls ---'
git ls-files 'tests/integration/broker/*' 'tests/integration/broker/**/*.ts' |
  sort
rg -n -C 3 --glob '!dist/**' --glob '!node_modules/**' \
  'concurrency|test\.run|test\(|describe\(|parallel|serial|only' tests/integration/broker package.json

printf '%s\n' '--- harness construction and lifecycle usage ---'
rg -n -C 4 --glob '!dist/**' --glob '!node_modules/**' \
  'new BrokerHarness|\.start\(\)|new HarnessDriverClient|BrokerHarnessOptions' tests/integration/broker

Repository: AgentWorkforce/relay

Length of output: 50377


🌐 Web query:

Node.js 22.14.0 test runner default test file concurrency --test-concurrency documentation

💡 Result:

In Node.js, including version 22.14.0, the --test-concurrency command-line flag controls the maximum number of test files that the test runner executes in parallel [1][2]. When process-level isolation is enabled (which is the default behavior), each test file is executed in a separate child process, and this flag determines how many of those child processes can run concurrently [3][4]. Regarding your query: 1. Flag purpose: The --test-concurrency flag is specific to the test runner CLI and governs parallel execution at the file level [2][5]. 2. Default value: The documentation specifies that the default behavior for file-level concurrency is to run only one test file at a time if the flag is not provided or explicitly set to false [3][1]. Note that while some internal commit discussions or variations exist regarding architectural defaults, the official documentation for Node.js 22.x defines the concurrency options clearly: - When using the concurrency option in the run() function or via the CLI for file execution, setting it to a number allows that many test processes to run in parallel [3][1]. - If set to true, it runs os.availableParallelism() - 1 test files in parallel [3][1]. - If set to false (the default), it runs one test file at a time [3][1]. It is important to distinguish this from the concurrency option used within test files (e.g., inside test() or describe() calls) [5]. The concurrency setting inside a test file manages how many tests run concurrently within that single application process and does not interact with the --test-concurrency flag, which is dedicated to the orchestration of multiple test files [6][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- exact broker harness construction counts ---'
rg -l --glob '*.test.ts' --glob '!dist/**' 'new BrokerHarness' tests/integration/broker | sort
printf 'files with BrokerHarness: '
rg -l --glob '*.test.ts' --glob '!dist/**' 'new BrokerHarness' tests/integration/broker | wc -l
printf 'constructor call count: '
rg -o --glob '*.test.ts' --glob '!dist/**' 'new BrokerHarness' tests/integration/broker | wc -l

printf '%s\n' '--- exact uniqueSuffix call count ---'
printf 'call sites: '
rg -n --glob '!dist/**' --glob '!node_modules/**' 'uniqueSuffix\(' tests/integration/broker | wc -l
rg -n --glob '!dist/**' --glob '!node_modules/**' 'uniqueSuffix\(' tests/integration/broker/utils/broker-harness.ts tests/integration/broker/utils/obligation-conformance.ts

printf '%s\n' '--- test runner version declarations ---'
rg -n -C 2 '"engines"|node-version|node --test' package.json package-lock.json .github/workflows tests/integration/broker/tsconfig.json

Repository: AgentWorkforce/relay

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
import math

space = 2**16
print(f"suffix_space={space}")
for n in (2, 18, 100, 256, 1000):
    # Probability of at least one suffix collision when n IDs share one millisecond.
    no_collision = math.prod((space - i) / space for i in range(n))
    print(f"same_millisecond_ids={n} collision_probability={1 - no_collision:.6%}")
PY

printf '%s\n' '--- constructors grouped by nearby test scope ---'
python3 - <<'PY'
from pathlib import Path
import re

for p in sorted(Path("tests/integration/broker").rglob("*.test.ts")):
    lines = p.read_text().splitlines()
    hits = [i for i, line in enumerate(lines) if "new BrokerHarness" in line]
    if hits:
        print(f"{p}: {len(hits)} constructors")
        for i in hits:
            start = max(0, i - 3)
            end = min(len(lines), i + 2)
            print("  " + " | ".join(f"{j+1}:{lines[j].strip()}" for j in range(start, end)))
PY

Repository: AgentWorkforce/relay

Length of output: 18532


Use a larger random suffix for test isolation.

When multiple identifiers are created within one millisecond, randomBytes(2) provides only 65,536 possible suffixes. Use randomBytes(8) in both locations. The default test command runs files serially, but callers can enable file concurrency.

🤖 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/broker/utils/broker-harness.ts` at line 92, Update both
broker identifier generation locations in the broker harness to use
randomBytes(8) instead of randomBytes(2), preserving the existing timestamp and
formatting logic.

… and turn baseline capture

Bug 4 — waitForDelivery: remove relay_inbound and delivery_queued from the
accepted-kinds set. Both fire before the message is injected into the
recipient worker (relay_inbound is the broker's first sight of the message
from Relaycast; delivery_queued is the internal queue slot). Accepting either
let arms proceed to read/reply/signal steps before the recipient actually had
the message, which is the same substitution of a well-formed signal for an
unverified fact that this fixture exists to catch. Minimum signal is now
delivery_injected (PTY) / delivery_ack (native).

Bug 5 — readReactions: relay.messages.reactions() returns ReactionGroup[]
where each group bundles all agents who reacted with the same emoji into
{ emoji, count, agents: string[] }. The substrate gap test's shapes assertion
— that author and recipient done-reaction records have identical key shapes —
was trivially true with one group entry per emoji. Flatten into per-actor
records ({ emoji, agent_name }) so the assertion requires two entries and the
shape comparison is meaningful.

Bug 6 — armA turn baseline race: watchForTurn on the native path reads the
high-water sequence from getAgentEventHistory. It was called right before
waitForReturn, after several network-bound steps (markRead, dm, emitSignal ×2,
recipientHasReadReceipt). If RELAY_OBLIGATION_INTERVAL_MS is short, the
boomerang return can fire and the recipient sidecar can emit turn.settled
before watchForTurn captures the baseline, placing the boomerang turn's
sequence <= baseline and making assertRecipientTookTurn miss it. Move the
call to immediately after waitForDelivery: after the initial delivery turn
completes, before any boomerang stimulus.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests/integration/broker/obligation-conformance.test.ts">

<violation number="1" location="tests/integration/broker/obligation-conformance.test.ts:197">
P2: Moving watchForTurn to right after waitForDelivery re-introduces the false-positive the old placement avoided: on the native path waitForDelivery resolves at `delivery_ack`, before the initial-delivery model turn settles, so the baseline is captured before that turn's `turn.settled`. assertRecipientTookTurn will then find that initial-delivery turn as `proof` that the boomerang return took a model turn, even when the return itself took no turn — defeating the very distinction this assertion exists to draw. The comment 'places the baseline after the initial delivery turn' only holds for paths where delivery confirmation implies the model turn finished, which is not the native path. To confirm the recipient's initial-delivery turn settled before capturing the baseline (e.g. wait for the first turn.settled for that message, then read the baseline), or the guard stays usable only when the delivery-turn gap is closed.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

//
// Capturing here places the baseline after the initial delivery turn (so we
// don't mistake it for the boomerang turn) and before any boomerang stimulus.
const watch = await watchForTurn(harness, recipient.name, path);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Moving watchForTurn to right after waitForDelivery re-introduces the false-positive the old placement avoided: on the native path waitForDelivery resolves at delivery_ack, before the initial-delivery model turn settles, so the baseline is captured before that turn's turn.settled. assertRecipientTookTurn will then find that initial-delivery turn as proof that the boomerang return took a model turn, even when the return itself took no turn — defeating the very distinction this assertion exists to draw. The comment 'places the baseline after the initial delivery turn' only holds for paths where delivery confirmation implies the model turn finished, which is not the native path. To confirm the recipient's initial-delivery turn settled before capturing the baseline (e.g. wait for the first turn.settled for that message, then read the baseline), or the guard stays usable only when the delivery-turn gap is closed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/integration/broker/obligation-conformance.test.ts, line 197:

<comment>Moving watchForTurn to right after waitForDelivery re-introduces the false-positive the old placement avoided: on the native path waitForDelivery resolves at `delivery_ack`, before the initial-delivery model turn settles, so the baseline is captured before that turn's `turn.settled`. assertRecipientTookTurn will then find that initial-delivery turn as `proof` that the boomerang return took a model turn, even when the return itself took no turn — defeating the very distinction this assertion exists to draw. The comment 'places the baseline after the initial delivery turn' only holds for paths where delivery confirmation implies the model turn finished, which is not the native path. To confirm the recipient's initial-delivery turn settled before capturing the baseline (e.g. wait for the first turn.settled for that message, then read the baseline), or the guard stays usable only when the delivery-turn gap is closed.</comment>

<file context>
@@ -184,6 +184,18 @@ async function armA(ctx: ConformanceContext, options: { setReadState: boolean })
+  //
+  // Capturing here places the baseline after the initial delivery turn (so we
+  // don't mistake it for the boomerang turn) and before any boomerang stimulus.
+  const watch = await watchForTurn(harness, recipient.name, path);
+
   // 3. Read state. The point of the arm is that this must not matter.
</file context>

@khaliqgant

Copy link
Copy Markdown
Member Author

Superseded by #1485 (full obligation/boomerang implementation, merged into v11.5.4). This test-only fixture is now DIRTY against main and the feature is live.

@khaliqgant khaliqgant closed this Aug 12, 2026
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.

2 participants