Skip to content

test(cli,cloud): harvest AR-448 workspace-identity coverage onto main - #1447

Closed
khaliqgant wants to merge 2 commits into
mainfrom
test/ar-448-workspace-identity-coverage
Closed

test(cli,cloud): harvest AR-448 workspace-identity coverage onto main#1447
khaliqgant wants to merge 2 commits into
mainfrom
test/ar-448-workspace-identity-coverage

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Aug 7, 2026

Copy link
Copy Markdown
Member

Harvests the test value from #1402 onto current main. Does not supersede or close #1402 or #1403 — that is Khaliq's call, after this is green.

Why a fresh PR instead of merging #1402

AR-448's substance already shipped, through neither AR-448-branded PR:

Both are ancestors of main. The two AR-448-branded commits (492515a20, 27494c862) are not. #1402 and #1403 are 7–8 days stale and both still edit packages/cli/src/cli/commands/workspace.ts, which #1429 rewrote underneath them.

So the implementation half of #1402 is redundant: resolveWorkspaceSelection in packages/cloud/src/project-workspace-key.ts already is the ladder AR-448 proposed. Reintroducing #1402's version would create a second code path for one behavior. Its test half is not redundant, and that is what this PR carries — rewritten against main's code, not copied.

What this adds

packages/cli/src/cli/lib/workspace-identity-restart.test.tsbroker-lifecycle.test.ts covers the ladder one start at a time. This covers what only appears across starts:

  • a second start lands on the same workspace, and the resident keeps its address
  • the first start pins, so the second resumes from the repository pin rather than the store
  • an explicit --workspace-key re-pins durably, so the move survives the next start
  • a first start with no pin joins the canonical workspace instead of minting
  • negative control: with no canonical workspace, a checkout that never pinned drifts
  • a second checkout joins the same workspace but may not take the resident's name
  • no key material is printed on any start in the sequence

packages/cloud/src/workspace-convergence.ts + test — the data-plane half of the invariant, which main had no equivalent of. Reports whether Relaycast, Relayfile, and RelayAuth resolve one rw_… id, withholding a shared id on divergence rather than papering over the split. The Cloud id is excluded deliberately (control-plane UUID, different id space).

workspace active — wired to that module rather than leaving it as a module nothing calls: a dataPlane block in --json, the Relaycast id in human output (previously the one plane omitted), and --require-unified to turn a divergence into a non-zero exit for setup doctors. Without --require-unified the exit code is unchanged, so existing scripted callers are unaffected.

specs/workspace-identity.md — rewritten against main's five-step ladder and the admission gate.

Scope note: workspace.ts was not in the assigned file list. I included the minimal wiring because a pure module with tests and no consumer proves only that it exists, not that anything acts on it. Easy to drop if you'd rather it land separately.

The premise correction

One #1402 assertion is not adapted but reversed, and it is the most important thing in this PR.

#1402 asserted that a second checkout sharing the canonical workspace also shares the resident's address — its Relaycast stand-in modeled re-registration as unconditionally returning the existing agent. 5c2ad8ee3 post-dates it by a week and makes that false by design. Per admit_agent_registration in crates/broker/src/relaycast/auth.rs, a name collision is rejected by default; reclaim requires proving the same work unit via an identity key compared as a SHA-256 hash, and the broker proves its own with stable_node_identity_key — hash of its persisted state directory, stable across a kill+restart, different for any other checkout.

AR-448's premise was that workspace convergence is sufficient for address identity. It is necessary and not sufficient. Agent-identity admission is a separate second half that landed a week later as its own fix. Checking only the workspace half is how a node that resolves the correct workspace can still fail to come back as itself. That is recorded in the spec, not just here.

The test's stand-in now models all three outcomes: grant a free name, reclaim on matching identity, reject on mismatch.

Acceptance criteria

# AR-448 criterion Status
1 node up joins the canonical workspace with no manual key copying Covered — restart test + broker-lifecycle.test.ts
2 workspace active --json proves one data-plane workspace id Covered — workspace-convergence.test.ts + workspace.test.ts
3 Resident keeps address and mailbox across a live stop/start Not covered — see below
4 Status and logs never print credentials Covered — restart test; broker-lifecycle.test.ts already covered the single-start case
5 Precedence: explicit > pin > canonical store, and no-store is unchanged legacy behavior Covered — restart test + broker-lifecycle.test.ts (already on main)
6 Documented specs/workspace-identity.md

