#604 item 1: fleet_list reports charterBytes alongside charterSha256 #605
@@ -871,10 +871,23 @@ public final class SessionManager implements TurnListener {
|
||||
// digest lets a lead tell at a glance whether all members got the same charter; the source
|
||||
// records whether a role charter was configured ("fleet.charters.<role>") or only the reply
|
||||
// charter was composed ("none").
|
||||
// #604: charterBytes rides along with charterSha256, not with charterSource — it is only
|
||||
// meaningful as the digest's companion (a length turns "they differ" into "by how much").
|
||||
// A member with no composed charter reports charterSource and nothing else, as before.
|
||||
//
|
||||
// Gating on the DIGEST rather than on the receipt is deliberate, and the two are not
|
||||
// always null together. CharterReceipt.compose() derives the digest with digestOf(), which
|
||||
// returns null for BLANK text, while the byte count is composed.getBytes().length, which
|
||||
// does not. So a whitespace-only charter (a blank fleet.charters.<role> on a profile with
|
||||
// no MCP, so no reply charter is appended) yields a null digest beside a non-zero size.
|
||||
// Reporting a size with no digest would say "they differ by N bytes" about an artifact we
|
||||
// cannot fingerprint, so this gate omits both. Never widen it to the receipt-level null
|
||||
// check without deciding what that case should report.
|
||||
if (session.charterReceipt() != null) {
|
||||
m.put("charterSource", session.charterReceipt().charterSource());
|
||||
if (session.charterReceipt().charterSha256() != null) {
|
||||
m.put("charterSha256", session.charterReceipt().charterSha256());
|
||||
m.put("charterBytes", session.charterReceipt().charterBytes());
|
||||
}
|
||||
}
|
||||
m.put("liveStatus", live == null ? "unknown" : live.status().name().toLowerCase());
|
||||
|
||||
@@ -328,9 +328,10 @@ class SessionManagerTest {
|
||||
void rosterViewExposesTheCharterReceiptButNeverTheCharterText() {
|
||||
// The roster (fleet_list and GET /members both render through rosterView) must let a lead
|
||||
// see which charter a member got, without ever carrying the charter prose itself (CB-571).
|
||||
String composed = "role charter\n\nreply";
|
||||
CharterReceipt receipt = CharterReceipt.compose(MemberRole.DEV, "prof", "role charter", composed);
|
||||
MemberSession s = new MemberSession("p1", "term1", "prof", MemberRole.DEV, "/cwd", null,
|
||||
0, 0, 0, MemberSession.State.READY, null, null,
|
||||
CharterReceipt.compose(MemberRole.DEV, "prof", "role charter", "role charter\n\nreply"), null);
|
||||
0, 0, 0, MemberSession.State.READY, null, null, receipt, null);
|
||||
|
||||
Map<String, Object> view = SessionManager.rosterView(s, null);
|
||||
|
||||
@@ -338,10 +339,49 @@ class SessionManagerTest {
|
||||
"the config key that supplied the role charter is reported");
|
||||
assertEquals(CharterReceipt.digestOf("role charter\n\nreply"), view.get("charterSha256"),
|
||||
"the digest of the exact composed charter bytes is reported");
|
||||
// #604: the byte count rides alongside the digest, and must match what the receipt itself
|
||||
// carries (not a hardcoded literal) so a bug that reads the wrong field is caught.
|
||||
assertEquals(receipt.charterBytes(), view.get("charterBytes"),
|
||||
"the exact composed byte count is reported, read from the receipt");
|
||||
assertEquals(composed.getBytes(java.nio.charset.StandardCharsets.UTF_8).length, view.get("charterBytes"),
|
||||
"the byte count is the real UTF-8 length of the composed charter");
|
||||
assertFalse(view.values().toString().contains("role charter"),
|
||||
"the roster row must not embed the charter text itself");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rosterViewOmitsCharterBytesAndDigestWhenNoCharterWasComposed() {
|
||||
// #604: a member with no role charter and no reply charter (composed == null) still reports
|
||||
// charterSource ("none"), but neither a digest nor a size — the digest is absent, and the
|
||||
// size only ever accompanies a real digest. This must not be satisfiable by code that always
|
||||
// writes charterBytes.
|
||||
CharterReceipt receipt = CharterReceipt.compose(MemberRole.DEV, "prof", null, null);
|
||||
MemberSession s = new MemberSession("p1", "term1", "prof", MemberRole.DEV, "/cwd", null,
|
||||
0, 0, 0, MemberSession.State.READY, null, null, receipt, null);
|
||||
|
||||
Map<String, Object> view = SessionManager.rosterView(s, null);
|
||||
|
||||
assertEquals(CharterReceipt.NO_SOURCE, view.get("charterSource"),
|
||||
"no configured role or reply charter reports the explicit \"none\" source");
|
||||
assertFalse(view.containsKey("charterSha256"), "no digest is reported when no charter was composed");
|
||||
assertFalse(view.containsKey("charterBytes"), "no byte count is reported when no charter was composed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rosterViewOmitsCharterFieldsEntirelyWhenTheReceiptItselfIsAbsent() {
|
||||
// #604 acceptance criterion 3: charterReceipt() can be null on its own (a session recorded
|
||||
// before CB-571, or a launcher that never composed one) — the outer null-guard must still
|
||||
// suppress charterSource, charterSha256 AND charterBytes together.
|
||||
MemberSession s = new MemberSession("p1", "term1", "prof", MemberRole.DEV, "/cwd", null,
|
||||
0, 0, 0, MemberSession.State.READY, null, null, null, null);
|
||||
|
||||
Map<String, Object> view = SessionManager.rosterView(s, null);
|
||||
|
||||
assertFalse(view.containsKey("charterSource"), "no charter fields at all when the receipt is null");
|
||||
assertFalse(view.containsKey("charterSha256"), "no charter fields at all when the receipt is null");
|
||||
assertFalse(view.containsKey("charterBytes"), "no charter fields at all when the receipt is null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aNullTerminalFromThePrimaryIsANoOpEvenWithSessionsRegistered() {
|
||||
// The primary resolves to a Principal with no terminal, and FleetMcp's context extractor
|
||||
|
||||
Reference in New Issue
Block a user