bc13b8e92c
Two leads now work as peers rather than one primary plus workers. The arc:
CB-530/531 lead identity: `leaders:` names panes, `leadScan:` discovers them by
tab label (LeadTabScanner, TTL-cached, worker spaces excluded).
CB-532 leads can message each other AND be answered. Principal.leader now
carries its terminal, so ownsSession() can be true for a lead; the
"and you must be a worker" conjunct beside it protected nothing.
Retires `primary:` — reply nudges follow the delegating lead, a
binding recorded at bridge_send where both halves are known.
CB-533 ClaudeCodeLauncher passes --model. argv is usually a wrapper
(`ccs <profile>`) that re-exports its own model family, so
ANTHROPIC_MODEL alone was silently overruled.
CB-534 a lead is deliverable. The CB-113 readiness gate only opened for
terminals in WorkerPresence, which only workers ever enter, so every
lead->lead send waited out the ~60s grace and failed having never
been typed. The gate guards a *spawned* peer's boot window; a lead
is never spawned.
CB-535 bridge_list returns `leads` alongside `workers`, with `self` on the
caller's row. An empty worker roster no longer reads as "no peers".
CB-536 CLAUDE.md: lead<->lead is coordinate-only, never sideways delegation.
Propagated byte-identically to wiki/7-Use-Cases.md.
MIXED PROVENANCE — recorded deliberately rather than hidden. This tree also carries
in-progress CB-537 (context separation) authored by the peer lead gpt-sol-5.6 and
its worker: Capability.CONTEXT_RESET, SessionManager.clearAfterTurn, and the
Injector/TurnListener/CompletionResolver/launcher changes around it. That work was
done in this shared working tree rather than a worktree, and is entangled with the
above in BridgedConfig.java, Bridged.java and ClaudeCodeLauncher.java, so neither
lead could stage its own half without sweeping in the other's. Committing the whole
green state is the honest resolution; the peer branches from here.
Note for whoever picks CB-537 up: the design in this commit is SUPERSEDED. Both
leads agreed to replace the global `clearAfterTurn` boolean with per-delivery
policy (inherit|fresh|thread) applied PRE-delivery, because a post-turn reset races
by construction — Injector.onStatus clears awaitingCompletion and dequeues the next
message in the same tick. `fresh` is also a correctness guarantee, so an adapter
without a reset capability must refuse it rather than log a no-op.
mvn clean install: Tests run: 464, Failures: 0, Errors: 0, Skipped: 0. BUILD SUCCESS.
173 lines
5.5 KiB
Java
173 lines
5.5 KiB
Java
package dev.ltms.bridged.mcp;
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
/**
|
|
* Unit tests for {@link PrimaryRegistry}: pin vs record, isKnown transitions,
|
|
* null/blank guard.
|
|
*/
|
|
class PrimaryRegistryTest {
|
|
|
|
@Test
|
|
void unpinnedInitiallyUnknown() {
|
|
var reg = new PrimaryRegistry(null);
|
|
assertFalse(reg.isKnown());
|
|
assertTrue(reg.primaryTerminal().isEmpty());
|
|
}
|
|
|
|
@Test
|
|
void unpinnedAcceptsBlankAsAbsent() {
|
|
var reg = new PrimaryRegistry("");
|
|
assertFalse(reg.isKnown());
|
|
assertTrue(reg.primaryTerminal().isEmpty());
|
|
}
|
|
|
|
@Test
|
|
void pinnedFromConstruction() {
|
|
var reg = new PrimaryRegistry("term_fixed");
|
|
assertTrue(reg.isKnown());
|
|
assertEquals("term_fixed", reg.primaryTerminal().orElseThrow());
|
|
}
|
|
|
|
@Test
|
|
void recordWhenUnpinnedSetsTheTerminal() {
|
|
var reg = new PrimaryRegistry(null);
|
|
reg.record("term_abc");
|
|
assertTrue(reg.isKnown());
|
|
assertEquals("term_abc", reg.primaryTerminal().orElseThrow());
|
|
}
|
|
|
|
@Test
|
|
void recordWithNullDoesNothingWhenUnpinned() {
|
|
var reg = new PrimaryRegistry(null);
|
|
reg.record(null);
|
|
assertFalse(reg.isKnown());
|
|
}
|
|
|
|
@Test
|
|
void recordWithBlankDoesNothingWhenUnpinned() {
|
|
var reg = new PrimaryRegistry(null);
|
|
reg.record(" ");
|
|
assertFalse(reg.isKnown());
|
|
}
|
|
|
|
@Test
|
|
void recordOverwritesWhenUnpinned() {
|
|
var reg = new PrimaryRegistry(null);
|
|
reg.record("term_first");
|
|
assertEquals("term_first", reg.primaryTerminal().orElseThrow());
|
|
reg.record("term_second");
|
|
assertEquals("term_second", reg.primaryTerminal().orElseThrow());
|
|
}
|
|
|
|
@Test
|
|
void recordIsIgnoredWhenPinned() {
|
|
var reg = new PrimaryRegistry("term_pinned");
|
|
reg.record("term_other");
|
|
assertEquals("term_pinned", reg.primaryTerminal().orElseThrow(), "pinned value must survive record");
|
|
}
|
|
|
|
@Test
|
|
void nullRecordIsIgnoredWhenPinned() {
|
|
var reg = new PrimaryRegistry("term_pinned");
|
|
reg.record(null);
|
|
assertTrue(reg.isKnown());
|
|
assertEquals("term_pinned", reg.primaryTerminal().orElseThrow());
|
|
}
|
|
|
|
@Test
|
|
void blankRecordIsIgnoredWhenPinned() {
|
|
var reg = new PrimaryRegistry("term_pinned");
|
|
reg.record(" ");
|
|
assertTrue(reg.isKnown());
|
|
assertEquals("term_pinned", reg.primaryTerminal().orElseThrow());
|
|
}
|
|
|
|
@Test
|
|
void isKnownFalseAfterConstructionWithNull() {
|
|
var reg = new PrimaryRegistry(null);
|
|
assertFalse(reg.isKnown());
|
|
}
|
|
|
|
@Test
|
|
void isKnownAfterRecord() {
|
|
var reg = new PrimaryRegistry(null);
|
|
reg.record("term_x");
|
|
assertTrue(reg.isKnown());
|
|
}
|
|
|
|
@Test
|
|
void primaryTerminalRoundTrip() {
|
|
var reg = new PrimaryRegistry(null);
|
|
assertTrue(reg.primaryTerminal().isEmpty());
|
|
reg.record("term_found");
|
|
assertEquals("term_found", reg.primaryTerminal().get());
|
|
}
|
|
|
|
// ── CB-532: nudges follow the delegating lead, not "the primary" ────────────────────────────
|
|
|
|
/**
|
|
* The bug that made `primary.terminal` unretirable: with one slot, whichever lead called
|
|
* bridge_send first captured every nudge — including nudges for the other lead's delegations.
|
|
*/
|
|
@Test
|
|
void aNudgeGoesToTheLeadThatDelegatedToThatWorker() {
|
|
var reg = new PrimaryRegistry(null);
|
|
reg.recordDelegation("term_worker_a", "term_lead_opus");
|
|
reg.recordDelegation("term_worker_b", "term_lead_sol");
|
|
|
|
assertEquals("term_lead_opus", reg.nudgeTargetFor("term_worker_a").orElseThrow());
|
|
assertEquals("term_lead_sol", reg.nudgeTargetFor("term_worker_b").orElseThrow());
|
|
}
|
|
|
|
@Test
|
|
void theMostRecentDelegatorWinsWhenAWorkerChangesHands() {
|
|
var reg = new PrimaryRegistry(null);
|
|
reg.recordDelegation("term_worker", "term_lead_opus");
|
|
reg.recordDelegation("term_worker", "term_lead_sol");
|
|
|
|
assertEquals("term_lead_sol", reg.nudgeTargetFor("term_worker").orElseThrow(),
|
|
"the lead waiting on the reply is the one that sent the work most recently");
|
|
}
|
|
|
|
/** After a restart the map is empty while the durable inbox still holds the reply. */
|
|
@Test
|
|
void anUnknownDelegationFallsBackToThePinnedPrimary() {
|
|
var reg = new PrimaryRegistry("term_pinned");
|
|
|
|
assertEquals("term_pinned", reg.nudgeTargetFor("term_never_seen").orElseThrow());
|
|
}
|
|
|
|
@Test
|
|
void withNoPinAndNoDelegationNobodyIsNudged() {
|
|
var reg = new PrimaryRegistry(null);
|
|
|
|
assertTrue(reg.nudgeTargetFor("term_worker").isEmpty(),
|
|
"guessing would interrupt the wrong lead with someone else's result; the durable "
|
|
+ "inbox makes pull the correct degradation");
|
|
}
|
|
|
|
@Test
|
|
void releasingASessionForgetsItsLead() {
|
|
var reg = new PrimaryRegistry(null);
|
|
reg.recordDelegation("term_worker", "term_lead");
|
|
|
|
reg.forgetDelegation("term_worker");
|
|
|
|
assertTrue(reg.nudgeTargetFor("term_worker").isEmpty());
|
|
}
|
|
|
|
@Test
|
|
void aBlankOrNullDelegationIsIgnoredRatherThanStored() {
|
|
var reg = new PrimaryRegistry(null);
|
|
reg.recordDelegation("term_worker", " ");
|
|
reg.recordDelegation(null, "term_lead");
|
|
reg.recordDelegation(" ", "term_lead");
|
|
|
|
assertTrue(reg.nudgeTargetFor("term_worker").isEmpty());
|
|
assertTrue(reg.nudgeTargetFor(null).isEmpty());
|
|
}
|
|
}
|