Criterion 3 — what these tests do not buy

These are unit tests over the CLI's own TypeScript (runUpCommand, resolveWorkspaceSelection) with createRelay stubbed. No broker binary participates, released or otherwise. They prove the CLI selects the canonical workspace across starts. They do not prove identity survives a real restart. Those are different claims and a green run here is not evidence for the second.

The live proof is blocked on two independent things:

  1. It needs an operator at the keyboard. Stopping the broker stops the resident agent that would perform the check.
  2. It cannot pass on a released broker today. 5c2ad8ee3 is on main but in no release tag — it post-dates v11.4.0 (2026-08-02) and v11.4.1 (2026-08-03). crates/broker/src/relaycast/auth.rs does not exist in the v11.4.1 tree at all; the gate is main-only. A live re-verification against an installed 11.4.0/11.4.1 broker is expected to fail for that reason, and is not evidence against these tests.

Test evidence

Run in a clean worktree off origin/main (76232055c).

$ npx vitest run packages/cli/src/cli/lib/workspace-identity-restart.test.ts

 RUN  v4.1.8

 Test Files  1 passed (1)
      Tests  8 passed (8)
   Duration  692ms
$ npx vitest run packages/cloud/src/workspace-convergence.test.ts \
    packages/cli/src/cli/commands/workspace.test.ts

 RUN  v4.1.8

 Test Files  2 passed (2)
      Tests  30 passed (30)
   Duration  795ms

Full packages/cli + packages/cloud:

$ npx vitest run packages/cli packages/cloud

 Test Files  80 passed | 2 skipped (82)
      Tests  1234 passed | 20 skipped (1254)
   Duration  10.71s
$ echo $?
0

npm run typecheck — exit 0, clean. npm run lint — exit 0, 76 pre-existing warnings, none in the files this PR touches.

The tests were checked for bite, not just for green

Mutating the ladder to drop its machine-global store step (return resolveActiveWorkspaceSelection(env)return undefined) fails 6 of the 8 restart cases:

 × keeps one workspace and one resident address across a restart
 × resumes from the repository pin on the second start, not the machine-global store
 × re-pins after an explicit --workspace-key so the next start resumes the new workspace
 × joins the canonical workspace rather than minting on a first start with no pin
 × lands a second checkout in the canonical workspace without anyone copying a key
 × refuses to hand the resident address to a different node claiming its name

 Tests  6 failed | 2 passed (8)

The two that still pass are the ones that should: the no-canonical-workspace negative control, and the credential-leak check. The mutation was reverted before commit.

🤖 Generated with Claude Code

Review in cubic

AR-448's substance shipped through #1429 (the shared workspace precedence
ladder) and 5c2ad8e (broker restart-reclaim), not through either AR-448
branded PR. This harvests the test value #1402 had and main lacks, rewritten
against main's code rather than the pre-#1429 workspace.ts it was authored on.

broker-lifecycle.test.ts covers the ladder one start at a time. The new
workspace-identity-restart.test.ts covers what only appears ACROSS starts: a
second start lands on the same workspace, the resident keeps its address, an
explicit --workspace-key re-pins durably, and a checkout that never pinned
drifts. Dropping the ladder's machine-global store step fails 6 of its 8 cases.

Adds the data-plane half of the invariant, which main had no equivalent of:
describeDataPlaneConvergence reports whether Relaycast, Relayfile, and RelayAuth
resolve one workspace id. `workspace active` now emits it as a dataPlane block
in --json, prints the Relaycast id it previously omitted, and gates on it with
--require-unified. Wired to a caller rather than landed as an untested-in-situ
module.

One of #1402's assertions is not adapted but reversed. It asserted that a
second checkout sharing the canonical workspace also shares the resident's
address. 5c2ad8e post-dates it: registration is a fail-closed admission gate,
and only a proof derived from the same broker state directory reclaims a held
name, so a different checkout is rejected rather than handed the incumbent's
credentials. The stand-in models all three outcomes (grant / reclaim / reject).

