Skip to content

Commit bafcd25

Browse files
hyperpolymathclaude
andcommitted
test(local-coord-mcp): restart-preserves-state integration tests
Three end-to-end tests exercise the durability seam: 1. Build state (sup + gem peers, context, unreceived inbox msg, held claim, pending Tier 3 quarantine) → close + reset + reopen + replay → every piece reconstructed: same peer slots / suffixes / roles, context readable, inbox delivers on receive, claim still held against a stealer, quarantine visible to supervisor. 2. Approve + restart: quarantine slot cleared, delivered message survives in recipient inbox (approve+deliver both logged, both replay). 3. Reject + restart: quarantine cleared, message NOT delivered to recipient (reject log replays as termination, inbox push never happened). Also fixes coord_reset(): previously only wiped peers + claims, leaving stale quarantine entries + next_request_id carryover across tests. Now resets every durable-state array. All 27 tests pass (19 baseline + 5 durability module + 3 integration). Task #7 Pass B complete. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 282de53 commit bafcd25

1 file changed

Lines changed: 229 additions & 0 deletions

File tree

cartridges/local-coord-mcp/ffi/local_coord_ffi.zig

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,8 @@ pub export fn coord_reset() void {
11271127
defer mutex.unlock();
11281128
peers = [_]Peer{empty_peer} ** MAX_PEERS;
11291129
claims = [_]Claim{empty_claim} ** MAX_CLAIMS;
1130+
quarantine = [_]QuarantineEntry{empty_quar} ** MAX_QUARANTINE;
1131+
next_request_id = 1;
11301132
}
11311133

