CB-548: correct architect premise — profile-only slots, registry-owned bindings, dup-key rejection
CI / build (pull_request) Successful in 54s
CI / contract (pull_request) Successful in 1m6s

This commit is contained in:
Dai Ha
2026-08-13 18:05:53 +02:00
parent 21cfc09f8e
commit 6123576c68
5 changed files with 412 additions and 117 deletions
@@ -3,24 +3,30 @@ package dev.ltms.bridged.auth;
import dev.ltms.bridged.config.BridgedConfig;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import static org.junit.jupiter.api.Assertions.*;
/**
* CB-548 — the architect-slot registry: the config snapshot of slot → profile, and the live
* terminal → slot binding the resolver reads. The role the binding produces is asserted in
* {@link CallerResolverTest}; this pins the registry object itself.
* terminal → slot bindings it owns. The role a binding produces is asserted in
* {@link CallerResolverTest}; this pins the registry object itself — its invariants and their
* thread-safety.
*/
class ArchitectRegistryTest {
private static final Map<String, BridgedConfig.Architect> SLOTS = Map.of(
"lead-designer", new BridgedConfig.Architect("term_design", "sonnet"),
"reviewer", new BridgedConfig.Architect(null, "gx10"));
"lead-designer", new BridgedConfig.Architect("sonnet"),
"reviewer", new BridgedConfig.Architect("gx10"));
private final ArchitectRegistry registry =
new ArchitectRegistry(SLOTS, () -> Map.of("term_design", "lead-designer"));
private final ArchitectRegistry registry = new ArchitectRegistry(SLOTS);
@Test
void exposesTheConfiguredSlots() {
@@ -37,30 +43,195 @@ class ArchitectRegistryTest {
}
@Test
void resolvesTheSlotOfALiveTerminal() {
assertEquals("lead-designer", registry.slotForTerminal("term_design"));
assertNull(registry.slotForTerminal("term_unbound"));
void startsEmptySoNoTerminalResolvesToAnArchitect() {
assertTrue(registry.snapshot().isEmpty());
assertNull(registry.slotForTerminal("term_design"),
"config declares no architect terminal — nothing is recognised until a bind");
assertNull(registry.slotForTerminal(null), "no terminal ⇒ no slot");
}
// ── bind ──────────────────────────────────────────────────────────────────────────────────
@Test
void theBindingIsLiveReReadPerCall() {
Map<String, String> live = new HashMap<>();
ArchitectRegistry r = new ArchitectRegistry(SLOTS, () -> live);
assertNull(r.slotForTerminal("term_design"));
live.put("term_design", "lead-designer"); // injected after construction
assertEquals("lead-designer", r.slotForTerminal("term_design"));
void bindResolvesTheTerminalToTheSlot() {
assertTrue(registry.bind("lead-designer", "term_design"));
assertEquals("lead-designer", registry.slotForTerminal("term_design"));
assertEquals(Map.of("term_design", "lead-designer"), registry.snapshot());
}
@Test
void bindRefusesAnUnknownSlot() {
assertFalse(registry.bind("nope", "term_x"),
"a slot that is not configured must be refused — bind is not a way to invent one");
assertNull(registry.slotForTerminal("term_x"));
}
@Test
void bindRefusesATerminalInTwoSlots() {
assertTrue(registry.bind("lead-designer", "term_design"));
assertFalse(registry.bind("reviewer", "term_design"),
"a terminal may occupy at most one slot");
assertEquals("lead-designer", registry.slotForTerminal("term_design"),
"the first binding survives the refused second");
}
@Test
void bindRefusesASlotWithTwoTerminals() {
assertTrue(registry.bind("lead-designer", "term_design"));
assertFalse(registry.bind("lead-designer", "term_other"),
"a slot may host at most one terminal");
assertEquals("lead-designer", registry.slotForTerminal("term_design"),
"the first binding survives the refused second");
assertNull(registry.slotForTerminal("term_other"));
}
@Test
void rebindingTheSamePairIsAnIdempotentNoOp() {
assertTrue(registry.bind("lead-designer", "term_design"));
assertTrue(registry.bind("lead-designer", "term_design"),
"the same terminal → slot is harmless to repeat");
assertEquals(1, registry.snapshot().size());
}
// ── unbind ────────────────────────────────────────────────────────────────────────────────
@Test
void unbindRemovesTheExactBinding() {
assertTrue(registry.bind("lead-designer", "term_design"));
assertTrue(registry.unbind("lead-designer", "term_design"));
assertNull(registry.slotForTerminal("term_design"));
assertTrue(registry.snapshot().isEmpty());
}
@Test
void aStaleUnbindDoesNotRemoveAReplacement() {
// Bind, tear down, and stand the slot back up with a NEW terminal.
assertTrue(registry.bind("lead-designer", "term_design"));
registry.unbind("lead-designer", "term_design");
assertTrue(registry.bind("lead-designer", "term_new"));
// A late unbind naming the OLD terminal must not remove the replacement binding.
assertFalse(registry.unbind("lead-designer", "term_design"));
assertEquals("lead-designer", registry.slotForTerminal("term_new"),
"the replacement terminal stays bound");
}
@Test
void aStaleUnbindForATerminalThatMovedSlotsDoesNothing() {
// term_design starts in lead-designer, is torn down, and stands back up in a FREE slot.
assertTrue(registry.bind("lead-designer", "term_design"));
registry.unbind("lead-designer", "term_design");
assertTrue(registry.bind("reviewer", "term_design"));
// Unbinding against the slot it no longer occupies is refused; the new binding is intact.
assertFalse(registry.unbind("lead-designer", "term_design"),
"the old slot must not unbind a terminal that moved elsewhere");
assertEquals("reviewer", registry.slotForTerminal("term_design"));
}
@Test
void unbindOfNothingIsAFalseNoOp() {
assertFalse(registry.unbind("lead-designer", "term_design"),
"nothing was bound, so nothing is removed");
}
// ── snapshot ─────────────────────────────────────────────────────────────────────────────
@Test
void theSnapshotIsAnImmutableCopyNotAliveState() {
assertTrue(registry.bind("lead-designer", "term_design"));
Map<String, String> snap = registry.snapshot();
assertThrows(UnsupportedOperationException.class, () -> snap.put("x", "y"),
"a handed-out snapshot cannot be mutated in place");
// Later binds must not leak into an earlier snapshot.
assertTrue(registry.bind("reviewer", "term_review"));
assertFalse(snap.containsKey("term_review"),
"a snapshot is a point-in-time copy, not a live view");
}
// ── concurrency (CB-548 invariants hold under contention) ─────────────────────────────────
@Test
void concurrentBindsNeverGiveASlotTwoTerminals() throws Exception {
int n = 16;
ExecutorService pool = Executors.newFixedThreadPool(n);
try {
CountDownLatch go = new CountDownLatch(1);
List<Future<Boolean>> results = new ArrayList<>();
for (int i = 0; i < n; i++) {
final String term = "term_" + i; // every thread races for the SAME slot
results.add(pool.submit(() -> {
go.await();
return registry.bind("lead-designer", term);
}));
}
go.countDown();
int won = 0;
for (Future<Boolean> r : results) {
if (r.get()) {
won++;
}
}
assertEquals(1, won, "exactly one terminal may win the sole slot, got " + won);
assertEquals(1, registry.snapshot().size(),
"the slot hosts at most one terminal after the race");
} finally {
pool.shutdownNow();
}
}
@Test
void concurrentBindsNeverPutOneTerminalInTwoSlots() throws Exception {
int n = 16;
ExecutorService pool = Executors.newFixedThreadPool(n);
try {
CountDownLatch go = new CountDownLatch(1);
List<Future<String>> results = new ArrayList<>();
for (int i = 0; i < n; i++) {
final String slot = (i % 2 == 0) ? "lead-designer" : "reviewer"; // all race for ONE terminal
results.add(pool.submit(() -> {
go.await();
return registry.bind(slot, "shared_term")
? registry.slotForTerminal("shared_term") : null;
}));
}
go.countDown();
// Rebinding the same terminal to the same slot is a harmless idempotent true, so count
// winners is not the assertion — agreement is: every thread that reported success must
// have seen the terminal in the SAME slot, never in two at once.
String bound = null;
boolean conflict = false;
for (Future<String> r : results) {
String s = r.get();
if (s != null) {
if (bound == null) {
bound = s;
} else if (!bound.equals(s)) {
conflict = true;
}
}
}
assertFalse(conflict, "a terminal was observed in two slots at once");
assertNotNull(bound, "at least one thread bound the terminal");
assertEquals(1, registry.snapshot().size(),
"the terminal occupies exactly one slot in the final snapshot");
assertEquals(bound, registry.slotForTerminal("shared_term"));
} finally {
pool.shutdownNow();
}
}
/** A handed-over slot map is snapshotted at construction, not offered as live state. */
@Test
void theSlotSnapshotIsFixedByConstruction() {
Map<String, BridgedConfig.Architect> mutable = new HashMap<>(SLOTS);
ArchitectRegistry r = new ArchitectRegistry(mutable, Map::of);
ArchitectRegistry r = new ArchitectRegistry(mutable);
mutable.put("hijack", new BridgedConfig.Architect("t", "gx10"));
mutable.put("hijack", new BridgedConfig.Architect("gx10"));
assertFalse(r.isSlot("hijack"), "a handed-over map is not offered as live state");
}
@@ -1,5 +1,6 @@
package dev.ltms.bridged.config;
import dev.ltms.bridged.auth.ArchitectRegistry;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -330,7 +331,7 @@ class BridgedConfigTest {
// ── CB-548: the architects registry ────────────────────────────────────────────────────────
@Test
void architectsBlockBindsSlotsByGatewayLocalName(@TempDir Path dir) throws Exception {
void architectsBlockDeclaresSlotsByNameAndProfileOnly(@TempDir Path dir) throws Exception {
Path f = dir.resolve("architects.yaml");
Files.writeString(f, """
bind:
@@ -340,7 +341,6 @@ class BridgedConfigTest {
baseUrl: http://gx10.gw:8000
architects:
lead-designer:
terminal: term_design
profile: sonnet
reviewer:
profile: sonnet
@@ -351,15 +351,15 @@ class BridgedConfigTest {
"slot names are the keys — gateway-local unique by construction");
assertEquals("sonnet", cfg.architects().get("lead-designer").profile(),
"each slot carries its strong-model profile reference");
assertEquals("term_design", cfg.architects().get("lead-designer").terminal());
// A slot with no terminal binds nothing yet — the live binding may supply it later.
assertTrue(cfg.architects().get("reviewer").terminal() == null
|| cfg.architects().get("reviewer").terminal().isBlank());
assertEquals("sonnet", cfg.architects().get("reviewer").profile());
}
@Test
void architectTerminalsMapsEachBoundSlotByItsPane(@TempDir Path dir) throws Exception {
Path f = dir.resolve("arch-terminals.yaml");
void anArchitectCarriesNoConfigTerminalSoNothingIsRecognisedYet(@TempDir Path dir) throws Exception {
// The corrected CB-548 premise: config declares slots (name + profile) only. A `terminal:`
// key left over from the earlier premise is ignored — an architect is NOT recognised from
// config the way a lead is, so it binds nothing at startup and resolves no architect.
Path f = dir.resolve("arch-stale-terminal.yaml");
Files.writeString(f, """
bind:
port: 8080
@@ -370,26 +370,24 @@ class BridgedConfigTest {
lead-designer:
terminal: term_design
profile: sonnet
reviewer:
terminal: term_review
profile: sonnet
unbound:
profile: sonnet
""");
BridgedConfig cfg = BridgedConfig.load(f);
assertEquals("sonnet", cfg.architects().get("lead-designer").profile(),
"the profile is still read even when a stray terminal is ignored");
assertEquals(Map.of("term_design", "lead-designer", "term_review", "reviewer"),
BridgedConfig.load(f).architectTerminals(),
"a slot with no terminal registers no binding; the value is the slot name");
// The registry built from this config owns no bindings: the slot is idle at startup.
ArchitectRegistry r = new ArchitectRegistry(cfg.architects());
assertTrue(r.snapshot().isEmpty());
assertNull(r.slotForTerminal("term_design"),
"a config terminal must not resolve an architect — slots start idle");
}
@Test
void noArchitectsBlockLeavesNothingBound(@TempDir Path dir) throws Exception {
void noArchitectsBlockLeavesNothingConfigured(@TempDir Path dir) throws Exception {
Path f = dir.resolve("no-arch.yaml");
Files.writeString(f, "bind:\n port: 8080\n");
BridgedConfig cfg = BridgedConfig.load(f);
assertNull(cfg.architects());
assertTrue(cfg.architectTerminals().isEmpty(),
assertNull(BridgedConfig.load(f).architects(),
"no architects: block ⇒ no architect identity, exactly as before CB-548");
}
@@ -406,7 +404,6 @@ class BridgedConfigTest {
baseUrl: http://gx10.gw:8000
architects:
lead-designer:
terminal: term_design
profile: ltms-local
""");
@@ -426,7 +423,6 @@ class BridgedConfigTest {
baseUrl: http://gx10.gw:8000
architects:
lead-designer:
terminal: term_design
profile: sonnet
""");
BridgedConfig cfg = BridgedConfig.load(f);
@@ -447,7 +443,6 @@ class BridgedConfigTest {
baseUrl: http://gx10.gw:8000
architects:
lead-designer:
terminal: term_design
profile: ""
""");
BridgedConfig cfg = BridgedConfig.load(f);
@@ -469,7 +464,6 @@ class BridgedConfigTest {
baseUrl: http://gx10.gw:8000
architects:
lead-designer:
terminal: term_design
profile: sonnet
reviewer:
profile: gx10
@@ -486,6 +480,48 @@ class BridgedConfigTest {
assertDoesNotThrow(() -> BridgedConfig.load(f).validateArchitects());
}
@Test
void duplicateArchitectSlotNamesAreRejectedAtParseTime(@TempDir Path dir) throws Exception {
Path f = dir.resolve("arch-dup.yaml");
Files.writeString(f, """
bind:
port: 8080
workers:
sonnet:
baseUrl: http://gx10.gw:8000
architects:
lead-designer:
profile: sonnet
lead-designer:
profile: sonnet
""");
IllegalStateException e =
assertThrows(IllegalStateException.class, () -> BridgedConfig.load(f));
assertTrue(e.getMessage().contains("lead-designer"),
"the refusal names the duplicated slot, was: " + e.getMessage());
assertTrue(e.getMessage().contains("duplicate architect"),
"the refusal says the slot name is duplicated");
}
@Test
void duplicateKeysOutsideArchitectsAreUnaffected(@TempDir Path dir) throws Exception {
// The duplicate check is scoped to the architects block — a duplicate elsewhere is not this
// guard's concern and must not change parsing of the rest of the config.
Path f = dir.resolve("dup-other.yaml");
Files.writeString(f, """
bind:
port: 8080
workers:
sonnet:
baseUrl: http://gx10.gw:8000
sonnet:
baseUrl: http://gx10.gw:8000
""");
// Last-wins for a non-architect duplicate is untouched: only the architects block is walked.
assertEquals(Set.of("sonnet"), BridgedConfig.load(f).workerProfiles().keySet());
}
@Test
void absentBrokerBlockLeavesInboxSoftState(@TempDir Path dir) throws Exception {
Path f = dir.resolve("no-broker.yaml");