Stage-1 walking skeleton: bridged Java daemon (herdr client + REST + guard)

Maven/Java 25 module under bridged/. End-to-end verified against live
herdr 0.7.0 (protocol 14): GET /healthz and GET /sessions serve real
workspace data through the socket client.

- herdr client (CB-101): Unix-socket JSON-RPC via UnixDomainSocketAddress.
  Two contract facts pinned by tests against the real daemon:
  ids MUST be strings, and herdr is one-shot per connection
  (connection-per-call, which also makes the client lock-free).
- subscription guard: worker base_url must be on the off-subscription
  allowlist (gx00.gw, ollama.ltms.dev); primary env must carry no base_url.
- config (CB-106): Jackson YAML + Logback; example grounded in ltms-local.
- REST app (CB-104 start): injectable HerdrClient so acceptance tests run
  on an ephemeral port with a fake herdr, no daemon/Claude in the loop.
- tests: 17 unit/acceptance (mvn test) + 3 contract (mvn test -Pcontract).
This commit is contained in:
Dai Ha
2026-07-12 20:00:02 +02:00
parent b415003245
commit 93cfdb640f
18 changed files with 1035 additions and 0 deletions
@@ -0,0 +1,55 @@
package dev.ltms.bridged.config;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
class BridgedConfigTest {
@Test
void loadsFullConfig(@TempDir Path dir) throws Exception {
Path f = dir.resolve("bridged.yaml");
Files.writeString(f, """
bind:
host: 127.0.0.1
port: 8080
herdrSocket: ~/.config/herdr/herdr.sock
worker:
profile: ltms-local
baseUrl: http://gx00.gw:8000
model: coder
guard:
offSubscriptionHosts:
- gx00.gw
- ollama.ltms.dev
""");
BridgedConfig cfg = BridgedConfig.load(f);
assertEquals(8080, cfg.bind().port());
assertEquals("ltms-local", cfg.worker().profile());
assertTrue(cfg.guard().hostSet().contains("gx00.gw"));
assertTrue(cfg.guard().hostSet().contains("ollama.ltms.dev"));
}
@Test
void appliesDefaultsForMissingSections(@TempDir Path dir) throws Exception {
Path f = dir.resolve("minimal.yaml");
Files.writeString(f, "bind:\n host: 0.0.0.0\n port: 9000\n");
BridgedConfig cfg = BridgedConfig.load(f);
assertEquals(9000, cfg.bind().port());
assertNotNull(cfg.guard(), "guard must default to empty, never null");
assertTrue(cfg.guard().offSubscriptionHosts().isEmpty());
}
@Test
void ignoresUnknownKeys(@TempDir Path dir) throws Exception {
Path f = dir.resolve("future.yaml");
Files.writeString(f, "bind:\n port: 8080\nfutureFeature:\n enabled: true\n");
assertDoesNotThrow(() -> BridgedConfig.load(f));
}
}
@@ -0,0 +1,52 @@
package dev.ltms.bridged.guard;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
/** The subscription boundary is the system's core invariant — test it hard. */
class SubscriptionGuardTest {
private final SubscriptionGuard guard =
new SubscriptionGuard(Set.of("gx00.gw", "ollama.ltms.dev"));
@Test
void acceptsAllowlistedWorkerHosts() {
assertDoesNotThrow(() -> guard.assertWorker("http://gx00.gw:8000"));
assertDoesNotThrow(() -> guard.assertWorker("https://ollama.ltms.dev"));
}
@Test
void rejectsHostNotOnAllowlist() {
GuardException ex = assertThrows(GuardException.class,
() -> guard.assertWorker("https://api.anthropic.com"));
assertTrue(ex.getMessage().contains("api.anthropic.com"));
}
@Test
void rejectsMissingWorkerBaseUrl() {
assertThrows(GuardException.class, () -> guard.assertWorker(null));
assertThrows(GuardException.class, () -> guard.assertWorker(" "));
}
@Test
void rejectsMalformedWorkerBaseUrl() {
assertThrows(GuardException.class, () -> guard.assertWorker("not a url"));
}
@Test
void primaryWithBaseUrlIsTainted() {
GuardException ex = assertThrows(GuardException.class,
() -> guard.assertPrimaryClean(Map.of("ANTHROPIC_BASE_URL", "http://gx00.gw:8000")));
assertTrue(ex.getMessage().contains("tainted"));
}
@Test
void cleanPrimaryPasses() {
assertDoesNotThrow(() -> guard.assertPrimaryClean(Map.of("PATH", "/usr/bin")));
assertDoesNotThrow(() -> guard.assertPrimaryClean(Map.of("ANTHROPIC_BASE_URL", "")));
}
}
@@ -0,0 +1,63 @@
package dev.ltms.bridged.herdr;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/** Unit tests for the herdr wire codec — the framing rules the spike pinned down. */
class HerdrCodecTest {
private final ObjectMapper mapper = new ObjectMapper();
private final HerdrCodec codec = new HerdrCodec(mapper);
@Test
void encodesStringIdAndTrailingNewline() throws Exception {
byte[] frame = codec.encode("7", "ping", null);
String s = new String(frame, StandardCharsets.UTF_8);
assertTrue(s.endsWith("\n"), "frame must be newline-terminated");
JsonNode node = mapper.readTree(s);
// id MUST be a JSON string — herdr rejects integer ids with invalid_request.
assertTrue(node.get("id").isTextual());
assertEquals("7", node.get("id").asText());
assertEquals("ping", node.get("method").asText());
assertTrue(node.get("params").isObject(), "null params serialize to {}");
}
@Test
void encodesParamsObject() throws Exception {
byte[] frame = codec.encode("1", "pane.list", Map.of("workspace_id", "w2"));
JsonNode node = mapper.readTree(new String(frame, StandardCharsets.UTF_8));
assertEquals("w2", node.get("params").get("workspace_id").asText());
}
@Test
void decodesResultNode() {
String line = "{\"id\":\"1\",\"result\":{\"type\":\"pong\",\"protocol\":14}}";
JsonNode result = codec.decodeResult(line);
assertEquals(14, result.get("protocol").asInt());
assertEquals("pong", result.get("type").asText());
}
@Test
void throwsOnErrorEnvelope() {
// Exactly the shape herdr returned for an integer id during the spike.
String line = "{\"id\":\"\",\"error\":{\"code\":\"invalid_request\","
+ "\"message\":\"invalid request: invalid type: integer\"}}";
HerdrException ex = assertThrows(HerdrException.class, () -> codec.decodeResult(line));
assertEquals("invalid_request", ex.code());
assertTrue(ex.getMessage().contains("invalid_request"));
}
@Test
void throwsOnFrameWithNeitherResultNorError() {
HerdrException ex = assertThrows(HerdrException.class,
() -> codec.decodeResult("{\"id\":\"1\"}"));
assertTrue(ex.getMessage().contains("neither result nor error"));
}
}
@@ -0,0 +1,60 @@
package dev.ltms.bridged.herdr;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
/**
* Contract test against a REAL running herdr. Tagged {@code contract} so it is
* excluded from {@code mvn test}; run it with {@code mvn test -Pcontract}. It fails
* loudly if herdr drifts from the protocol {@code bridged} was built against
* (0.7.0, protocol 14) — catching breakage that unit tests with canned frames cannot.
*/
@Tag("contract")
class HerdrContractTest {
private Path socket() {
return UnixSocketHerdrClient.defaultSocketPath();
}
@Test
void pingReturnsProtocol14() {
assumeTrue(Files.exists(socket()), "no herdr socket at " + socket() + " — skipping");
try (UnixSocketHerdrClient herdr = UnixSocketHerdrClient.connect()) {
JsonNode pong = herdr.call("ping");
assertEquals("pong", pong.get("type").asText());
assertEquals(14, pong.get("protocol").asInt(),
"bridged is built against herdr protocol 14");
assertFalse(pong.get("version").asText().isBlank());
}
}
@Test
void workspaceListReturnsWorkspaces() {
assumeTrue(Files.exists(socket()), "no herdr socket — skipping");
try (UnixSocketHerdrClient herdr = UnixSocketHerdrClient.connect()) {
JsonNode result = herdr.call("workspace.list");
assertEquals("workspace_list", result.get("type").asText());
assertTrue(result.get("workspaces").isArray());
}
}
@Test
void multipleSequentialCallsSucceed() {
// herdr is one-shot per connection: a persistent socket breaks on the 2nd call.
// The connection-per-call client must let many sequential calls through — this
// is the exact scenario that failed the first end-to-end smoke test.
assumeTrue(Files.exists(socket()), "no herdr socket — skipping");
try (UnixSocketHerdrClient herdr = UnixSocketHerdrClient.connect()) {
assertEquals("pong", herdr.call("ping").get("type").asText());
assertEquals("workspace_list", herdr.call("workspace.list").get("type").asText());
assertEquals("pong", herdr.call("ping").get("type").asText());
}
}
}
@@ -0,0 +1,97 @@
package dev.ltms.bridged.rest;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.ltms.bridged.herdr.HerdrClient;
import dev.ltms.bridged.herdr.HerdrException;
import io.javalin.Javalin;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import static org.junit.jupiter.api.Assertions.*;
/**
* REST acceptance tests — the feature contract exercised over plain HTTP with a fake
* herdr, no live daemon and no Claude in the loop. This is the surface later MCP
* tools must match by parity.
*/
class BridgedAppTest {
private final ObjectMapper mapper = new ObjectMapper();
private final HttpClient http = HttpClient.newHttpClient();
private Javalin app;
@AfterEach
void stop() {
if (app != null) app.stop();
}
/** Fake herdr returning canned frames captured from the real 0.7.0 daemon. */
private HerdrClient fakeHerdr(boolean healthy) {
return new HerdrClient() {
@Override
public JsonNode call(String method, Object params) {
if (!healthy) throw new HerdrException("herdr unreachable (fake)");
try {
return switch (method) {
case "ping" -> mapper.readTree(
"{\"type\":\"pong\",\"version\":\"0.7.0\",\"protocol\":14}");
case "workspace.list" -> mapper.readTree("""
{"type":"workspace_list","workspaces":[
{"workspace_id":"w1","label":"dev-mgnl","focused":true,"pane_count":7,"agent_status":"unknown"},
{"workspace_id":"w2","label":"ltms","focused":false,"pane_count":5,"agent_status":"done"}]}""");
default -> throw new HerdrException("unexpected method " + method);
};
} catch (Exception e) {
throw new HerdrException("fake decode failed", e);
}
}
@Override public void close() { }
};
}
private int startWith(HerdrClient herdr) {
app = new BridgedApp(herdr).build().start("127.0.0.1", 0);
return app.port();
}
private HttpResponse<String> get(int port, String path) throws Exception {
return http.send(
HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + port + path)).GET().build(),
HttpResponse.BodyHandlers.ofString());
}
@Test
void healthzOkWhenHerdrAnswers() throws Exception {
int port = startWith(fakeHerdr(true));
HttpResponse<String> res = get(port, "/healthz");
assertEquals(200, res.statusCode());
JsonNode body = mapper.readTree(res.body());
assertEquals("ok", body.get("status").asText());
assertEquals(14, body.get("herdr").get("protocol").asInt());
}
@Test
void healthzDegradedWhenHerdrDown() throws Exception {
int port = startWith(fakeHerdr(false));
HttpResponse<String> res = get(port, "/healthz");
assertEquals(503, res.statusCode());
assertEquals("degraded", mapper.readTree(res.body()).get("status").asText());
}
@Test
void sessionsMapsWorkspaceList() throws Exception {
int port = startWith(fakeHerdr(true));
HttpResponse<String> res = get(port, "/sessions");
assertEquals(200, res.statusCode());
JsonNode sessions = mapper.readTree(res.body()).get("sessions");
assertEquals(2, sessions.size());
assertEquals("w1", sessions.get(0).get("id").asText());
assertEquals("done", sessions.get(1).get("agentStatus").asText());
}
}