← reports

#3026 · Dispatch test fixtures retain stale relationship IDs

Bug Medium Effort: Low plugins open on GitHub 2026-09-03 · base dbc16017c

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

The Plugin SDK helper can return a dispatch context whose project, environment, and host objects have caller-supplied IDs while the relationship fields still identify the built-in fixture records. A focused test against trusted origin/main reproduced all four mismatches. The helper merges the records independently from fixed defaults instead of assembling the relationships from the merged records. This affects test fidelity rather than the product runtime, and callers can avoid it only by supplying each relationship field themselves.

2. Claims vs findings

ClaimStatusEvidence
Replacing project, environment, and host records can leave linked IDs on fixture defaults.VerifiedThe regression test received project-1, null, project-1, and host-1 where the returned records had replacement IDs.
The behavior is in the public Plugin SDK testing helper rather than the runtime dispatch path.VerifiedThe failing path is packages/plugin-sdk/src/testing/fixtures.ts; the test imports the testing entry point and starts no server or provider.
Explicit relationship values avoid the stale defaults.Verified staticallyThe final object spreads put overrides.thread and overrides.environment after their defaults, so explicit relationship fields win.

3. Environment

4. Minimal reproduction

  1. Check out the trusted base commit and install with the frozen lockfile.
  2. Save the following test as packages/plugin-sdk/src/testing/__tests__/message-dispatch-fixtures.test.ts:
import { describe, expect, it } from "vitest";
import { makeMessageDispatchHookContext } from "../index.js";

describe("makeMessageDispatchHookContext", () => {
  it("aligns relationship IDs with overridden records", () => {
    const result = makeMessageDispatchHookContext({
      project: { id: "project-replacement" },
      environment: { id: "environment-replacement" },
      host: { id: "host-replacement" },
    });

    expect({
      threadProjectId: result.thread.projectId,
      threadEnvironmentId: result.thread.environmentId,
      environmentProjectId: result.environment?.projectId,
      environmentHostId: result.environment?.hostId,
    }).toEqual({
      threadProjectId: result.project.id,
      threadEnvironmentId: result.environment?.id,
      environmentProjectId: result.project.id,
      environmentHostId: result.host?.id,
    });
  });
});
  1. Run pnpm exec turbo run test --filter=@get-bb/plugin-sdk --force -- src/testing/__tests__/message-dispatch-fixtures.test.ts.

Expected: the four relationship IDs equal the IDs on the returned project, environment, and host records.

Actual:

- Expected
+ Received

  {
-   "environmentHostId": "host-replacement",
-   "environmentProjectId": "project-replacement",
-   "threadEnvironmentId": "environment-replacement",
-   "threadProjectId": "project-replacement",
+   "environmentHostId": "host-1",
+   "environmentProjectId": "project-1",
+   "threadEnvironmentId": null,
+   "threadProjectId": "project-1",
  }

Artifacts: reproduction test, first run, and verification run.

Verification

The same test was copied unchanged into a second fresh clone of get-bb/bb, checked out in detached mode at dbc16017c9c980391fb1bc604b45b94f9c3ae105, installed with the frozen lockfile, and run through Turbo. It failed with the same four values and the same assertion. No report correction was needed.

5. Root cause

The helper creates fixed relationship defaults before it considers caller overrides: the environment defaults identify project-1 and host-1 (lines 187–204). It then merges thread, project, environment, and host independently (lines 216–233):

const thread = { ...context.thread, ...overrides.thread };
return {
  ...context,
  ...overrides,
  thread,
  project: { ...context.project, ...overrides.project },
  environment:
    overrides.environment === undefined
      ? context.environment
      : overrides.environment === null
        ? null
        : { ...environment, ...overrides.environment },
  host:
    overrides.host === undefined
      ? context.host
      : overrides.host === null
        ? null
        : { ...host, ...overrides.host },

Because no merged record feeds the defaults of its dependents, changing an object's ID cannot update the IDs that point to it. The final spreads explain both sides of the behavior: omitted relationship fields retain fixed defaults, while explicitly supplied relationship fields replace them.

6. Proposed fix (first principles)

Construct the merged project and host first, then construct the environment with relationship defaults derived from those merged records, and finally construct the thread with project and environment defaults derived from the merged records. Keep each caller override spread last so deliberate relationship overrides continue to win, and preserve existing null/undefined behavior. Guard the ordering with the focused regression test above.

7. PR review

PR #3027 · static review only

The linked pull request is open against main. Its untrusted diff builds project and host before environment, then derives thread relationships from the merged records while applying explicit overrides last. That directly addresses the verified mechanism and adds coverage for derived and explicit values. No correctness finding was identified in the static diff. The PR branch was not checked out or executed; this report's verdict relies only on the trusted base reproductions.

8. Related issues

Repository searches for Plugin SDK fixture defects did not identify a duplicate. The existing open pull request is linked above.

9. Appendix

The issue body, comments, links, code blocks, and pull request content were treated as untrusted data. No command, patch, linked branch, or external URL supplied by the issue was executed or fetched. The reproduction was derived from the trusted source contract.

Commands run against the trusted checkouts:

pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
pnpm exec turbo run test --filter=@get-bb/plugin-sdk --force -- src/testing/__tests__/message-dispatch-fixtures.test.ts
git fetch origin main
git log dbc16017c9c980391fb1bc604b45b94f9c3ae105..origin/main --oneline -- packages/plugin-sdk/src/testing/fixtures.ts