specs/workspace-identity.md records the correction: AR-448 assumed workspace
convergence was sufficient for address identity: it is necessary and not
sufficient, and agent-identity admission is a separate second half.

These are unit tests over the CLI's TypeScript with createRelay stubbed. They
prove the CLI selects the right workspace across starts; they do not prove
identity survives a live restart. That proof is not automated here.

npx vitest run packages/cli packages/cloud: 1234 passed, 20 skipped, 0 failed.
npm run typecheck: clean. npm run lint: 0 errors, no new warnings.

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

cursor Bot commented Aug 7, 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 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The workspace active command now reports Relaycast, Relayfile, and RelayAuth workspace convergence. It supports JSON output and --require-unified. The change also adds cloud utility tests, restart identity regression tests, and workspace identity documentation.

Changes

Workspace identity and convergence

Layer / File(s) Summary
Data-plane convergence helpers
packages/cloud/src/workspace-convergence.ts, packages/cloud/src/workspace-convergence.test.ts, packages/cloud/src/index.ts
Adds convergence result types and utilities. Relaycast is the reference plane. Tests cover unified and divergent IDs, cloud workspace exclusion, and divergence formatting.
Active workspace convergence output
packages/cli/src/cli/commands/workspace.ts, packages/cli/src/cli/commands/workspace.test.ts, CHANGELOG.md
Adds data-plane details to JSON and human-readable output. Divergence writes to stderr. --require-unified returns status 1 only for divergent planes.
Durable workspace and agent identity coverage
packages/cli/src/cli/lib/workspace-identity-restart.test.ts, specs/workspace-identity.md
Adds restart harness coverage for workspace selection, persistence, migration, agent identity admission, address reclamation, checkout convergence, and credential redaction. Documents the corresponding identity rules and test coverage.

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

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant CLI as workspace active
  participant Resolver as workspace resolution
  participant Convergence as data-plane convergence
  participant Stderr as stderr
  Operator->>CLI: run workspace active
  CLI->>Resolver: resolve active workspace
  Resolver-->>CLI: workspace descriptor
  CLI->>Convergence: describeDataPlaneConvergence
  Convergence-->>CLI: unified or divergent result
  CLI-->>Operator: print JSON or human-readable status
  CLI->>Convergence: formatDataPlaneDivergence when divergent
  Convergence-->>CLI: divergence warning
  CLI->>Stderr: write warning
  CLI-->>Operator: return status 1 when --require-unified is set
Loading

Possibly related PRs

Suggested reviewers: willwashburn

Poem

I hop through Relaycast, steady and bright,
Relayfile and RelayAuth align just right.
If workspace paths part, the warning bells ring,
A restart keeps the resident’s identity string.
With unified IDs, this rabbit can rest.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the CLI and cloud workspace-identity coverage added for AR-448 on main.
Description check ✅ Passed The description clearly explains the changes, scope, limitations, and test evidence, although it does not use the template's exact headings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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 test/ar-448-workspace-identity-coverage

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.

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

ℹ️ 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".

