@@ -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