11321134
// ═══════════════════════════════════════════════════════════════════════
@@ -1557,3 +1559,230 @@ test "invoke: buffer too small returns -3" {
15571559
try std.testing.expectEqual(@as(i32, -3), rc);
15581560
try std.testing.expect(len > 4);
15591561
}
1562+
1563+
// ═══════════════════════════════════════════════════════════════════════
1564+
// Durability integration tests — restart-preserves-state
1565+
// ═══════════════════════════════════════════════════════════════════════
1566+
1567+
fn tmpCoordDir(buf: []u8) ![]u8 {
1568+
return std.fmt.bufPrint(buf, "/tmp/boj-coord-integ-{d}-{d}", .{
1569+
std.time.milliTimestamp(),
1570+
std.crypto.random.int(u32),
1571+
});
1572+
}
1573+
1574+
test "restart replay restores peer, claim, inbox, quarantine" {
1575+
coord_reset();
1576+
dur.close();
1577+
1578+
var path_buf: [256]u8 = undefined;
1579+
const dir = try tmpCoordDir(&path_buf);
1580+
defer std.fs.cwd().deleteTree(dir) catch {};
1581+
1582+
try std.testing.expect(dur.openWithDir(dir));
1583+
dur.truncate();
1584+
1585+
// ── Phase 1: build up state under durable logging ─────────────────
1586+
var sup_tok: [TOKEN_LEN]u8 = undefined;
1587+
var sup_suf: [4]u8 = undefined;
1588+
const sup_idx = coord_register(0, 1, &sup_tok, &sup_suf); // claude as executor
1589+
try std.testing.expect(sup_idx >= 0);
1590+
// Promote directly via state mutation — avoids env-var gymnastics
1591+
// in-test, and still gets persisted via the coord_set_role path below.
1592+
peers[@intCast(sup_idx)].role = .supervisor;
1593+
dur.logPeerRoleSet(@intCast(sup_idx), @intCast(@intFromEnum(Role.supervisor)));
1594+
1595+
var gem_tok: [TOKEN_LEN]u8 = undefined;
1596+
var gem_suf: [4]u8 = undefined;
1597+
const gem_idx = coord_register(1, -1, &gem_tok, &gem_suf); // gemini → supervised
1598+
try std.testing.expect(gem_idx >= 0);
1599+
1600+
// Remember identities for post-replay comparison.
1601+
const sup_suf_copy = sup_suf;
1602+
const gem_suf_copy = gem_suf;
1603+
1604+
// Set a context on the supervised peer.
1605+
const ctx = "007-lang";
1606+
try std.testing.expectEqual(
1607+
@as(c_int, 0),
1608+
coord_set_context(&gem_tok, TOKEN_LEN, ctx.ptr, @intCast(ctx.len)),
1609+
);
1610+
1611+
// Send a direct message sup → gem; leave it unreceived so it must
1612+
// come back from replay.
1613+
const pending_msg = "pending-direct-message";
1614+
try std.testing.expectEqual(
1615+
@as(c_int, 1),
1616+
coord_send(&sup_tok, TOKEN_LEN, gem_idx, pending_msg.ptr, @intCast(pending_msg.len)),
1617+
);
1618+
1619+
// Claim a task as the supervisor.
1620+
const task = "restart-replay-task";
1621+
try std.testing.expectEqual(
1622+
@as(c_int, 0),
1623+
coord_claim_task(&sup_tok, TOKEN_LEN, task.ptr, @intCast(task.len)),
1624+
);
1625+
1626+
// Gemini files a Tier 3 gated op — lands in quarantine.
1627+
const gated_msg = "proposed-commit";
1628+
const gated_rc = coord_send_gated(&gem_tok, TOKEN_LEN, sup_idx, gated_msg.ptr, @intCast(gated_msg.len), 3);
1629+
try std.testing.expect(gated_rc < -1000);
1630+
const request_id: u32 = @intCast(-(gated_rc + 1000));
1631+
1632+
// ── Phase 2: simulate adapter restart — close log, wipe memory, reopen, replay ──
1633+
dur.close();
1634+
coord_reset();
1635+
try std.testing.expect(dur.openWithDir(dir));
1636+
dur.replay(replayDispatch);
1637+
defer {
1638+
dur.close();
1639+
}
1640+
1641+
// ── Phase 3: verify state reconstructed ───────────────────────────
1642+
1643+
// Peers re-occupy their original slots with original suffixes.
1644+
try std.testing.expect(peers[@intCast(sup_idx)].active);
1645+
try std.testing.expectEqualSlices(u8, &sup_suf_copy, &peers[@intCast(sup_idx)].suffix);
1646+
try std.testing.expectEqual(Role.supervisor, peers[@intCast(sup_idx)].role);
1647+
1648+
try std.testing.expect(peers[@intCast(gem_idx)].active);
1649+
try std.testing.expectEqualSlices(u8, &gem_suf_copy, &peers[@intCast(gem_idx)].suffix);
1650+
try std.testing.expectEqual(Role.supervised, peers[@intCast(gem_idx)].role);
1651+
1652+
// Context survives replay.
1653+
var ctx_buf: [MAX_CONTEXT]u8 = undefined;
1654+
const ctx_len = coord_read_peer_context(gem_idx, &ctx_buf, @intCast(ctx_buf.len));
1655+
try std.testing.expectEqual(@as(c_int, @intCast(ctx.len)), ctx_len);
1656+
try std.testing.expectEqualSlices(u8, ctx, ctx_buf[0..@intCast(ctx_len)]);
1657+
1658+
// Pending inbox message delivers to gemini on receive.
1659+
var recv_buf: [512]u8 = undefined;
1660+
const recv_len = coord_receive(&gem_tok, TOKEN_LEN, &recv_buf, @intCast(recv_buf.len));
1661+
try std.testing.expectEqual(@as(c_int, @intCast(pending_msg.len)), recv_len);
1662+
try std.testing.expectEqualSlices(u8, pending_msg, recv_buf[0..@intCast(recv_len)]);
1663+
1664+
// Claim still held by supervisor — another peer can't grab it.
1665+
const steal_rc = coord_claim_task(&gem_tok, TOKEN_LEN, task.ptr, @intCast(task.len));
1666+
try std.testing.expectEqual(@as(c_int, 1), steal_rc); // Held
1667+
1668+
// Supervisor's own re-claim is idempotent.
1669+
try std.testing.expectEqual(
1670+
@as(c_int, 0),
1671+
coord_claim_task(&sup_tok, TOKEN_LEN, task.ptr, @intCast(task.len)),
1672+
);
1673+
1674+
// Quarantine entry reappears for the supervisor to review.
1675+
var review_buf: [512]u8 = undefined;
1676+
const n = coord_review(&sup_tok, TOKEN_LEN, &review_buf, @intCast(review_buf.len));
1677+
try std.testing.expectEqual(@as(c_int, 1), n);
1678+
1679+
var body_buf: [512]u8 = undefined;
1680+
const body_len = coord_review_entry(&sup_tok, TOKEN_LEN, @intCast(request_id), &body_buf, @intCast(body_buf.len));
1681+
try std.testing.expectEqual(@as(c_int, @intCast(gated_msg.len)), body_len);
1682+
try std.testing.expectEqualSlices(u8, gated_msg, body_buf[0..@intCast(body_len)]);
1683+
1684+
coord_reset();
1685+
}
1686+
1687+
test "approve then restart: quarantine gone, delivered message survives" {
1688+
coord_reset();
1689+
dur.close();
1690+
1691+
var path_buf: [256]u8 = undefined;
1692+
const dir = try tmpCoordDir(&path_buf);
1693+
defer std.fs.cwd().deleteTree(dir) catch {};
1694+
1695+
try std.testing.expect(dur.openWithDir(dir));
1696+
dur.truncate();
1697+
1698+
var sup_tok: [TOKEN_LEN]u8 = undefined;
1699+
var sup_suf: [4]u8 = undefined;
1700+
const sup_idx = coord_register(0, 1, &sup_tok, &sup_suf);
1701+
try std.testing.expect(sup_idx >= 0);
1702+
peers[@intCast(sup_idx)].role = .supervisor;
1703+
dur.logPeerRoleSet(@intCast(sup_idx), @intCast(@intFromEnum(Role.supervisor)));
1704+
1705+
var gem_tok: [TOKEN_LEN]u8 = undefined;
1706+
var gem_suf: [4]u8 = undefined;
1707+
const gem_idx = coord_register(1, -1, &gem_tok, &gem_suf);
1708+
try std.testing.expect(gem_idx >= 0);
1709+
1710+
// Supervised files, supervisor approves — approved message is now in
1711+
// sup's inbox and the quarantine slot is freed.
1712+
const msg = "gated-and-approved";
1713+
const gated_rc = coord_send_gated(&gem_tok, TOKEN_LEN, sup_idx, msg.ptr, @intCast(msg.len), 3);
1714+
try std.testing.expect(gated_rc < -1000);
1715+
const rid: u32 = @intCast(-(gated_rc + 1000));
1716+
try std.testing.expectEqual(@as(c_int, 0), coord_approve(&sup_tok, TOKEN_LEN, @intCast(rid)));
1717+
1718+
dur.close();
1719+
coord_reset();
1720+
try std.testing.expect(dur.openWithDir(dir));
1721+
dur.replay(replayDispatch);
1722+
defer dur.close();
1723+
1724+
// Quarantine empty after replay (add + approve cancel out).
1725+
var review_buf: [256]u8 = undefined;
1726+
try std.testing.expectEqual(@as(c_int, 0), coord_review(&sup_tok, TOKEN_LEN, &review_buf, @intCast(review_buf.len)));
1727+
1728+
// Approved message remains in sup's inbox.
1729+
var recv_buf: [512]u8 = undefined;
1730+
const n = coord_receive(&sup_tok, TOKEN_LEN, &recv_buf, @intCast(recv_buf.len));
1731+
try std.testing.expectEqual(@as(c_int, @intCast(msg.len)), n);
1732+
try std.testing.expectEqualSlices(u8, msg, recv_buf[0..@intCast(n)]);
1733+
1734+
coord_reset();
1735+
}
1736+
1737+
test "reject then restart: quarantine gone, message NOT delivered" {
1738+
coord_reset();
1739+
dur.close();
1740+
1741+
var path_buf: [256]u8 = undefined;
1742+
const dir = try tmpCoordDir(&path_buf);
1743+
defer std.fs.cwd().deleteTree(dir) catch {};
1744+
1745+
try std.testing.expect(dur.openWithDir(dir));
1746+
dur.truncate();
1747+
1748+
var sup_tok: [TOKEN_LEN]u8 = undefined;
1749+
var sup_suf: [4]u8 = undefined;
1750+
_ = coord_register(0, 1, &sup_tok, &sup_suf);
1751+
peers[0].role = .supervisor;
1752+
dur.logPeerRoleSet(0, @intCast(@intFromEnum(Role.supervisor)));
1753+
1754+
var gem_tok: [TOKEN_LEN]u8 = undefined;
1755+
var gem_suf: [4]u8 = undefined;
1756+
_ = coord_register(1, -1, &gem_tok, &gem_suf);
1757+
1758+
const msg = "gated-and-rejected";
1759+
const gated_rc = coord_send_gated(&gem_tok, TOKEN_LEN, 0, msg.ptr, @intCast(msg.len), 3);
1760+
try std.testing.expect(gated_rc < -1000);
1761+
const rid: u32 = @intCast(-(gated_rc + 1000));
1762+
const reason = "confabulated-path";
1763+
try std.testing.expectEqual(
1764+
@as(c_int, 0),
1765+
coord_reject(&sup_tok, TOKEN_LEN, @intCast(rid), reason.ptr, @intCast(reason.len)),
1766+
);
1767+
1768+
dur.close();
1769+
coord_reset();
1770+
try std.testing.expect(dur.openWithDir(dir));
1771+
dur.replay(replayDispatch);
1772+
defer dur.close();
1773+
1774+
// Supervisor inbox empty — rejected msg not delivered across restart.
1775+
var recv_buf: [256]u8 = undefined;
1776+
try std.testing.expectEqual(
1777+
@as(c_int, 0),
1778+
coord_receive(&sup_tok, TOKEN_LEN, &recv_buf, @intCast(recv_buf.len)),
1779+
);
1780+
1781+
// Quarantine empty too.
1782+
try std.testing.expectEqual(
1783+
@as(c_int, 0),
1784+
coord_review(&sup_tok, TOKEN_LEN, &recv_buf, @intCast(recv_buf.len)),
1785+
);
1786+
1787+
coord_reset();
1788+
}

0 commit comments

Comments
 (0)