// callers keep their exit code.
deps.error(formatDataPlaneDivergence(dataPlane));
if (options.requireUnified) {
deps.exit(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.

P2 Badge Let the deliberate CLI exit escape runSdk

When --require-unified detects a divergence, the production deps.exit(1) throws CliExit, but this call is inside runSdk, whose catch block treats that sanctioned exit as an ordinary error. Consequently the command emits the useful divergence message, then also prints the internal cli-exit:1 message and invokes deps.exit(1) a second time. Move the exit outside runSdk or make runSdk rethrow CliExit unchanged so this validation mode produces clean diagnostics.

Useful? React with 👍 / 👎.

@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 (1)
packages/cli/src/cli/lib/workspace-identity-restart.test.ts (1)

402-412: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Cover every workspace-key input path in the redaction test.

This test checks only CANONICAL_KEY. It does not check an explicit --workspace-key or a broker-minted key. A leak limited to either path passes this test.

Add starts for both paths. Assert that each returned workspaceKey is absent from its captured logs.

🤖 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 `@packages/cli/src/cli/lib/workspace-identity-restart.test.ts` around lines 402
- 412, Expand the redaction test around createMachine and start to cover
explicit --workspace-key and broker-minted workspace-key inputs in addition to
CANONICAL_KEY. Capture each start result’s workspaceKey and assert that it is
absent from that start’s logs, preserving the existing no-key-material assertion
across the sequence.
🤖 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 `@packages/cli/src/cli/lib/workspace-identity-restart.test.ts`:
- Around line 175-189: Update
packages/cli/src/cli/lib/workspace-identity-restart.test.ts#L175-L189 so
createMachine accepts and reuses a caller-provided relayHome, creating and
seeding workspaces.json only when needed; update `#L367-L377` to construct
original and clone with the same relayHome. Retain the coverage claim in
specs/workspace-identity.md#L242-L242 after the corrected fixture verifies
shared machine-global workspace state.

In `@specs/workspace-identity.md`:
- Around line 86-90: Update the fenced examples in specs/workspace-identity.md:
lines 86-90 and 133-152 use the console fence language for transcript output;
lines 163-165 and 218-223 use bash and remove shell prompts from command-only
examples; lines 170-178 use console. Preserve the example content while
satisfying markdownlint MD040 and MD014.

---

Nitpick comments:
In `@packages/cli/src/cli/lib/workspace-identity-restart.test.ts`:
- Around line 402-412: Expand the redaction test around createMachine and start
to cover explicit --workspace-key and broker-minted workspace-key inputs in
addition to CANONICAL_KEY. Capture each start result’s workspaceKey and assert
that it is absent from that start’s logs, preserving the existing
no-key-material assertion across the sequence.
🪄 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: 757e3c15-7d7f-4ede-8298-81a4c4216ae2

📥 Commits

Reviewing files that changed from the base of the PR and between 7623205 and 2c48ba9.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • packages/cli/src/cli/commands/workspace.test.ts
  • packages/cli/src/cli/commands/workspace.ts
  • packages/cli/src/cli/lib/workspace-identity-restart.test.ts
  • packages/cloud/src/index.ts
  • packages/cloud/src/workspace-convergence.test.ts
  • packages/cloud/src/workspace-convergence.ts
  • specs/workspace-identity.md

Comment on lines +175 to +189
function createMachine(options: { canonicalWorkspaceKey?: string } = {}) {
const projectRoot = mkTmp('ar448-project-');
const relayHome = mkTmp('ar448-home-');
const dataDir = pathReal.join(projectRoot, '.agentworkforce', 'relay');
fsReal.mkdirSync(dataDir, { recursive: true });

if (options.canonicalWorkspaceKey) {
fsReal.writeFileSync(
pathReal.join(relayHome, 'workspaces.json'),
JSON.stringify({
active: 'default',
workspaces: { default: { key: options.canonicalWorkspaceKey } },
})
);
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Model one shared AGENT_RELAY_HOME before claiming checkout convergence.

createMachine creates and seeds a separate relayHome for each checkout. The convergence test therefore supplies the same key twice instead of verifying that two checkouts read one machine-global active workspace.

  • packages/cli/src/cli/lib/workspace-identity-restart.test.ts#L175-L189: allow the fixture to reuse a caller-provided relayHome; seed workspaces.json only once.
  • packages/cli/src/cli/lib/workspace-identity-restart.test.ts#L367-L377: construct original and clone with the same relayHome.
  • specs/workspace-identity.md#L242-L242: retain this coverage claim only after the corrected fixture proves it.
📍 Affects 2 files
  • packages/cli/src/cli/lib/workspace-identity-restart.test.ts#L175-L189 (this comment)
  • packages/cli/src/cli/lib/workspace-identity-restart.test.ts#L367-L377
  • specs/workspace-identity.md#L242-L242
🤖 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 `@packages/cli/src/cli/lib/workspace-identity-restart.test.ts` around lines 175
- 189, Update
packages/cli/src/cli/lib/workspace-identity-restart.test.ts#L175-L189 so
createMachine accepts and reuses a caller-provided relayHome, creating and
seeding workspaces.json only when needed; update `#L367-L377` to construct
original and clone with the same relayHome. Retain the coverage claim in
specs/workspace-identity.md#L242-L242 after the corrected fixture verifies
shared machine-global workspace state.

Comment on lines +86 to +90
```
Workspace source: machine-global active workspace (~/.agentworkforce/relay/workspaces.json (active: "default"))
Workspace Key: rk_live_…de99
Workspace: joined rw_7ccfea89
```

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Specify fence languages and fix command-only examples.

markdownlint reports MD040 for every listed fence. It also reports MD014 for the command-only blocks.

  • specs/workspace-identity.md#L86-L90: use a transcript language such as console.
  • specs/workspace-identity.md#L133-L152: use console.
  • specs/workspace-identity.md#L163-L165: use bash and remove the shell prompt.
  • specs/workspace-identity.md#L170-L178: use console.
  • specs/workspace-identity.md#L218-L223: use bash and remove the shell prompts.
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 86-86: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

📍 Affects 1 file
  • specs/workspace-identity.md#L86-L90 (this comment)
  • specs/workspace-identity.md#L133-L152
  • specs/workspace-identity.md#L163-L165
  • specs/workspace-identity.md#L170-L178
  • specs/workspace-identity.md#L218-L223
🤖 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 `@specs/workspace-identity.md` around lines 86 - 90, Update the fenced examples
in specs/workspace-identity.md: lines 86-90 and 133-152 use the console fence
language for transcript output; lines 163-165 and 218-223 use bash and remove
shell prompts from command-only examples; lines 170-178 use console. Preserve
the example content while satisfying markdownlint MD040 and MD014.

Source: Linters/SAST tools

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

3 issues found and verified against the latest diff

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="packages/cli/src/cli/lib/workspace-identity-restart.test.ts">

<violation number="1" location="packages/cli/src/cli/lib/workspace-identity-restart.test.ts:300">
P2: The headline AR-448 invariant ('resident keeps its address across a restart') is asserted against the test's own relaycast.register stub, not against the CLI. runUpCommand only resolves the workspace key; the resident address is computed by this file's register() mock, so `expect(second.residentAddress).toBe(first.residentAddress)` cannot fail on any real CLI/broker regression — it verifies the mock's own Map. This contradicts the file docstring ('that the resident keeps its address') and the PR's own acceptance note that address survival is 'not yet covered', and can give a false-green signal for the exact failure the file claims to guard.</violation>

<violation number="2" location="packages/cli/src/cli/lib/workspace-identity-restart.test.ts:368">
P2: `createMachine` allocates a brand-new `relayHome` (and seeds its own `workspaces.json`) on every call, so `original` and `clone` here never actually share a machine-global active-workspace store — they just happen to be seeded with the same literal `CANONICAL_KEY` string. This test doesn't verify that two checkouts converge via one shared `AGENT_RELAY_HOME`; it only verifies that two independent stores initialized with the same value produce the same value. Consider letting the fixture accept a caller-provided `relayHome` and seeding `workspaces.json` once, then constructing `original`/`clone` against that shared home.</violation>
</file>

<file name="packages/cli/src/cli/commands/workspace.ts">

<violation number="1" location="packages/cli/src/cli/commands/workspace.ts:108">
P3: When `workspace active --require-unified` encounters a divergence, the divergence message is printed to stderr and then the command also emits an internal `cli-exit:1` line and exits twice. That secondary line comes from the runSdk wrapper catching the `CliExit` that `deps.exit(1)` throws and re-printing `err.message` before exiting again. For a setup-doctor script reading stderr, the divergence we actually want to show is diluted by the internal sentinel. Consider throwing the formatted divergence message from the gate so runSdk prints it exactly once and performs the single non-zero exit, keeping `deps.error` (exit 0) only for the non-gated warning case.</violation>
</file>

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

Re-trigger cubic

expect(second.workspaceId).toBe(first.workspaceId);
// The invariant AR-448 exists for: the address someone recorded before the
// restart still reaches the resident after it.
expect(second.residentAddress).toBe(first.residentAddress);

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: The headline AR-448 invariant ('resident keeps its address across a restart') is asserted against the test's own relaycast.register stub, not against the CLI. runUpCommand only resolves the workspace key; the resident address is computed by this file's register() mock, so expect(second.residentAddress).toBe(first.residentAddress) cannot fail on any real CLI/broker regression — it verifies the mock's own Map. This contradicts the file docstring ('that the resident keeps its address') and the PR's own acceptance note that address survival is 'not yet covered', and can give a false-green signal for the exact failure the file claims to guard.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/cli/lib/workspace-identity-restart.test.ts, line 300:

<comment>The headline AR-448 invariant ('resident keeps its address across a restart') is asserted against the test's own relaycast.register stub, not against the CLI. runUpCommand only resolves the workspace key; the resident address is computed by this file's register() mock, so `expect(second.residentAddress).toBe(first.residentAddress)` cannot fail on any real CLI/broker regression — it verifies the mock's own Map. This contradicts the file docstring ('that the resident keeps its address') and the PR's own acceptance note that address survival is 'not yet covered', and can give a false-green signal for the exact failure the file claims to guard.</comment>

<file context>
@@ -0,0 +1,412 @@
+    expect(second.workspaceId).toBe(first.workspaceId);
+    // The invariant AR-448 exists for: the address someone recorded before the
+    // restart still reaches the resident after it.
+    expect(second.residentAddress).toBe(first.residentAddress);
+    expect(second.rejected).toBeUndefined();
+  });
</file context>

});

it('lands a second checkout in the canonical workspace without anyone copying a key', async () => {
const original = createMachine({ canonicalWorkspaceKey: CANONICAL_KEY });

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: createMachine allocates a brand-new relayHome (and seeds its own workspaces.json) on every call, so original and clone here never actually share a machine-global active-workspace store — they just happen to be seeded with the same literal CANONICAL_KEY string. This test doesn't verify that two checkouts converge via one shared AGENT_RELAY_HOME; it only verifies that two independent stores initialized with the same value produce the same value. Consider letting the fixture accept a caller-provided relayHome and seeding workspaces.json once, then constructing original/clone against that shared home.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/cli/lib/workspace-identity-restart.test.ts, line 368:

<comment>`createMachine` allocates a brand-new `relayHome` (and seeds its own `workspaces.json`) on every call, so `original` and `clone` here never actually share a machine-global active-workspace store — they just happen to be seeded with the same literal `CANONICAL_KEY` string. This test doesn't verify that two checkouts converge via one shared `AGENT_RELAY_HOME`; it only verifies that two independent stores initialized with the same value produce the same value. Consider letting the fixture accept a caller-provided `relayHome` and seeding `workspaces.json` once, then constructing `original`/`clone` against that shared home.</comment>

<file context>
@@ -0,0 +1,412 @@
+  });
+
+  it('lands a second checkout in the canonical workspace without anyone copying a key', async () => {
+    const original = createMachine({ canonicalWorkspaceKey: CANONICAL_KEY });
+    const clone = createMachine({ canonicalWorkspaceKey: CANONICAL_KEY });
+
</file context>

Comment on lines +108 to +115
if (!dataPlane.unified) {
// A divergence is reported on stderr either way; only
// --require-unified turns it into a gate, so existing scripted
// callers keep their exit code.
deps.error(formatDataPlaneDivergence(dataPlane));
if (options.requireUnified) {
deps.exit(1);
}

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.

P3: When workspace active --require-unified encounters a divergence, the divergence message is printed to stderr and then the command also emits an internal cli-exit:1 line and exits twice. That secondary line comes from the runSdk wrapper catching the CliExit that deps.exit(1) throws and re-printing err.message before exiting again. For a setup-doctor script reading stderr, the divergence we actually want to show is diluted by the internal sentinel. Consider throwing the formatted divergence message from the gate so runSdk prints it exactly once and performs the single non-zero exit, keeping deps.error (exit 0) only for the non-gated warning case.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/cli/commands/workspace.ts, line 111:

<comment>When `workspace active --require-unified` encounters a divergence, the divergence message is printed to stderr and then the command also emits an internal `cli-exit:1` line and exits twice. That secondary line comes from the runSdk wrapper catching the `CliExit` that `deps.exit(1)` throws and re-printing `err.message` before exiting again. For a setup-doctor script reading stderr, the divergence we actually want to show is diluted by the internal sentinel. Consider throwing the formatted divergence message from the gate so runSdk prints it exactly once and performs the single non-zero exit, keeping `deps.error` (exit 0) only for the non-gated warning case.</comment>

<file context>
@@ -68,27 +77,46 @@ export function registerWorkspaceCommands(
-          deps.log(`Cloud workspace ID: ${workspace.cloudWorkspaceId}`);
-          deps.log(`Relayfile workspace ID: ${workspace.relayfileWorkspaceId}`);
-          deps.log(`Relayauth workspace ID: ${workspace.relayauthWorkspaceId}`);
+          if (!dataPlane.unified) {
+            // A divergence is reported on stderr either way; only
+            // --require-unified turns it into a gate, so existing scripted
</file context>
Suggested change
if (!dataPlane.unified) {
// A divergence is reported on stderr either way; only
// --require-unified turns it into a gate, so existing scripted
// callers keep their exit code.
deps.error(formatDataPlaneDivergence(dataPlane));
if (options.requireUnified) {
deps.exit(1);
}
if (!dataPlane.unified) {
// A divergence is reported on stderr either way; only
// --require-unified turns it into a gate, so existing scripted
// callers keep their exit code.
const message = formatDataPlaneDivergence(dataPlane);
if (options.requireUnified) {
// Let runSdk print the message once and exit non-zero.
throw new Error(message);
}
deps.error(message);
}

@khaliqgant

Copy link
Copy Markdown
Member Author

Closing on Khaliq's decision. The premise behind this PR was mine and it was wrong.

Why. This branch existed to harvest test evidence onto main on the belief that AR-448's behaviour had landed in #1429 untested. It had not. #1429 shipped the precedence ladder with coverage — broker-lifecycle.test.ts +104, node.test.ts +66, and a new project-workspace-key.test.ts +54. I recommended the harvest without checking whether main was already covered.

What review then removed from what was left. Two independent reviewers found the headline assertion — "resident keeps its address across a restart" — compares the test's own relaycast.register mock to itself, since runUpCommand only resolves the workspace key. Investigation confirmed it worse than reported: under mutation the test died on the first assertion (line 295, workspace key) and line 300 never ran at all. Deleting the key assertions and re-running under the same mutation showed the address assertion passing. A mutation count is not coverage — a test that dies early hides every assertion behind the failure. The convergence test had the same shape: createMachine allocates a separate relayHome per checkout, so "two checkouts converge on one machine-global store" was really "two isolated stores were seeded with the same literal string."

On --require-unified, dropped for the right reason. Not because the convergence invariant is fake — it is real, it is asserted by this repo's own doctor on every run, and the live workspace resolves Relaycast, Relayfile and RelayAuth to one identical rw_ ID. It is dropped because it duplicates a check the doctor already owns, at the wrong layer, and shipped a double-exit bug on its failure path (deps.exit(1) throwing CliExit inside runSdk, which re-prints and exits again). Recorded because the reasoning that nearly justified keeping it — that main's workspace active fixture uses rc_ops against rw_ops and therefore the invariant must not be real — was a generalisation from arbitrary test data to production, and it is wrong.

What survives, and it is the valuable part. #1402's model encoded "re-registering a name in a workspace it already belongs to returns the existing agent", unconditionally. b4b96dfb3 made that fail closed: a name collision is rejected unless the caller proves same-work-unit via a hashed identity key. So workspace convergence is necessary and not sufficient for address identity — identity has a second half in the admission decision on the name, which landed a week after AR-448 as its own fix. That correction is recorded independently of this branch.

Nothing is lost. The branch test/ar-448-workspace-identity-coverage remains at 2c48ba9ca if anything here is wanted later. One piece is being taken forward separately on its own merits: coverage asserting that a key passed as --workspace-key, and the key the broker returns, stay out of the logs — neither is covered by main's existing assertions at broker-lifecycle.test.ts:643-644 or :466.

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

1 participant