Skip to content
zit Artifact [5ba24c8117]
Login

Artifact [5ba24c8117]

Artifact 5ba24c8117526907da1e2e495b4a9211174640f243b2c5c4a4d1555648536b1e:


//! Unit tests for the sync protocol (draft-fossil-sync-protocol-00).
//!
//! Separated from the implementation per the kind-separation convention
//! (mirrors the sibling `*_fuzz.zig` split). Every test here exercises only
//! `sync`'s public surface; tests reaching file-private decls stay inline in
//! `sync.zig`.

const std = @import("std");
const artifact = @import("artifact.zig");
const repostate = @import("repostate.zig");
const delta = @import("delta.zig");
const sync = @import("sync.zig");
const codec = @import("container.zig");

const Admission = sync.Admission;
const Capability = sync.Capability;
const CapabilityDecision = sync.CapabilityDecision;
const CapabilitySet = sync.CapabilitySet;
const CardParseError = sync.CardParseError;
const CiLockResult = sync.CiLockResult;
const Client = sync.Client;
const ClientErrorAction = sync.ClientErrorAction;
const ClientState = sync.ClientState;
const CompressLevel = codec.Level;
const ConfigError = sync.ConfigError;
const ConfigFrameError = sync.ConfigFrameError;
const ConfigKind = sync.ConfigKind;
const ConfigPushDecision = sync.ConfigPushDecision;
const ContentFrameError = sync.ContentFrameError;
const CookieStore = sync.CookieStore;
const Credential = sync.Credential;
const DateTime = sync.DateTime;
const DeltaBasisSend = sync.DeltaBasisSend;
const Disposition = sync.Disposition;
const Eligibility = sync.Eligibility;
const ErrorReset = sync.ErrorReset;
const ErrorSite = sync.ErrorSite;
const FatalCause = sync.FatalCause;
const GateDecision = sync.GateDecision;
const GimmeResponse = sync.GimmeResponse;
const IgotDisposition = sync.IgotDisposition;
const IntegritySchedule = sync.IntegritySchedule;
const LineError = sync.LineError;
const Identity = sync.Identity;
const LoginCard = sync.LoginCard;
const LoginResult = sync.LoginResult;
const LoginTransport = sync.LoginTransport;
const MediaType = sync.MediaType;
const MessageAction = sync.MessageAction;
const MethodDisposition = sync.MethodDisposition;
const NextRequest = sync.NextRequest;
const Operation = sync.Operation;
const Operator = sync.Operator;
const Peer = sync.Peer;
const PendingDeltaQueue = sync.PendingDeltaQueue;
const PragmaDirection = sync.PragmaDirection;
const PragmaError = sync.PragmaError;
const PragmaName = sync.PragmaName;
const PragmaSchedule = sync.PragmaSchedule;
const PrivateIgotAction = sync.PrivateIgotAction;
const PrivateReceipt = sync.PrivateReceipt;
const PushPull = sync.PushPull;
const PushPullCard = sync.PushPullCard;
const QuoteError = sync.QuoteError;
const ReplyBodyStep = sync.ReplyBodyStep;
const ReplyFraming = sync.ReplyFraming;
const ReqconfigDisposition = sync.ReqconfigDisposition;
const RequestCard = sync.RequestCard;
const RequestOrder = sync.RequestOrder;
const RoundObservation = sync.RoundObservation;
const RoundTables = sync.RoundTables;
const SendPrivateDecision = sync.SendPrivateDecision;
const Server = sync.Server;
const ServerUvRequest = sync.ServerUvRequest;
const ShunSet = sync.ShunSet;
const Side = sync.Side;
const SignalKind = sync.SignalKind;
const SyncProjectCode = sync.SyncProjectCode;
const TimestampComment = sync.TimestampComment;
const Tokens = sync.Tokens;
const Transport = sync.Transport;
const UvGimmeReply = sync.UvGimmeReply;
const UvLanding = sync.UvLanding;
const UvLocalAction = sync.UvLocalAction;
const UvResponse = sync.UvResponse;
const UvRow = sync.UvRow;
const UvSendTable = sync.UvSendTable;
const UvSession = sync.UvSession;
const UvStatus = sync.UvStatus;
const UvUpload = sync.UvUpload;
const UvfileCard = sync.UvfileCard;
const VersionInfo = sync.VersionInfo;
const admitInbound = sync.admitInbound;
const capabilityFromLetter = sync.capabilityFromLetter;
const capabilitySetFromLetters = sync.capabilitySetFromLetters;
const causeIsFatal = sync.causeIsFatal;
const checkRedirectDowngrade = sync.checkRedirectDowngrade;
const checkRequestOrder = sync.checkRequestOrder;
const classifyCard = sync.classifyCard;
const classifyPragma = sync.classifyPragma;
const clientMaySendUvHash = sync.clientMaySendUvHash;
const clientOnErrorCard = sync.clientOnErrorCard;
const clientOnMessageCard = sync.clientOnMessageCard;
const clientOnUvPullOnly = sync.clientOnUvPullOnly;
const clientOnUvPushOk = sync.clientOnUvPushOk;
const clientSendsCiUnlock = sync.clientSendsCiUnlock;
const clientSendsContent = sync.clientSendsContent;
const clientSendsSendPrivate = sync.clientSendsSendPrivate;
const cloneSeqnoIsFatal = sync.cloneSeqnoIsFatal;
const cloneServerMustAdvance = sync.cloneServerMustAdvance;
const clone_version = sync.clone_version;
const commitMayProceedAfterCiLockFail = sync.commitMayProceedAfterCiLockFail;
const computeNonce = sync.computeNonce;
const computeSignature = sync.computeSignature;
const configKind = sync.configKind;
const configNameHasSlashPrefix = sync.configNameHasSlashPrefix;
const config_push_unauthorized_text = sync.config_push_unauthorized_text;
const config_scalar_marker = sync.config_scalar_marker;
const SubmanifestConfig = sync.SubmanifestConfig;
const parseSubmanifestConfigPayload = sync.parseSubmanifestConfigPayload;
const writeSubmanifestConfigPayload = sync.writeSubmanifestConfigPayload;
const submanifest_config_mtime = sync.submanifest_config_mtime;
const submanifest_config_payload_max = sync.submanifest_config_payload_max;
const submanifest_setting_auto_retune = sync.submanifest_setting_auto_retune;
const submanifest_setting_enable = sync.submanifest_setting_enable;
const submanifest_setting_k = sync.submanifest_setting_k;
const consumeConfigPayload = sync.consumeConfigPayload;
const consumeContentPayload = sync.consumeContentPayload;
const consumeUvfilePayload = sync.consumeUvfilePayload;
const continueExchange = sync.continueExchange;
const cookie_gate_min_date = sync.cookie_gate_min_date;
const cookie_gate_min_time = sync.cookie_gate_min_time;
const cookie_gate_min_version = sync.cookie_gate_min_version;
const deltaBasisSend = sync.deltaBasisSend;
const deriveSharedSecret = sync.deriveSharedSecret;
const duplicateLogin = sync.duplicateLogin;
const encodeVersion = sync.encodeVersion;
const establishedCapabilities = sync.establishedCapabilities;
const fossilQuote = sync.fossilQuote;
const fossilUnquote = sync.fossilUnquote;
const gimmeEligible = sync.gimmeEligible;
const gimmeResponse = sync.gimmeResponse;
const igotDisposition = sync.igotDisposition;
const initialState = sync.initialState;
const integritySchedule = sync.integritySchedule;
const isCommentLine = sync.isCommentLine;
const isFatal = sync.isFatal;
const isFossilWhitespace = sync.isFossilWhitespace;
const loginTransport = sync.loginTransport;
const matchesPullOnly = sync.matchesPullOnly;
const max_line_len = sync.max_line_len;
const max_redirects = sync.max_redirects;
const max_tokens = sync.max_tokens;
const media_props = sync.media_props;
const needsSshTransportHeader = sync.needsSshTransportHeader;
const nextRequestKind = sync.nextRequestKind;
const parseCfileCard = sync.parseCfileCard;
const parseCiLock = sync.parseCiLock;
const parseCiLockFail = sync.parseCiLockFail;
const parseCiUnlock = sync.parseCiUnlock;
const parseClientUrl = sync.parseClientUrl;
const parseCloneSeqno = sync.parseCloneSeqno;
const parseConfigCard = sync.parseConfigCard;
const parseCookieLogin = sync.parseCookieLogin;
const parseFileCard = sync.parseFileCard;
const parseGimmeCard = sync.parseGimmeCard;
const parseIgotCard = sync.parseIgotCard;
const parseLoginCard = sync.parseLoginCard;
const parseMediaType = sync.parseMediaType;
const parseOperator = sync.parseOperator;
const parsePragmaName = sync.parsePragmaName;
const parsePushPull = sync.parsePushPull;
const parseReqconfigName = sync.parseReqconfigName;
const parseScalarConfig = sync.parseScalarConfig;
const parseSignalText = sync.parseSignalText;
const parseTimestampComment = sync.parseTimestampComment;
const parseUvHash = sync.parseUvHash;
const parseUvfileCard = sync.parseUvfileCard;
const parseUvgimmeCard = sync.parseUvgimmeCard;
const parseUvigotCard = sync.parseUvigotCard;
const parseVersionPragma = sync.parseVersionPragma;
const peerGatedForCookie = sync.peerGatedForCookie;
const peer_version_error_text = sync.peer_version_error_text;
const pragmaDirection = sync.pragmaDirection;
const pragmaIsArgless = sync.pragmaIsArgless;
const pragmaSchedule = sync.pragmaSchedule;
const privateIgotAction = sync.privateIgotAction;
const privateReceipt = sync.privateReceipt;
const private_igot_min_peer_date = sync.private_igot_min_peer_date;
const readLine = sync.readLine;
const receivePrivateIgot = sync.receivePrivateIgot;
const recordPeerVersion = sync.recordPeerVersion;
const redirectAllowed = sync.redirectAllowed;
const redirectAllowedForTransport = sync.redirectAllowedForTransport;
const replyBodyStep = sync.replyBodyStep;
const replyCapCheck = sync.replyCapCheck;
const replyFraming = sync.replyFraming;
const reply_body_cap = sync.reply_body_cap;
const requiredCapability = sync.requiredCapability;
const send_private_unauthorized_text = sync.send_private_unauthorized_text;
const sendableAfterCloneRound0 = sync.sendableAfterCloneRound0;
const serverCapabilityDecision = sync.serverCapabilityDecision;
const serverCiLockResult = sync.serverCiLockResult;
const serverConfigPushDecision = sync.serverConfigPushDecision;
const serverEmitsServerVersion = sync.serverEmitsServerVersion;
const serverErrorReset = sync.serverErrorReset;
const serverMethodDisposition = sync.serverMethodDisposition;
const serverReqconfigDisposition = sync.serverReqconfigDisposition;
const serverSendPrivateDecision = sync.serverSendPrivateDecision;
const serverSyncProjectCodeDecision = sync.serverSyncProjectCodeDecision;
const serverUvCatalog = sync.serverUvCatalog;
const serverUvResponse = sync.serverUvResponse;
const serverVerifyLogin = sync.serverVerifyLogin;
const sha3PeerVersionGate = sync.sha3PeerVersionGate;
const sha3_min_peer_version = sync.sha3_min_peer_version;
const ssh_transport_header = sync.ssh_transport_header;
const stripRedirectXfer = sync.stripRedirectXfer;
const syncRequestTarget = sync.syncRequestTarget;
const tokenizeCard = sync.tokenizeCard;
const transition = sync.transition;
const uvCatalogHash = sync.uvCatalogHash;
const uvDecide = sync.uvDecide;
const uvGimmeReply = sync.uvGimmeReply;
const uvLandDecision = sync.uvLandDecision;
const uvLandingClearsCatalog = sync.uvLandingClearsCatalog;
const uvStatus = sync.uvStatus;
const uvfilePayloadFollows = sync.uvfilePayloadFollows;
const uvfile_flag_omitted = sync.uvfile_flag_omitted;
const verifyUvContent = sync.verifyUvContent;
const writeBadCommand = sync.writeBadCommand;
const writeCiLock = sync.writeCiLock;
const writeCiLockFail = sync.writeCiLockFail;
const writeCiUnlock = sync.writeCiUnlock;
const writeClientUrl = sync.writeClientUrl;
const writeClientVersion = sync.writeClientVersion;
const writeCloneCard = sync.writeCloneCard;
const writeConfigCard = sync.writeConfigCard;
const writeCookieCard = sync.writeCookieCard;
const writeCookieLogin = sync.writeCookieLogin;
const writeLogin = sync.writeLogin;
const writeFlagPragma = sync.writeFlagPragma;
const writeMalformedCard = sync.writeMalformedCard;
const writePushPullCard = sync.writePushPullCard;
const writeReqconfig = sync.writeReqconfig;
const writeScalarConfig = sync.writeScalarConfig;
const writeServerVersion = sync.writeServerVersion;
const writeSignalCard = sync.writeSignalCard;
const zlibContainerDecode = codec.decode;
const zlibContainerEncode = codec.encode;

test "client starts in init" {
    const c: Client = .{};
    try std.testing.expectEqual(ClientState.init, c.state);
}

test "sync §5.1: message body lines are LF-separated" {
    const body = "a\nb\n";
    const first = try readLine(body, 0);
    try std.testing.expectEqualStrings("a", first.bytes);
    try std.testing.expectEqual(@as(usize, 2), first.next);
    const second = try readLine(body, first.next);
    try std.testing.expectEqualStrings("b", second.bytes);
    try std.testing.expectEqual(@as(usize, 4), second.next);
}

test "sync §5.1: each line is a card or a comment" {
    try std.testing.expect(isCommentLine("#x"));
    try std.testing.expect(!isCommentLine("push a b"));
}

test "sync §5.1: payload framed by byte count, not by scanning for LF" {
    // "file h 5\n" is the card line; the declared payload is 5 bytes,
    // "AB\nCD", which itself contains a LF that must not be scanned for.
    const body = "file h 5\nAB\nCD";
    const line = try readLine(body, 0);
    try std.testing.expectEqualStrings("file h 5", line.bytes);
    const payload = body[line.next .. line.next + 5];
    try std.testing.expectEqualStrings("AB\nCD", payload);
}

test "sync §5.2: a card is operator plus zero or more argument tokens" {
    try std.testing.expectEqual(@as(usize, 1), tokenizeCard("private").len);
    try std.testing.expectEqual(@as(usize, 3), tokenizeCard("push a b").len);
}

test "sync §5.2: canonical wire form is single-space, no surrounding whitespace" {
    const t = tokenizeCard("push a b");
    try std.testing.expectEqual(@as(usize, 3), t.len);
    try std.testing.expectEqualStrings("push", t.items[0]);
    try std.testing.expectEqualStrings("a", t.items[1]);
    try std.testing.expectEqualStrings("b", t.items[2]);
}

test "sync §5.2: operator token is lowercase letters plus underscore for clone_seqno" {
    try std.testing.expectEqual(Operator.clone_seqno, parseOperator("clone_seqno").?);
    try std.testing.expectEqual(Operator.push, parseOperator("push").?);
    try std.testing.expect(parseOperator("PUSH") == null);
    try std.testing.expect(parseOperator("push_") == null);
    try std.testing.expect(parseOperator("cl0ne") == null);
}

test "sync §5.2: card tokenizes on Fossil-whitespace runs, LF is not a token byte" {
    const t = tokenizeCard("push\ta\x0b\x0c\rb");
    try std.testing.expectEqual(@as(usize, 3), t.len);
    try std.testing.expectEqualStrings("push", t.items[0]);
    try std.testing.expectEqualStrings("a", t.items[1]);
    try std.testing.expectEqualStrings("b", t.items[2]);
}

test "sync §5.2: receiver accepts the canonical single-space form" {
    const t = tokenizeCard("igot h 1");
    try std.testing.expectEqual(@as(usize, 3), t.len);
    try std.testing.expectEqualStrings("igot", t.items[0]);
    try std.testing.expectEqualStrings("h", t.items[1]);
    try std.testing.expectEqualStrings("1", t.items[2]);
}

test "sync §5.2: receiver accepts Fossil-whitespace runs (compat)" {
    const t = tokenizeCard("push   a\t\tb");
    try std.testing.expectEqual(@as(usize, 3), t.len);
    try std.testing.expectEqualStrings("push", t.items[0]);
    try std.testing.expectEqualStrings("a", t.items[1]);
    try std.testing.expectEqualStrings("b", t.items[2]);
}

test "sync: zit bounds card/comment lines at 65535 bytes including LF (local limit; §5.2 L962 imposes none)" {
    // A ZIT limit, not a protocol one: §5.2 L962 says "This protocol imposes
    // no separate maximum line length", and §11.3 L2199 repeats it. An
    // over-length line (no LF within our window) is MissingTerminator because
    // `readLine` stops scanning there -- a conforming peer may legally send a
    // longer line, and we refuse it as a resource limit of our own.
    var ok_buf: [max_line_len]u8 = undefined;
    @memset(ok_buf[0 .. max_line_len - 1], 'x');
    ok_buf[max_line_len - 1] = '\n';
    const ok_line = try readLine(&ok_buf, 0);
    try std.testing.expectEqual(@as(usize, max_line_len - 1), ok_line.bytes.len);

    var over_buf: [max_line_len]u8 = undefined;
    @memset(&over_buf, 'x');
    try std.testing.expectError(LineError.MissingTerminator, readLine(&over_buf, 0));
}

test "sync §5.2: the line is parsed through LF, excluding the byte-counted payload" {
    // §5.2 L961: "Card and comment lines are parsed through LF before payload
    // framing." Our window is measured the same way -- through the terminating
    // LF, excluding any following byte-counted payload. A short card line followed
    // by a payload larger than the whole 65535 bound still parses — readLine
    // stops at the early LF and never scans the payload.
    const hash = "a" ** 40;
    const card_line = "file " ++ hash ++ " 100000\n"; // 53 bytes, LF at index 52
    const payload_len: usize = max_line_len + 64;
    var body: [card_line.len + payload_len]u8 = undefined;
    @memcpy(body[0..card_line.len], card_line);
    @memset(body[card_line.len..], 'x'); // non-LF byte-counted payload

    const line = try readLine(&body, 0);
    try std.testing.expectEqual(@as(usize, card_line.len - 1), line.bytes.len); // 52, excludes LF
    try std.testing.expectEqual(@as(usize, card_line.len), line.next); // 53, just past LF
    // The trailing payload exceeds the bound, yet readLine succeeded: it found
    // the LF within the window without scanning into the payload.
    try std.testing.expect(body.len - line.next > max_line_len);
}

test "sync: an over-length line aborts before card dispatch (zit limit; replyBodyStep .truncated, never .card_ready)" {
    // Consequence of our own window, not of a draft clause: a line we refuse
    // is never interpreted as a card, nor is any following payload consumed.
    // replyBodyStep runs readLine before it can return .card_ready, so an
    // unterminated over-length line yields .truncated and never dispatches.
    const over: usize = max_line_len + 50;
    var body: [over + 1]u8 = undefined;
    @memset(body[0..over], 'x'); // no LF within the window
    body[over] = '\n'; // LF sits beyond the bound

    try std.testing.expectEqual(ReplyBodyStep.truncated, replyBodyStep(&body, 0));
    try std.testing.expect(replyBodyStep(&body, 0) != .card_ready);
    try std.testing.expectError(LineError.MissingTerminator, readLine(&body, 0));
}

test "sync §11.3 L2200-2201: declared byte counts, not the zit line window, govern payloads" {
    // §11.3 L2200-2201: "entity and payload limits remain governed by their
    // declared counts and storage policy." So our line window must not cap the
    // SIZE/CSIZE payload of a file/cfile/uvfile/config card. 70000 is the
    // smallest round value above it.
    const hash = "a" ** 40;
    const card_line = "file " ++ hash ++ " 70000\n"; // 52 bytes
    const declared: usize = 70000;
    const sentinel = "pong\n";
    var body: [card_line.len + declared + sentinel.len]u8 = undefined;
    @memcpy(body[0..card_line.len], card_line);
    @memset(body[card_line.len..][0..declared], 'y'); // non-LF declared payload
    @memcpy(body[card_line.len + declared ..], sentinel);

    const first = try readLine(&body, 0);
    try std.testing.expectEqualStrings("file " ++ hash ++ " 70000", first.bytes);
    // Caller advances past the declared byte count, not by re-scanning for LF.
    const advance = first.next + declared;
    const second = try readLine(&body, advance);
    try std.testing.expectEqualStrings("pong", second.bytes);
}

test "sync §5.2: unknown card operator is a fatal unknown-operator disposition" {
    const c = classifyCard("frobnicate x");
    try std.testing.expectEqual(Disposition.unknown_operator, c.disposition);
    try std.testing.expect(c.operator == null);
    try std.testing.expect(isFatal(c.disposition, .client));
    try std.testing.expect(isFatal(c.disposition, .server));
}

test "sync §5.2: defined operator with in-window arg count is dispatched not unknown" {
    const c = classifyCard("private extra");
    try std.testing.expectEqual(Operator.private, c.operator.?);
    try std.testing.expectEqual(Disposition.dispatch, c.disposition);
}

test "sync §5.2: card with surplus tokens beyond the six-slot window is malformed" {
    const t = tokenizeCard("push a b c d e f");
    try std.testing.expectEqual(@as(usize, max_tokens), t.len);
    try std.testing.expect(t.surplus);
    const c = classifyCard("push a b c d e f");
    try std.testing.expect(c.disposition != .dispatch);
}

test "sync §5.2: receiver treats surplus tokens as malformed" {
    const simple = classifyCard("push a b c d e f");
    try std.testing.expectEqual(Disposition.bad_command, simple.disposition);
    const content = classifyCard("file a b c d e f");
    try std.testing.expectEqual(Disposition.malformed_content, content.disposition);
}

test "sync §5.2: content card arity failure yields malformed CARD line and aborts" {
    const c = classifyCard("file a b c d e f");
    try std.testing.expectEqual(Disposition.malformed_content, c.disposition);
    var out: [64]u8 = undefined;
    const msg = writeMalformedCard(.file, &out);
    try std.testing.expectEqualStrings("malformed file line", msg);
    try std.testing.expect(isFatal(c.disposition, .client));
    try std.testing.expect(isFatal(c.disposition, .server));
}

test "sync §5.2: simple card arity failure yields bad command, fatal client non-fatal server" {
    const line = "push a b c d e f";
    const c = classifyCard(line);
    try std.testing.expectEqual(Disposition.bad_command, c.disposition);
    var out: [64]u8 = undefined;
    const msg = writeBadCommand(line, &out);
    try std.testing.expectEqualStrings("bad command: push a b c d e f", msg);
    try std.testing.expect(isFatal(c.disposition, .client));
    try std.testing.expect(!isFatal(c.disposition, .server));
}

test "sync §5.2: unrecognized card framing is exactly 'bad command:' plus offending line" {
    var out: [64]u8 = undefined;
    const msg = writeBadCommand("frobnicate x y", &out);
    try std.testing.expectEqualStrings("bad command: frobnicate x y", msg);
    try std.testing.expect(std.mem.indexOf(u8, msg, "Unknown message") == null);
}

test "sync §5.2.1: defined operator catalog resolves client/server operators" {
    try std.testing.expectEqual(Operator.cookie, parseOperator("cookie").?);
    try std.testing.expectEqual(Operator.login, parseOperator("login").?);
    try std.testing.expectEqual(Operator.push, parseOperator("push").?);
    try std.testing.expectEqual(Operator.pull, parseOperator("pull").?);
}

test "sync §5.2.1: remaining operators resolve and catalog size is 19" {
    const names = [_][]const u8{
        "clone",  "file",        "cfile",  "uvfile",  "private",
        "igot",   "uvigot",      "gimme",  "uvgimme", "reqconfig",
        "config", "clone_seqno", "pragma", "error",   "message",
    };
    for (names) |name| {
        try std.testing.expect(parseOperator(name) != null);
    }
    try std.testing.expectEqual(@as(usize, 19), @typeInfo(Operator).@"enum".fields.len);
}

test "sync §5.3: a line whose first byte is # is a comment line" {
    try std.testing.expect(isCommentLine("#x"));
    try std.testing.expect(!isCommentLine(""));
    try std.testing.expect(!isCommentLine("push"));
}

test "sync §5.3: comment lines are not dispatched as cards" {
    // A comment line may textually contain an operator name, but its
    // leading # is what a card dispatcher sees as the first token, and
    // "#" is not in the operator catalog -- so even a comment line that
    // reads like a push card can never resolve to Operator.push
    // (§5-r22).
    try std.testing.expect(isCommentLine("# push a b"));
    const c = classifyCard("# push a b");
    try std.testing.expectEqual(Disposition.unknown_operator, c.disposition);
    try std.testing.expect(c.operator == null);
}

test "sync §5.3: timestamp subform exposes DATETIME and informational error count" {
    const tc = parseTimestampComment("# timestamp 2026-07-02T18:31:13 errors 0").?;
    try std.testing.expectEqualStrings("2026-07-02T18:31:13", tc.datetime);
    try std.testing.expectEqual(@as(u64, 0), tc.errors);
}

test "sync §5.3: other comment lines are not semantically interpreted" {
    try std.testing.expect(parseTimestampComment("# F2A895EF75B47B7157664A7FEAC978EFFFDB01C4") == null);
    try std.testing.expect(parseTimestampComment("# sending-clusters") == null);
    try std.testing.expect(parseTimestampComment("# timestamp X errors zz") == null);
}

test "sync §10 r1: a pragma card is 'pragma NAME [ARGUMENTS]'" {
    const c1 = classifyPragma(tokenizeCard("pragma send-catalog"));
    try std.testing.expectEqual(PragmaName.@"send-catalog", c1.name.?);
    const c2 = classifyPragma(tokenizeCard("pragma ci-lock H ID"));
    try std.testing.expectEqual(PragmaName.@"ci-lock", c2.name.?);
}

test "sync §10 r2: an unrecognised pragma name is silently ignored" {
    try std.testing.expect(parsePragmaName("future-x") == null);
    const c = classifyPragma(tokenizeCard("pragma future-x arg"));
    try std.testing.expect(c.name == null);
}

test "sync §10 r3: unknown pragma name is ignored but unknown card operator is fatal" {
    const c = classifyPragma(tokenizeCard("pragma future-x"));
    try std.testing.expect(c.name == null);

    const bad = classifyCard("frobnicate x");
    try std.testing.expectEqual(Disposition.unknown_operator, bad.disposition);
    try std.testing.expect(isFatal(bad.disposition, .client));
    try std.testing.expect(isFatal(bad.disposition, .server));
}

test "sync §3.5: a pragma is an ordinary body card parsed in round order [sync-3-5-r1]" {
    // L693-694 — pragmas are ordinary body cards parsed in round order
    // along with all other body cards; the pragma dispatches at its
    // positional offset, not pre-filtered out of band.
    const body = "igot H1\npragma client-version 20000\ngimme H2\n";
    const want = [_]Operator{ .igot, .pragma, .gimme };
    var offset: usize = 0;
    for (want) |op| {
        try std.testing.expectEqual(ReplyBodyStep.card_ready, replyBodyStep(body, offset));
        const line = try readLine(body, offset);
        const cc = classifyCard(line.bytes);
        try std.testing.expectEqual(op, cc.operator.?);
        try std.testing.expectEqual(Disposition.dispatch, cc.disposition);
        offset = line.next;
    }
    try std.testing.expectEqual(ReplyBodyStep.round_complete, replyBodyStep(body, offset));
}

test "sync §3.5: an unknown pragma name is not a protocol error [sync-3-5-r2]" {
    // L694-696 — unknown pragma names are silently ignored and MUST NOT be
    // treated as protocol errors. The pragma card still dispatches (the
    // operator resolves); only the NAME is unrecognized.
    const cc = classifyCard("pragma future-x arg");
    try std.testing.expectEqual(Operator.pragma, cc.operator.?);
    try std.testing.expectEqual(Disposition.dispatch, cc.disposition);
    try std.testing.expect(!isFatal(cc.disposition, .client));
    try std.testing.expect(!isFatal(cc.disposition, .server));
    try std.testing.expect(classifyPragma(cc.tokens).name == null);
}

test "sync §3.5: an unknown non-pragma operator is a fatal protocol error [sync-3-5-r3]" {
    // L696 — an unknown non-pragma operator remains a fatal protocol error
    // (§5-r12: unknown operator is fatal on either side).
    const cc = classifyCard("frobnicate x y");
    try std.testing.expect(cc.operator == null);
    try std.testing.expectEqual(Disposition.unknown_operator, cc.disposition);
    try std.testing.expect(isFatal(cc.disposition, .client));
    try std.testing.expect(isFatal(cc.disposition, .server));
}

test "sync §10 r4: fourteen pragma names, nine c2s and five s2c" {
    const Case = struct { name: []const u8, dir: PragmaDirection };
    const cases = [_]Case{
        .{ .name = "send-private", .dir = .client_to_server },
        .{ .name = "send-catalog", .dir = .client_to_server },
        .{ .name = "client-version", .dir = .client_to_server },
        .{ .name = "uv-hash", .dir = .client_to_server },
        .{ .name = "ci-lock", .dir = .client_to_server },
        .{ .name = "ci-unlock", .dir = .client_to_server },
        .{ .name = "client-url", .dir = .client_to_server },
        .{ .name = "req-links", .dir = .client_to_server },
        .{ .name = "req-clusters", .dir = .client_to_server },
        .{ .name = "server-version", .dir = .server_to_client },
        .{ .name = "uv-push-ok", .dir = .server_to_client },
        .{ .name = "uv-pull-only", .dir = .server_to_client },
        .{ .name = "ci-lock-fail", .dir = .server_to_client },
        .{ .name = "avoid-delta-manifests", .dir = .server_to_client },
    };
    try std.testing.expectEqual(@as(usize, 14), cases.len);
    var c2s: usize = 0;
    var s2c: usize = 0;
    for (cases) |c| {
        const name = parsePragmaName(c.name).?;
        try std.testing.expectEqual(c.dir, pragmaDirection(name));
        switch (c.dir) {
            .client_to_server => c2s += 1,
            .server_to_client => s2c += 1,
        }
    }
    try std.testing.expectEqual(@as(usize, 9), c2s);
    try std.testing.expectEqual(@as(usize, 5), s2c);
}

test "sync §10 r5: pragma resolution is not version-gated" {
    const names = [_][]const u8{
        "send-private", "send-catalog", "client-version", "uv-hash",               "ci-lock",
        "ci-unlock",    "client-url",   "req-links",      "req-clusters",          "server-version",
        "uv-push-ok",   "uv-pull-only", "ci-lock-fail",   "avoid-delta-manifests",
    };
    for (names) |name| {
        try std.testing.expect(parsePragmaName(name) != null);
    }
    try std.testing.expect(parsePragmaName("unknown-thing") == null);
}

test "sync §10.1 r6: client-version is 'pragma client-version VERSION DATE TIME'" {
    var buf: [64]u8 = undefined;
    const v: VersionInfo = .{ .version = 20000, .datetime = .{ .date = 20260709, .time = 120000 } };
    try std.testing.expectEqualStrings("pragma client-version 20000 20260709 120000", try writeClientVersion(v, &buf));
}

test "sync §10.1 r7: VERSION = MAJOR*10000+MINOR*100+PATCH; 2.0 -> 20000" {
    try std.testing.expectEqual(@as(u32, 20000), encodeVersion(2, 0, 0));
    try std.testing.expectEqual(@as(u32, 19999), encodeVersion(1, 99, 99));
    try std.testing.expect(encodeVersion(1, 99, 99) < 20000);
}

test "sync §10.1 r8: DATE and TIME are optional; VERSION-only is allowed" {
    var buf: [64]u8 = undefined;
    const v: VersionInfo = .{ .version = 20000 };
    try std.testing.expectEqualStrings("pragma client-version 20000", try writeClientVersion(v, &buf));

    const parsed = try parseVersionPragma(tokenizeCard("pragma client-version 20000"));
    try std.testing.expect(parsed.datetime == null);
}

test "sync §10.1 r9: received client-version updates server peer version and timestamp" {
    var p = Peer.serverInit();
    recordPeerVersion(&p, .{ .version = 20000, .datetime = .{ .date = 20260709, .time = 120000 } });
    try std.testing.expectEqual(@as(u32, 20000), p.version);
    try std.testing.expectEqual(@as(u32, 20260709), p.datetime.?.date);
    try std.testing.expectEqual(@as(u32, 120000), p.datetime.?.time);
}

test "sync §10.1 r10: before client-version, server peer VERSION is 0" {
    const p = Peer.serverInit();
    try std.testing.expectEqual(@as(u32, 0), p.version);
    try std.testing.expect(p.datetime == null);
}

test "sync §10.1 r11: server emits server-version iff pragma had DATE and TIME (>=5 tokens)" {
    const v = try parseVersionPragma(tokenizeCard("pragma client-version 20000 20260709 120000"));
    try std.testing.expect(serverEmitsServerVersion(v));
}

test "sync §10.1 r12: VERSION-only client-version suppresses server-version" {
    const v = try parseVersionPragma(tokenizeCard("pragma client-version 20000"));
    try std.testing.expect(!serverEmitsServerVersion(v));
}

test "sync §10.1 r13: received server-version updates client peer version and timestamp" {
    var p = Peer.clientInit(19999);
    recordPeerVersion(&p, .{ .version = 20000, .datetime = .{ .date = 20260709, .time = 120000 } });
    try std.testing.expectEqual(@as(u32, 20000), p.version);
    try std.testing.expectEqual(@as(u32, 20260709), p.datetime.?.date);
}

test "sync §10.1 r14: before server-version, client peer VERSION is its own release integer" {
    const p = Peer.clientInit(19999);
    try std.testing.expectEqual(@as(u32, 19999), p.version);
    try std.testing.expect(p.datetime == null);
}

test "sync §10.1: server-version DATE/TIME zero-padded to YYYYMMDD/HHMMSS form" {
    var buf: [64]u8 = undefined;
    const v1: VersionInfo = .{ .version = 20000, .datetime = .{ .date = 20260709, .time = 90503 } };
    try std.testing.expectEqualStrings("pragma server-version 20000 20260709 090503", try writeServerVersion(v1, &buf));

    var buf2: [64]u8 = undefined;
    const v2: VersionInfo = .{ .version = 20000, .datetime = .{ .date = 20260709, .time = 1 } };
    try std.testing.expectEqualStrings("pragma server-version 20000 20260709 000001", try writeServerVersion(v2, &buf2));
}

test "sync §10.1: client-version numeric fields must parse (rejection)" {
    const cases = [_][]const u8{
        "pragma client-version",
        "pragma client-version x",
        "pragma client-version 20000 x 120000",
        "pragma client-version 20000 20260709 x",
    };
    for (cases) |line| {
        try std.testing.expectError(PragmaError.Malformed, parseVersionPragma(tokenizeCard(line)));
    }
}

test "sync §10.2 r15: uv-hash carries the client's catalog hash" {
    const t = tokenizeCard("pragma uv-hash ABC123");
    try std.testing.expectEqualStrings("ABC123", parseUvHash(t).?);
    const c = classifyPragma(t);
    try std.testing.expectEqual(PragmaName.@"uv-hash", c.name.?);
}

test "sync §10.2 r16: uv-hash sent at most once per session and never on clone round 0" {
    const Case = struct { hash_sent: bool, op: Operation, round: u64, want: bool };
    const cases = [_]Case{
        .{ .hash_sent = false, .op = .sync, .round = 3, .want = true },
        .{ .hash_sent = true, .op = .sync, .round = 3, .want = false },
        .{ .hash_sent = false, .op = .clone, .round = 0, .want = false },
        .{ .hash_sent = false, .op = .clone, .round = 1, .want = true },
    };
    for (cases) |c| {
        try std.testing.expectEqual(c.want, clientMaySendUvHash(c.hash_sent, c.op, c.round));
    }
}

test "sync §10.2 r17: matching catalog hash suppresses the uv response" {
    try std.testing.expectEqual(UvResponse.none, serverUvResponse(true, true, true));
}

test "sync §10.2 r18: differing hash with read yields push-ok or pull-only" {
    const Case = struct { match: bool, read: bool, upload: bool, want: UvResponse };
    const cases = [_]Case{
        .{ .match = false, .read = true, .upload = true, .want = .push_ok },
        .{ .match = false, .read = true, .upload = false, .want = .pull_only },
        .{ .match = false, .read = false, .upload = true, .want = .none },
    };
    for (cases) |c| {
        try std.testing.expectEqual(c.want, serverUvResponse(c.match, c.read, c.upload));
    }
}

test "sync §10.2 r19: uv-pull-only latches the client and push-ok cannot undo it" {
    var s: UvSession = .{};
    try std.testing.expectEqual(UvUpload.undetermined, s.upload);
    clientOnUvPushOk(&s);
    try std.testing.expectEqual(UvUpload.push_ok, s.upload);
    clientOnUvPullOnly(&s);
    try std.testing.expectEqual(UvUpload.pull_only, s.upload);
    clientOnUvPushOk(&s);
    try std.testing.expectEqual(UvUpload.pull_only, s.upload);
}

test "sync §9.3 r20: reqconfig NAME must begin with /" {
    try std.testing.expect(configNameHasSlashPrefix("/ui"));
    try std.testing.expect(!configNameHasSlashPrefix("ui"));
    var buf: [32]u8 = undefined;
    try std.testing.expectEqualStrings("reqconfig /ui", try writeReqconfig("/ui", &buf));
    const name = parseReqconfigName(tokenizeCard("reqconfig /ui")).?;
    try std.testing.expectEqualStrings("/ui", name);
}

test "sync §9.3 r21: server silently ignores reqconfig without Read, no error card" {
    try std.testing.expectEqual(ReqconfigDisposition.ignore, serverReqconfigDisposition(false));
    try std.testing.expectEqual(ReqconfigDisposition.reply, serverReqconfigDisposition(true));
}

test "sync §9.3/§8.4: server answers reqconfig /all with no error and no config cards" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    const server = sync.Server{
        .version = .{ .version = 22800, .datetime = .{ .date = 20260715, .time = 0 } },
        .self_servercode = "s",
        .project_code = "p",
    };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);
    // §8.4 — the client's deferred configuration request in the post-clone
    // cleanup round: an undated client-version (no server-version solicited),
    // then reqconfig /all. zit models no config group, so its group mask is
    // zero: the reply carries NO config cards and — the point of the fix —
    // NO `error bad command`, so a clone completes past the config phase.
    const reply = try server.respond(gpa, ms.store(), "pragma client-version 20000\nreqconfig /all\n", &out);
    try std.testing.expect(std.mem.indexOf(u8, reply, "error") == null);
    try std.testing.expect(std.mem.indexOf(u8, reply, "config") == null);
}

test "sync §9.3/§9.3 r20: a reqconfig NAME without a leading slash is error bad command" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    const server = sync.Server{ .version = .{ .version = 22800, .datetime = .{ .date = 20260715, .time = 0 } } };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);
    // §9.3 r20 L1882 — NAME MUST begin with '/'; a bare group name is a
    // §5.2 malformed card, reset-and-error like any other bad command.
    const reply = try server.respond(gpa, ms.store(), "reqconfig all\n", &out);
    try std.testing.expect(std.mem.startsWith(u8, reply, "error bad command:"));
}

test "sync §9.1: deriveProjectCode is a 40-hex artifact hash, distinct and deterministic" {
    const a = sync.deriveProjectCode(&.{"/tmp/repo-a/.zit"});
    const b = sync.deriveProjectCode(&.{"/tmp/repo-b/.zit"});
    // §12 ABNF `projectcode = artifact-hash` — 40 lowercase-hex bytes.
    try std.testing.expectEqual(@as(usize, 40), a.len);
    for (a) |c| try std.testing.expect((c >= '0' and c <= '9') or (c >= 'a' and c <= 'f'));
    // Distinct repositories (distinct entropy) get distinct codes; never the
    // all-zero placeholder.
    try std.testing.expect(!std.mem.eql(u8, &a, &b));
    try std.testing.expect(!std.mem.eql(u8, &a, "0" ** 40));
    // Deterministic: the same entropy replays the same code (no ambient RNG).
    const a2 = sync.deriveProjectCode(&.{"/tmp/repo-a/.zit"});
    try std.testing.expectEqualSlices(u8, &a, &a2);
}

test "sync §8.3/§9.1: a clone reply's push card teaches the client the peer project code" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .clone, .state = .clone_round0 };
    defer client.deinit(gpa);
    // §8.2 — the clone reply closes with `clone_seqno NEXT` then `push
    // SERVERCODE PROJECTCODE`; the client records the peer's project code from
    // it (§3.2 L405-408) for the CLI to persist as the shared identity (§9.1).
    const pcode = "aa87ea41b0bb05ba528f7a3bdba700d7e23d6318";
    _ = try client.consumeReply(gpa, ms.store(), "clone_seqno 0\npush 1111111111111111 " ++ pcode ++ "\n");
    try std.testing.expectEqualStrings(pcode, client.peerProjectCode());
}

test "sync §9.3 r22: clients defer reqconfig until after clone round 0" {
    const Case = struct { op: Operation, round: u64, want: bool };
    const cases = [_]Case{
        .{ .op = .clone, .round = 0, .want = false },
        .{ .op = .clone, .round = 1, .want = true },
        .{ .op = .sync, .round = 0, .want = true },
        .{ .op = .pull, .round = 3, .want = true },
    };
    for (cases) |c| {
        try std.testing.expectEqual(c.want, sendableAfterCloneRound0(c.op, c.round));
    }
}

test "sync §9.3 r23: config GROUPNAME SIZE card then SIZE content bytes" {
    const c = try parseConfigCard(tokenizeCard("config /config 32"));
    try std.testing.expectEqualStrings("/config", c.groupname);
    try std.testing.expectEqual(@as(u32, 32), c.size);

    var buf: [32]u8 = undefined;
    try std.testing.expectEqualStrings("config /config 32", try writeConfigCard("/config", 32, &buf));

    const payload = try consumeConfigPayload("abc\n", 0, 3);
    try std.testing.expectEqualStrings("abc", payload.bytes);
    try std.testing.expectEqual(@as(usize, 4), payload.next);
}

test "sync §9.3 r24: scalar settings travel as /config, an encoding marker not a group id" {
    try std.testing.expectEqual(ConfigKind.scalar, configKind("/config"));
    try std.testing.expect(std.mem.eql(u8, config_scalar_marker, "/config"));
}

test "sync §9.3 r25: scalar payload is MTIME SP QUOTED-NAME SP value SP QUOTED-VALUE" {
    var buf: [64]u8 = undefined;
    const written = try writeScalarConfig(1720483200, "autosync", "on", &buf);
    try std.testing.expectEqualStrings("1720483200 'autosync' value 'on'", written);

    const parsed = try parseScalarConfig(written);
    try std.testing.expectEqual(@as(u64, 1720483200), parsed.mtime);
    try std.testing.expectEqualStrings("'autosync'", parsed.quoted_name);
    try std.testing.expectEqualStrings("'on'", parsed.quoted_value);
}

test "sync §9.3 r26: Fossil quote encoding wraps in ' and doubles internal '" {
    var qbuf: [16]u8 = undefined;
    try std.testing.expectEqualStrings("'it''s'", fossilQuote("it's", &qbuf));
    var ubuf: [16]u8 = undefined;
    try std.testing.expectEqualStrings("it's", try fossilUnquote("'it''s'", &ubuf));

    var rbuf: [16]u8 = undefined;
    const requoted = fossilQuote("a'b'c", &rbuf);
    var rubuf: [16]u8 = undefined;
    try std.testing.expectEqualStrings("a'b'c", try fossilUnquote(requoted, &rubuf));

    var xbuf: [8]u8 = undefined;
    try std.testing.expectEqualStrings("'x'", fossilQuote("x", &xbuf));
}

test "sync §9.3 r27: /config card name alone cannot identify the requested group" {
    // Two conceptually distinct requests both surface under the same
    // fixed marker: the byte carries no group information.
    try std.testing.expectEqual(ConfigKind.scalar, configKind(config_scalar_marker));
    try std.testing.expect(std.mem.eql(u8, config_scalar_marker, "/config"));
}

test "sync §9.3 r28: table entries travel as /GROUPNAME identifying the table" {
    try std.testing.expectEqual(ConfigKind.table, configKind("/shun"));
    try std.testing.expectEqual(ConfigKind.table, configKind("/user"));
    try std.testing.expectEqual(ConfigKind.scalar, configKind("/config"));
}

test "sync §9.3 r29: a single trailing newline follows each config payload and is consumed" {
    const ok = try consumeConfigPayload("hi\nX", 0, 2);
    try std.testing.expectEqualStrings("hi", ok.bytes);
    try std.testing.expectEqual(@as(usize, 3), ok.next);

    try std.testing.expectError(ConfigFrameError.MissingNewline, consumeConfigPayload("hiZ", 0, 2));
}

test "sync §9.3 r30: Admin may push a scalar config NAME SIZE card, same /config form" {
    var payload_buf: [64]u8 = undefined;
    const payload = try writeScalarConfig(1720483200, "autosync", "on", &payload_buf);
    var card_buf: [32]u8 = undefined;
    const card = try writeConfigCard("/config", @intCast(payload.len), &card_buf);
    try std.testing.expectEqualStrings("config /config 32", card);
    try std.testing.expectEqual(ConfigKind.scalar, configKind("/config"));
}

test "sync §9.3 r31: server requires Admin to push; else error card, not applied (rejection)" {
    try std.testing.expectEqual(ConfigPushDecision.reject, serverConfigPushDecision(false));
    try std.testing.expectEqual(ConfigPushDecision.apply, serverConfigPushDecision(true));
    try std.testing.expectEqualStrings("not authorized to push configuration", config_push_unauthorized_text);
}

test "sync §9.3 r32: pushed config card with negative SIZE is fatal (rejection)" {
    try std.testing.expectError(ConfigError.NegativeSize, parseConfigCard(tokenizeCard("config /config -1")));
}

test "sync §9.3 r33: well-formed authorized push is applied; SIZE payload + newline consumed" {
    try std.testing.expectEqual(ConfigPushDecision.apply, serverConfigPushDecision(true));
    const payload = try consumeConfigPayload("on\nNEXT", 0, 2);
    try std.testing.expectEqualStrings("on", payload.bytes);
    try std.testing.expectEqual(@as(usize, 3), payload.next);
}

test "sync §9.3 r34: only the scalar /config form is pushed, not table groups" {
    try std.testing.expectEqual(ConfigKind.scalar, configKind("/config"));
    try std.testing.expectEqual(ConfigKind.table, configKind("/shun"));
}

test "sync §9.3: quote/parse robustness negatives" {
    var buf: [16]u8 = undefined;
    try std.testing.expectError(QuoteError.Malformed, fossilUnquote("abc", &buf));
    try std.testing.expectError(QuoteError.Malformed, fossilUnquote("'a'b'", &buf));
    try std.testing.expectError(error.Malformed, parseScalarConfig("123 'n' notvalue 'v'"));
    try std.testing.expectError(ConfigError.Malformed, parseConfigCard(tokenizeCard("config /config x")));
    try std.testing.expectError(ConfigError.Malformed, parseConfigCard(tokenizeCard("config /config 5000000000")));
}

// --- §9.3 scalar /config propagation of the three submanifest settings -----
//
// draft-fossil-submanifests-00 §6.3 requires PROJECT scope for the partition
// settings ("configuration propagating to clones"), and §8.1 says the same of
// `enable-submanifests`; §6.4's `submanifest-auto-retune` is the
// third. [FossilSync] §9.3's scalar channel is the transport, so these tests
// pin the wire form, the server's two dispositions, and the client's latch.

test "sync §9.3 submanifests §6.3/§8.1: the three settings round-trip through one scalar payload" {
    var buf: [submanifest_config_payload_max]u8 = undefined;
    const payload = try writeSubmanifestConfigPayload(true, 3, true, &buf);
    // The exact wire bytes: three LF-separated §9.3 r25 rows, each MTIME SP
    // QUOTED-NAME SP value SP QUOTED-VALUE, booleans as on/off and k decimal.
    // MTIME is the 0 sentinel — zit's config frame tracks no per-field write
    // time, so 0 says "untracked" rather than inventing a plausible clock.
    try std.testing.expectEqualStrings(
        "0 'enable-submanifests' value 'on'\n" ++
            "0 'submanifest-k' value '3'\n" ++
            "0 'submanifest-auto-retune' value 'on'",
        payload,
    );
    try std.testing.expectEqual(@as(u64, 0), submanifest_config_mtime);

    const got = parseSubmanifestConfigPayload(payload);
    try std.testing.expectEqual(@as(?bool, true), got.enable);
    try std.testing.expectEqual(@as(?u8, 3), got.k);
    try std.testing.expectEqual(@as(?bool, true), got.auto_retune);
    try std.testing.expect(got.any());

    // The off/unpinned triple round-trips too — a defaulted origin is a real
    // wire case, not an absence.
    var off_buf: [submanifest_config_payload_max]u8 = undefined;
    const off_payload = try writeSubmanifestConfigPayload(false, 0, false, &off_buf);
    const off = parseSubmanifestConfigPayload(off_payload);
    try std.testing.expectEqual(@as(?bool, false), off.enable);
    try std.testing.expectEqual(@as(?u8, 0), off.k);
    try std.testing.expectEqual(@as(?bool, false), off.auto_retune);
}

test "sync §9.3: an unknown scalar name or an unparseable value is skipped, never fatal" {
    // §3.5 tolerate-and-ignore at row granularity: a peer that carries settings
    // this implementation does not model, or a value shape it cannot read, must
    // leave the rows it DOES understand intact and raise nothing.
    const mixed =
        "0 'autosync' value 'on'\n" ++ // a real Fossil setting zit models not
        "0 'enable-submanifests' value 'on'\n" ++
        "0 'submanifest-k' value 'banana'\n" ++ // unparseable k
        "not-a-row\n" ++ // not even §9.3 r25 shaped
        "0 'submanifest-auto-retune' value 'off'";
    const got = parseSubmanifestConfigPayload(mixed);
    try std.testing.expectEqual(@as(?bool, true), got.enable);
    try std.testing.expectEqual(@as(?u8, null), got.k); // skipped, not defaulted
    try std.testing.expectEqual(@as(?bool, false), got.auto_retune);

    // An empty payload names nothing: the all-null triple, which the adoption
    // wrapper reads as "peer sent none; keep local".
    const none = parseSubmanifestConfigPayload("");
    try std.testing.expect(!none.any());
}

test "sync §9.3/§9.3 r21: a server with Read answers reqconfig /config with the three settings" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    const server = sync.Server{
        .version = .{ .version = 22800, .datetime = .{ .date = 20260715, .time = 0 } },
        .self_servercode = "s",
        .project_code = "p",
        .submanifests_enabled = true,
        .submanifest_k = 5,
        .submanifest_auto_retune = true,
    };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);
    const reply = try server.respond(gpa, ms.store(), "pragma client-version 20000\nreqconfig /config\n", &out);
    try std.testing.expect(std.mem.indexOf(u8, reply, "error") == null);

    // The card header frames its payload by byte count (§9.3 r23/r29), and the
    // payload carries this repository's three current values.
    var want_buf: [submanifest_config_payload_max]u8 = undefined;
    const want_payload = try writeSubmanifestConfigPayload(true, 5, true, &want_buf);
    var head_buf: [64]u8 = undefined;
    const head = try sync.writeConfigCard(config_scalar_marker, @intCast(want_payload.len), &head_buf);
    const at = std.mem.indexOf(u8, reply, head) orelse return error.TestUnexpectedResult;
    const payload_start = at + head.len + 1;
    const framed = try consumeConfigPayload(reply, payload_start, @intCast(want_payload.len));
    try std.testing.expectEqualStrings(want_payload, framed.bytes);
}

test "sync §9.3: a peer that never asks gets no config card (group mask zero, no error)" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    const server = sync.Server{
        .version = .{ .version = 22800, .datetime = .{ .date = 20260715, .time = 0 } },
        .submanifests_enabled = true,
        .submanifest_k = 5,
        .submanifest_auto_retune = true,
    };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);

    // An older peer's request: no reqconfig at all. §9.3's "no cards, no error"
    // extends to it trivially — the reply is what it was before this feature.
    const silent = try server.respond(gpa, ms.store(), "pragma client-version 20000\npull s p\n", &out);
    try std.testing.expect(std.mem.indexOf(u8, silent, "config") == null);
    try std.testing.expect(std.mem.indexOf(u8, silent, "error") == null);

    // And a peer that asks for a TABLE group still gets the zero-mask no-op.
    var out2: std.ArrayListUnmanaged(u8) = .empty;
    defer out2.deinit(gpa);
    const table = try server.respond(gpa, ms.store(), "pragma client-version 20000\nreqconfig /shun\n", &out2);
    try std.testing.expect(std.mem.indexOf(u8, table, "config /") == null);
    try std.testing.expect(std.mem.indexOf(u8, table, "error") == null);
}

test "sync §9.3 r31: a pushed config card without Admin is rejected verbatim and applies nothing" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    // No credential: zit's anonymous server can never grant Admin, so every
    // pushed config card is rejected — the safe direction §9.3 r31 requires.
    const server = sync.Server{ .version = .{ .version = 22800, .datetime = .{ .date = 20260715, .time = 0 } } };
    var payload_buf: [submanifest_config_payload_max]u8 = undefined;
    const payload = try writeSubmanifestConfigPayload(true, 7, true, &payload_buf);
    var request_buf: [512]u8 = undefined;
    const request = try std.fmt.bufPrint(
        &request_buf,
        "pragma client-version 20000\nconfig /config {d}\n{s}\n",
        .{ payload.len, payload },
    );
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);
    const reply = try server.respondWithMedia(gpa, ms.store(), request, null, &out);
    // §11.1 r2 — an error card's free text is fossilize-encoded, so the RFC's
    // exact TEXT travels with its spaces escaped. Both halves are pinned: the
    // literal wire bytes, and the unescaped string §9.3 r31 fixes.
    try std.testing.expectEqualStrings("error not\\sauthorized\\sto\\spush\\sconfiguration\n", reply.body);
    try std.testing.expectEqualStrings("not authorized to push configuration", config_push_unauthorized_text);
    // Nothing to apply: the rejection is reset-and-emit, so no setting escapes.
    try std.testing.expect(!reply.config_apply.any());
}

test "sync §9.3 r33: a pushed config card from a verified login is applied" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    const secret: [40]u8 = "0123456789abcdef0123456789abcdef01234567".*;
    const server = sync.Server{
        .version = .{ .version = 22800, .datetime = .{ .date = 20260715, .time = 0 } },
        .credential = .{ .user = "drh", .secret_hex = secret },
    };
    var payload_buf: [submanifest_config_payload_max]u8 = undefined;
    const payload = try writeSubmanifestConfigPayload(true, 7, false, &payload_buf);

    // §6.2 — the nonce covers exactly the bytes after the login LF, so the
    // signed region is built first and the login card prefixed onto it.
    var tail_buf: [512]u8 = undefined;
    const tail = try std.fmt.bufPrint(&tail_buf, "config /config {d}\n{s}\n", .{ payload.len, payload });
    const nonce = computeNonce(tail);
    const signature = computeSignature(&nonce, &secret);
    var request_buf: [768]u8 = undefined;
    const request = try std.fmt.bufPrint(
        &request_buf,
        "login drh {s} {s}\n{s}",
        .{ &nonce, &signature, tail },
    );
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);
    const reply = try server.respondWithMedia(gpa, ms.store(), request, null, &out);
    try std.testing.expect(std.mem.indexOf(u8, reply.body, "error") == null);
    try std.testing.expectEqual(@as(?bool, true), reply.config_apply.enable);
    try std.testing.expectEqual(@as(?u8, 7), reply.config_apply.k);
    try std.testing.expectEqual(@as(?bool, false), reply.config_apply.auto_retune);
}

test "sync §9.3/§8.4: a clone's cleanup round requests /config; the clone-card round does not" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();

    // §8.2 L1541-1547 — the post-clone_seqno-0 cleanup round carries no clone
    // card; §9.3 "Clone defers requests" makes it the one place the client asks.
    var cleanup = Client{ .operation = .clone, .state = .exchange, .clone_seqno = 0 };
    defer cleanup.deinit(gpa);
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);
    const body = try cleanup.buildRequest(gpa, ms.store(), &out);
    try expectHasLine(body, "reqconfig /config");

    // Round 0 and every paging round carry the clone card and no request.
    var paging = Client{ .operation = .clone, .state = .clone_round0, .clone_seqno = 1 };
    defer paging.deinit(gpa);
    var out2: std.ArrayListUnmanaged(u8) = .empty;
    defer out2.deinit(gpa);
    const paging_body = try paging.buildRequest(gpa, ms.store(), &out2);
    try std.testing.expect(std.mem.indexOf(u8, paging_body, "reqconfig") == null);

    // Scope: a pull/push/sync never asks — clone-time adoption only (a later
    // refresh would need real MTIME conflict resolution the config frame has
    // no storage for).
    var pull = Client{ .operation = .pull };
    defer pull.deinit(gpa);
    var out3: std.ArrayListUnmanaged(u8) = .empty;
    defer out3.deinit(gpa);
    const pull_body = try pull.buildRequest(gpa, ms.store(), &out3);
    try std.testing.expect(std.mem.indexOf(u8, pull_body, "reqconfig") == null);
}

test "sync §9.3: a client latches the three settings off a scalar config card" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .clone, .state = .exchange, .clone_seqno = 0 };
    defer client.deinit(gpa);
    try std.testing.expect(!client.peerSubmanifestConfig().any());

    var payload_buf: [submanifest_config_payload_max]u8 = undefined;
    const payload = try writeSubmanifestConfigPayload(true, 4, true, &payload_buf);
    var reply_buf: [512]u8 = undefined;
    const reply = try std.fmt.bufPrint(&reply_buf, "config /config {d}\n{s}\n", .{ payload.len, payload });
    _ = try client.consumeReply(gpa, ms.store(), reply);
    const got = client.peerSubmanifestConfig();
    try std.testing.expectEqual(@as(?bool, true), got.enable);
    try std.testing.expectEqual(@as(?u8, 4), got.k);
    try std.testing.expectEqual(@as(?bool, true), got.auto_retune);

    // A TABLE group's payload is framed and skipped, exactly as before: the
    // latch is untouched by a card that carries no scalar settings.
    var table_client = Client{ .operation = .clone, .state = .exchange, .clone_seqno = 0 };
    defer table_client.deinit(gpa);
    _ = try table_client.consumeReply(gpa, ms.store(), "config /shun 3\nabc\n");
    try std.testing.expect(!table_client.peerSubmanifestConfig().any());
}

test "sync §9.4 r35: pragma ci-lock HASH CLIENT-ID" {
    const lock = parseCiLock(tokenizeCard("pragma ci-lock abcdef0123 checkout-42")).?;
    try std.testing.expectEqualStrings("abcdef0123", lock.hash);
    try std.testing.expectEqualStrings("checkout-42", lock.client_id);

    var buf: [48]u8 = undefined;
    try std.testing.expectEqualStrings("pragma ci-lock abcdef0123 checkout-42", try writeCiLock("abcdef0123", "checkout-42", &buf));
}

test "sync §9.4 r36: lock held by a different client -> ci-lock-fail LOGIN MTIME (rejection)" {
    try std.testing.expectEqual(CiLockResult.failed, serverCiLockResult("alice", "bob"));
    try std.testing.expectEqual(CiLockResult.granted, serverCiLockResult("alice", "alice"));
    try std.testing.expectEqual(CiLockResult.granted, serverCiLockResult(null, "bob"));

    var buf: [48]u8 = undefined;
    const written = try writeCiLockFail("alice", 1720483200, &buf);
    try std.testing.expectEqualStrings("pragma ci-lock-fail alice 1720483200", written);
    const parsed = parseCiLockFail(tokenizeCard(written)).?;
    try std.testing.expectEqualStrings("alice", parsed.login);
    try std.testing.expectEqual(@as(u64, 1720483200), parsed.mtime);
}

test "sync §9.4 r37: recorded ci-lock-fault blocks only a fork-creating commit without an override" {
    // §9.4 L1965-1970 records the fault and continues; the fork/override gate is zit-local: block iff a fault is recorded AND the commit
    // would fork AND no override was requested -- exhaustive 8-row
    // truth table, one row per combination.
    const Case = struct { fault: bool, fork: bool, override: bool, want: bool };
    const cases = [_]Case{
        .{ .fault = false, .fork = false, .override = false, .want = true },
        .{ .fault = false, .fork = false, .override = true, .want = true },
        .{ .fault = false, .fork = true, .override = false, .want = true },
        .{ .fault = false, .fork = true, .override = true, .want = true },
        .{ .fault = true, .fork = false, .override = false, .want = true },
        .{ .fault = true, .fork = false, .override = true, .want = true },
        .{ .fault = true, .fork = true, .override = false, .want = false }, // the single blocked row
        .{ .fault = true, .fork = true, .override = true, .want = true },
    };
    for (cases) |c| {
        try std.testing.expectEqual(c.want, commitMayProceedAfterCiLockFail(c.fault, c.fork, c.override));
    }
}

test "sync §9.4 r38: pragma ci-unlock CLIENT-ID releases the lock" {
    const id = parseCiUnlock(tokenizeCard("pragma ci-unlock checkout-42")).?;
    try std.testing.expectEqualStrings("checkout-42", id);

    var buf: [32]u8 = undefined;
    try std.testing.expectEqualStrings("pragma ci-unlock checkout-42", try writeCiUnlock("checkout-42", &buf));
}

test "sync §9.4 r39: known-id clients send ci-unlock in later rounds when no lock pending" {
    const Case = struct { has_client_id: bool, lock_pending: bool, round: u64, want: bool };
    const cases = [_]Case{
        .{ .has_client_id = true, .lock_pending = false, .round = 3, .want = true },
        .{ .has_client_id = true, .lock_pending = true, .round = 3, .want = false },
        .{ .has_client_id = false, .lock_pending = false, .round = 3, .want = false },
        .{ .has_client_id = true, .lock_pending = false, .round = 0, .want = false },
    };
    for (cases) |c| {
        try std.testing.expectEqual(c.want, clientSendsCiUnlock(c.has_client_id, c.lock_pending, c.round));
    }
}

test "sync §9.5 r40: pragma send-private is client-to-server, arg-less" {
    var buf: [32]u8 = undefined;
    try std.testing.expectEqualStrings("pragma send-private", try writeFlagPragma(.@"send-private", &buf));
    try std.testing.expectEqual(PragmaDirection.client_to_server, pragmaDirection(.@"send-private"));
}

test "sync §9.5 r41: server enables private sync only with the Private (x) capability" {
    try std.testing.expectEqual(SendPrivateDecision.enable, serverSendPrivateDecision(true));
    try std.testing.expectEqual(SendPrivateDecision.reject, serverSendPrivateDecision(false));
}

test "sync §9.5 r42: unauthorized send-private emits a non-reset error card (rejection)" {
    try std.testing.expectEqualStrings("not authorized to sync private content", send_private_unauthorized_text);

    var buf: [128]u8 = undefined;
    try std.testing.expectEqualStrings(
        "error not\\sauthorized\\sto\\ssync\\sprivate\\scontent",
        writeSignalCard(.err, send_private_unauthorized_text, &buf),
    );

    try std.testing.expectEqual(ErrorReset.append, serverErrorReset(.unauthorized_send_private));
}

test "sync §9.5 r43: send-private repeats every private round; the decision is idempotent" {
    try std.testing.expectEqual(PragmaSchedule.every_private_round, pragmaSchedule(.@"send-private").?);
    try std.testing.expectEqual(SendPrivateDecision.enable, serverSendPrivateDecision(true));
    try std.testing.expectEqual(SendPrivateDecision.enable, serverSendPrivateDecision(true));
}

test "sync §9.5 r44: pragma send-catalog requests every igot (full resync)" {
    var buf: [32]u8 = undefined;
    try std.testing.expectEqualStrings("pragma send-catalog", try writeFlagPragma(.@"send-catalog", &buf));
}

test "sync §9.5 r45: pragma req-clusters requests cluster igot cards" {
    var buf: [32]u8 = undefined;
    try std.testing.expectEqualStrings("pragma req-clusters", try writeFlagPragma(.@"req-clusters", &buf));
}

test "sync §9.5 r46: pragma req-links requests link-graph data" {
    var buf: [32]u8 = undefined;
    try std.testing.expectEqualStrings("pragma req-links", try writeFlagPragma(.@"req-links", &buf));
}

test "sync §9.5 r47: pragma client-url communicates the client's canonical URL" {
    var buf: [64]u8 = undefined;
    try std.testing.expectEqualStrings("pragma client-url http://x/y", writeClientUrl("http://x/y", &buf));

    var write_buf: [64]u8 = undefined;
    const written = writeClientUrl("a b", &write_buf);
    try std.testing.expectEqualStrings("pragma client-url a\\sb", written);

    var decode_buf: [16]u8 = undefined;
    const parsed = parseClientUrl(tokenizeCard(written), &decode_buf).?;
    try std.testing.expectEqualStrings("a b", parsed);
}

test "sync §9.5 r48: client-url and req-links are sent once, before the round loop" {
    try std.testing.expectEqual(PragmaSchedule.once_pre_loop, pragmaSchedule(.@"client-url").?);
    try std.testing.expectEqual(PragmaSchedule.once_pre_loop, pragmaSchedule(.@"req-links").?);
}

test "sync §9.5 r49: pragma avoid-delta-manifests is server-to-client" {
    try std.testing.expectEqual(PragmaDirection.server_to_client, pragmaDirection(.@"avoid-delta-manifests"));
    var buf: [40]u8 = undefined;
    try std.testing.expectEqualStrings("pragma avoid-delta-manifests", try writeFlagPragma(.@"avoid-delta-manifests", &buf));
}

test "sync §9.5: arg-less pragma set matches the ABNF" {
    const Case = struct { name: PragmaName, argless: bool };
    const cases = [_]Case{
        .{ .name = .@"send-private", .argless = true },
        .{ .name = .@"send-catalog", .argless = true },
        .{ .name = .@"req-links", .argless = true },
        .{ .name = .@"req-clusters", .argless = true },
        .{ .name = .@"avoid-delta-manifests", .argless = true },
        .{ .name = .@"uv-push-ok", .argless = true },
        .{ .name = .@"uv-pull-only", .argless = true },
        .{ .name = .@"client-version", .argless = false },
        .{ .name = .@"server-version", .argless = false },
        .{ .name = .@"uv-hash", .argless = false },
        .{ .name = .@"ci-lock", .argless = false },
        .{ .name = .@"ci-lock-fail", .argless = false },
        .{ .name = .@"ci-unlock", .argless = false },
        .{ .name = .@"client-url", .argless = false },
    };
    try std.testing.expectEqual(@as(usize, 14), cases.len);
    for (cases) |c| {
        try std.testing.expectEqual(c.argless, pragmaIsArgless(c.name));
    }
}

test "sync §11.1 r1: an error card is 'error TEXT' (fatal outside clone round 0)" {
    // L2604-2607. The "fatal except the first clone round" half is realized by
    // r4 (terminate elsewhere) and r10 (discard in round 0) below.
    var buf: [16]u8 = undefined;
    try std.testing.expectEqualStrings("error abc", writeSignalCard(.err, "abc", &buf));
    try std.testing.expectEqual(Operator.@"error", classifyCard("error abc").operator.?);
}

test "sync §11.1 r2: error TEXT is fossilize-encoded, spaces as \\s" {
    // L2609-2610.
    var buf: [16]u8 = undefined;
    try std.testing.expectEqualStrings("error a\\sb", writeSignalCard(.err, "a b", &buf));
}

test "sync §11.1 r3: a receiver defossilizes error TEXT before display/log" {
    // L2611-2612.
    var decode_buf: [8]u8 = undefined;
    const decoded = parseSignalText(tokenizeCard("error a\\sb"), &decode_buf).?;
    try std.testing.expectEqualStrings("a b", decoded);
}

test "sync §11.1 r4: an error card outside clone round 0 stops processing and terminates" {
    // L2614-2616. "Stop processing the response body" is realized as
    // terminating the sync loop; every non-round-0 state terminates.
    const states = [_]ClientState{ .init, .exchange, .terminated, .aborted };
    for (states) |s| try std.testing.expectEqual(ClientErrorAction.terminate, clientOnErrorCard(s));
}

test "sync §11.1 r5: no retry via alternative encoding or hash algorithm (rejection)" {
    // L2616-2617; A.2 "No SHA1 retry" L3219. The client's only dispositions are
    // discard/terminate -- no "retry" variant, so a retry is unrepresentable.
    const fields = @typeInfo(ClientErrorAction).@"enum".fields;
    try std.testing.expectEqual(@as(usize, 2), fields.len);
    try std.testing.expectEqualStrings("discard", fields[0].name);
    try std.testing.expectEqualStrings("terminate", fields[1].name);
    try std.testing.expectEqual(ClientErrorAction.terminate, clientOnErrorCard(.exchange));
}

test "sync §11.1 r6/r8: reset sites discard prior output; append sites extend it" {
    const Case = struct { site: ErrorSite, want: ErrorReset };
    const cases = [_]Case{
        // r6 L2619-2623 -- fatal protocol-error sites reset prior partial output.
        .{ .site = .wrong_project, .want = .reset },
        .{ .site = .auth_failure, .want = .reset },
        .{ .site = .duplicate_login, .want = .reset },
        .{ .site = .unknown_operator, .want = .reset },
        // r8 L2626-2629 -- gate / send-private / private sites append to the
        // already-built response instead of resetting it.
        .{ .site = .peer_version_gate, .want = .append },
        .{ .site = .unauthorized_send_private, .want = .append },
        .{ .site = .private_card, .want = .append },
    };
    for (cases) |c| try std.testing.expectEqual(c.want, serverErrorReset(c.site));
}

test "sync §11.1 r7: a reset does not promise an error-only response body" {
    // L2623-2626. The disposition is binary {reset,append}; there is no
    // "error_only" variant, so a reset governs only prior-output discard --
    // the unauthorized-clone site still emits a push card after the reset and
    // before the error card.
    const fields = @typeInfo(ErrorReset).@"enum".fields;
    try std.testing.expectEqual(@as(usize, 2), fields.len);
    try std.testing.expectEqualStrings("reset", fields[0].name);
    try std.testing.expectEqualStrings("append", fields[1].name);
    try std.testing.expectEqual(ErrorReset.reset, serverErrorReset(.auth_failure));
}

test "sync §11.1 r9: the error is signalled in the body, not an HTTP status" {
    // L2639-2640; A.2 header "HTTP 200" L3203. classifyCard detects the error
    // in the body; clientOnErrorCard takes no status input.
    try std.testing.expectEqual(Operator.@"error", classifyCard("error abc").operator.?);
}

test "sync §11.1 r10: the first clone round silently discards an error card" {
    // L2642-2644.
    try std.testing.expectEqual(ClientErrorAction.discard, clientOnErrorCard(.clone_round0));
}

test "sync §11.1 r11: the SHA3 peer-version gate text is exact" {
    // L2648-2650 (decoded text); encoded form per A.2 L3206.
    try std.testing.expectEqualStrings("Fossil version 2.0 or later required.", peer_version_error_text);
    var buf: [96]u8 = undefined;
    try std.testing.expectEqualStrings(
        "error Fossil\\sversion\\s2.0\\sor\\slater\\srequired.",
        writeSignalCard(.err, peer_version_error_text, &buf),
    );
}

test "sync §11.1 App. A.2: SHA3 version-gate error card, discarded by a round-0 clone" {
    // First response-body line L3206; decoded text L3211. The 191-byte
    // Content-Length (L3204) is already pinned by the §7.6 App. A.2 test below,
    // so it is not re-asserted here.
    var buf: [96]u8 = undefined;
    try std.testing.expectEqualStrings(
        "error Fossil\\sversion\\s2.0\\sor\\slater\\srequired.",
        writeSignalCard(.err, peer_version_error_text, &buf),
    );
    var decode_buf: [64]u8 = undefined;
    const line = "error Fossil\\sversion\\s2.0\\sor\\slater\\srequired.";
    const decoded = parseSignalText(tokenizeCard(line), &decode_buf).?;
    try std.testing.expectEqualStrings("Fossil version 2.0 or later required.", decoded);
    // A round-0 clone client silently discards it (L3221-3227); no SHA1 retry
    // (L3219) -- the disposition enum has no retry variant.
    try std.testing.expectEqual(ClientErrorAction.discard, clientOnErrorCard(.clone_round0));
}

test "sync §11.2 r1: a message card is 'message TEXT', a non-fatal advisory" {
    // L2654-2656.
    var buf: [16]u8 = undefined;
    try std.testing.expectEqualStrings("message hi", writeSignalCard(.message, "hi", &buf));
    try std.testing.expectEqual(Operator.message, classifyCard("message hi").operator.?);
}

test "sync §11.2 r2: message TEXT is encoded identically to an error card" {
    // L2658. Same single-text-argument encoder: the tail after the operator
    // word is byte-identical between the two cards.
    var mbuf: [16]u8 = undefined;
    var ebuf: [16]u8 = undefined;
    const msg = writeSignalCard(.message, "a b", &mbuf);
    const err = writeSignalCard(.err, "a b", &ebuf);
    try std.testing.expectEqualStrings("message a\\sb", msg);
    try std.testing.expectEqualStrings(err["error ".len..], msg["message ".len..]);
}

test "sync §11.2 r1: a written message card tokenizes to exactly two tokens" {
    // §11.2 item preamble — "exactly two whitespace-separated tokens". TEXT is
    // fossilize-encoded, so any embedded SP/LF becomes an escape and never a
    // second separator: the card always splits into `message` + one argument,
    // never surplus, even for whitespace-laden text.
    var buf: [64]u8 = undefined;
    const texts = [_][]const u8{ "a b", "x\ny", "c\\d", "pull only " };
    for (texts) |t| {
        const card = writeSignalCard(.message, t, &buf);
        const toks = tokenizeCard(card);
        try std.testing.expectEqual(@as(usize, 2), toks.len);
        try std.testing.expect(!toks.surplus);
        try std.testing.expectEqualStrings("message", toks.items[0]);
    }
}

test "sync §11.2 r2: message TEXT round-trips fossilize/defossilize" {
    // §11.2 item preamble — TEXT uses the error-card encoding; a receiver
    // defossilizes with the same scheme. parseSignalText(tokenize(write)) is
    // the identity on the original text.
    var enc: [64]u8 = undefined;
    var dec: [64]u8 = undefined;
    const texts = [_][]const u8{ "a b", "x\ny", "c\\d", "pull only for now" };
    for (texts) |t| {
        const card = writeSignalCard(.message, t, &enc);
        const got = parseSignalText(tokenizeCard(card), &dec).?;
        try std.testing.expectEqualStrings(t, got);
    }
}

test "sync §11.2 r3: a message card never terminates the session" {
    // §11.2 item 2/3 — the client's dispositions are {pull_only_silent,
    // display_optional}; neither aborts or errors the exchange.
    const fields = @typeInfo(MessageAction).@"enum".fields;
    try std.testing.expectEqual(@as(usize, 2), fields.len);
    try std.testing.expectEqualStrings("pull_only_silent", fields[0].name);
    try std.testing.expectEqualStrings("display_optional", fields[1].name);
    try std.testing.expectEqual(MessageAction.display_optional, clientOnMessageCard(true, "hello"));
}

test "sync §11.2 r7: pull only GLOB is case-sensitive with a fixed 'pull only ' prefix" {
    // §11.2 item 1 — "the comparison is case-sensitive; the literal ASCII
    // sequence pull only ... separated by a single SP and followed by a
    // mandatory trailing SP is the fixed prefix". Anything not carrying that
    // exact 10-byte prefix at offset 0 fails.
    const Case = struct { text: []const u8, want: bool };
    const cases = [_]Case{
        .{ .text = "pull only ", .want = true }, // exact prefix, empty suffix
        .{ .text = "Pull only ", .want = false }, // case-sensitive
        .{ .text = "PULL ONLY ", .want = false }, // case-sensitive
        .{ .text = "pull  only ", .want = false }, // double SP != single-SP prefix
        .{ .text = "pull only", .want = false }, // no mandatory trailing SP
        .{ .text = "xpull only ", .want = false }, // prefix not at start
    };
    for (cases) |c| try std.testing.expectEqual(c.want, matchesPullOnly(c.text));
}

test "sync §11.2 r8: worked-example pull only payloads match; no whitespace trimming" {
    // §11.2 item 1 worked examples, verbatim: a bare "pull only " (trailing SP,
    // empty suffix) matches, as do "pull only for now" and
    // "pull only pushing disabled". "No trimming of surrounding whitespace is
    // performed", so a leading SP defeats the fixed-prefix match.
    const Case = struct { text: []const u8, want: bool };
    const cases = [_]Case{
        .{ .text = "pull only ", .want = true },
        .{ .text = "pull only for now", .want = true },
        .{ .text = "pull only pushing disabled", .want = true },
        .{ .text = " pull only ", .want = false }, // leading SP, no trimming
    };
    for (cases) |c| try std.testing.expectEqual(c.want, matchesPullOnly(c.text));
}

test "sync §11.2 r4/r9/r14: clear only when push is live and the payload matches" {
    // §11.2 item 1 clears iff push intent is live AND payload matches pull
    // only * (r4); every other combination is item 2's advisory path — a
    // non-matching payload (r9) or a match while push is already clear (r14).
    const Case = struct { push_active: bool, text: []const u8, want: MessageAction };
    const cases = [_]Case{
        .{ .push_active = true, .text = "pull only ", .want = .pull_only_silent }, // r4 empty suffix
        .{ .push_active = true, .text = "pull only now", .want = .pull_only_silent }, // r4 suffix
        .{ .push_active = true, .text = "hello", .want = .display_optional }, // r9 non-match
        .{ .push_active = true, .text = "pull only", .want = .display_optional }, // r9 bare, no trailing SP
        .{ .push_active = false, .text = "pull only ", .want = .display_optional }, // r14 push already clear
        .{ .push_active = false, .text = "hello", .want = .display_optional }, // r9 non-match, pull
    };
    for (cases) |c| try std.testing.expectEqual(c.want, clientOnMessageCard(c.push_active, c.text));
}

test "sync §3.6: server emits 'cookie TEXT', TEXT opaque" {
    var buf: [24]u8 = undefined;
    try std.testing.expectEqualStrings("cookie s3cr3t", writeCookieCard("s3cr3t", &buf));
    const c = classifyCard("cookie s3cr3t");
    try std.testing.expectEqual(Operator.cookie, c.operator.?);
    try std.testing.expectEqual(Disposition.dispatch, c.disposition);
}

test "sync §3.6/§5.2: cookie TEXT is fossilize-encoded" {
    var buf: [16]u8 = undefined;
    try std.testing.expectEqualStrings("cookie a\\sb", writeCookieCard("a b", &buf));
}

test "sync §3.6: client stores cookie, replacing any previously held" {
    var store: CookieStore = .{};
    store.store("v1");
    store.store("v2");
    try std.testing.expectEqualStrings("v2", store.value().?);

    var decode_buf: [8]u8 = undefined;
    const decoded = parseSignalText(tokenizeCard("cookie v2"), &decode_buf).?;
    store.store(decoded);
    try std.testing.expectEqualStrings("v2", store.value().?);
}

test "sync §3.6: client returns the stored cookie in a later request" {
    var store: CookieStore = .{};
    store.store("a b");
    var buf: [16]u8 = undefined;
    try std.testing.expectEqualStrings("cookie a\\sb", writeCookieCard(store.value().?, &buf));
}

test "sync §3.6: server does not depend on a cookie card" {
    const c = classifyCard("cookie x");
    try std.testing.expectEqual(Operator.cookie, c.operator.?);
    try std.testing.expectEqual(Disposition.dispatch, c.disposition);
    try std.testing.expect(!isFatal(c.disposition, .server));
}

test "sync §3.6: server tolerates a cookie from a different server" {
    const written = "cookie a\\sb";
    const c = classifyCard(written);
    try std.testing.expectEqual(Disposition.dispatch, c.disposition);
    try std.testing.expect(!isFatal(c.disposition, .server));

    var decode_buf: [8]u8 = undefined;
    const decoded = parseSignalText(tokenizeCard(written), &decode_buf).?;
    try std.testing.expectEqualStrings("a b", decoded);
}

test "sync §3.6: server tolerates more than one cookie card in a request" {
    const lines = [_][]const u8{ "cookie one", "cookie two", "cookie three" };
    for (lines) |line| {
        const c = classifyCard(line);
        try std.testing.expectEqual(Disposition.dispatch, c.disposition);
        try std.testing.expect(!isFatal(c.disposition, .server));
    }
}

test "sync §3.6: a client that never returns a cookie is conformant" {
    const store: CookieStore = .{};
    try std.testing.expect(store.value() == null);
}

test "sync §7.6/§12.4: SHA3-256 artifact refused to peer version < 20000 (rejection)" {
    const sha3_hex = "a" ** 64;
    try std.testing.expectEqual(GateDecision.refuse, sha3PeerVersionGate(19999, sha3_hex));
    try std.testing.expectEqual(GateDecision.emit, sha3PeerVersionGate(20000, sha3_hex));
}

test "sync §12.4 / §7.6 r3: no silent downgrade to SHA1" {
    const sha3_hex = "a" ** 64;
    const sha1_hex = "a" ** 40;
    try std.testing.expectEqual(GateDecision.refuse, sha3PeerVersionGate(19999, sha3_hex));
    try std.testing.expectEqual(GateDecision.emit, sha3PeerVersionGate(19999, sha1_hex));
}

test "sync §12.4: gate has no truncate/re-identify path (rejection)" {
    try std.testing.expectEqual(@as(usize, 2), @typeInfo(GateDecision).@"enum".fields.len);
    const sha3_hex = "a" ** 64;
    const sha1_hex = "a" ** 40;
    const not_hex_64 = "z" ** 64;
    try std.testing.expectEqual(GateDecision.refuse, sha3PeerVersionGate(19999, sha3_hex));
    try std.testing.expectEqual(GateDecision.emit, sha3PeerVersionGate(20000, sha3_hex));
    try std.testing.expectEqual(GateDecision.emit, sha3PeerVersionGate(19999, sha1_hex));
    try std.testing.expectEqual(GateDecision.emit, sha3PeerVersionGate(20000, sha1_hex));
    try std.testing.expectEqual(GateDecision.emit, sha3PeerVersionGate(19999, not_hex_64));
}

test "sync §10.1: gate is pure — unrecorded-peer default is the caller's, side-dependent" {
    const sha3_hex = "a" ** 64;
    // Server side: before any client-version pragma, VERSION is treated
    // as 0 (L2317-2319) -> the caller passes 0 -> refuse.
    try std.testing.expectEqual(GateDecision.refuse, sha3PeerVersionGate(0, sha3_hex));
    // Client side: before any server-version pragma, VERSION is treated
    // as the client's own release integer (L2325-2327); a SHA3-capable
    // client's own integer is >= 20000 -> the caller passes that -> emit.
    const own_release: u32 = 20500;
    try std.testing.expectEqual(GateDecision.emit, sha3PeerVersionGate(own_release, sha3_hex));
}

test "sync §7.6 r6: the one gate governs the file, cfile, and uvfile 64-hex forms" {
    const sites = [_][]const u8{
        "1" ** 64, // stands for a file-card HASH
        "2" ** 64, // stands for a cfile-card HASH
        "3" ** 64, // stands for a uvfile-card HASH field
    };
    const sha1_hex = "a" ** 40;
    for (sites) |hash| {
        try std.testing.expectEqual(GateDecision.refuse, sha3PeerVersionGate(19999, hash));
        try std.testing.expectEqual(GateDecision.emit, sha3PeerVersionGate(20000, hash));
    }
    try std.testing.expectEqual(GateDecision.emit, sha3PeerVersionGate(19999, sha1_hex));
}

test "sync §7.6 App. A.2: gate-fire response body is 191 bytes; zit's error card is its first line" {
    const body =
        "error Fossil\\sversion\\s2.0\\sor\\slater\\srequired.\n" ++
        "clone_seqno 0\n" ++
        "push 34302acad64fe2397bd907704dec457899e2f9a3 c85e3efddf993e86e8325d08672ea94bdba6c2c4\n" ++
        "# timestamp 2026-07-02T18:26:49 errors 0\n";
    try std.testing.expectEqual(@as(usize, 191), body.len);

    var buf: [96]u8 = undefined;
    const err_card = writeSignalCard(.err, peer_version_error_text, &buf);
    const first_line_end = std.mem.indexOfScalar(u8, body, '\n').?;
    try std.testing.expectEqualStrings(err_card, body[0..first_line_end]);
}

test "sync §3.7 r1: another round iff at least one of (a)-(f) holds" {
    const base = RoundObservation{ .operation = .pull };
    try std.testing.expect(!continueExchange(base));

    var a = base;
    a.versioned_content_delivered = true;
    a.phantom_table_nonempty = true;
    try std.testing.expect(continueExchange(a));

    var b = base;
    b.content_queued = true;
    try std.testing.expect(continueExchange(b));

    var c = base;
    c.private_discovery_pending = true;
    try std.testing.expect(continueExchange(c));

    var d = base;
    d.uv_send_table_nonempty = true;
    d.uv_upload_enabled = true;
    try std.testing.expect(continueExchange(d));

    const e = RoundObservation{ .operation = .clone, .first_clone_round = true };
    try std.testing.expect(continueExchange(e));

    var f = base;
    f.config_exchange_pending = true;
    try std.testing.expect(continueExchange(f));
}

test "sync §3.7 r2: versioned progress with non-empty phantom table continues (a)" {
    const Case = struct { delivered: bool = false, created: bool = false, phantom_nonempty: bool, want: bool };
    const cases = [_]Case{
        .{ .delivered = true, .phantom_nonempty = true, .want = true },
        .{ .created = true, .phantom_nonempty = true, .want = true },
        .{ .delivered = true, .phantom_nonempty = false, .want = false }, // guard, L735-736
        .{ .phantom_nonempty = true, .want = false }, // nothing progressed
    };
    for (cases) |tc| {
        const obs = RoundObservation{
            .operation = .pull,
            .versioned_content_delivered = tc.delivered,
            .phantom_created = tc.created,
            .phantom_table_nonempty = tc.phantom_nonempty,
        };
        try std.testing.expectEqual(tc.want, continueExchange(obs));
    }
}

test "sync §3.7 r3: non-empty send queue continues (b)" {
    var obs = RoundObservation{ .operation = .push };
    obs.content_queued = true;
    try std.testing.expect(continueExchange(obs));
}

test "sync §3.7 r4: private-content discovery round continues (c)" {
    var obs = RoundObservation{ .operation = .sync };
    obs.private_discovery_pending = true;
    try std.testing.expect(continueExchange(obs));
}

test "sync §3.7 r5: unversioned send table + upload-enabled continues (d)(1)" {
    const Case = struct { table_nonempty: bool, upload_enabled: bool, want: bool };
    const cases = [_]Case{
        .{ .table_nonempty = true, .upload_enabled = true, .want = true },
        .{ .table_nonempty = true, .upload_enabled = false, .want = false },
        .{ .table_nonempty = false, .upload_enabled = true, .want = false },
    };
    for (cases) |tc| {
        const obs = RoundObservation{
            .operation = .push,
            .uv_send_table_nonempty = tc.table_nonempty,
            .uv_upload_enabled = tc.upload_enabled,
        };
        try std.testing.expectEqual(tc.want, continueExchange(obs));
    }
}

test "sync §3.7 r6: uvgimme with uvfile or round<3 continues (d)(2)" {
    const Case = struct { queued: bool, uvfile: bool, round: u64, want: bool };
    const cases = [_]Case{
        .{ .queued = true, .uvfile = true, .round = 5, .want = true },
        .{ .queued = true, .uvfile = false, .round = 2, .want = true },
        .{ .queued = true, .uvfile = false, .round = 3, .want = false },
        .{ .queued = false, .uvfile = false, .round = 0, .want = false },
    };
    for (cases) |tc| {
        const obs = RoundObservation{
            .operation = .push,
            .uvgimme_queued = tc.queued,
            .uvfile_received = tc.uvfile,
            .session_round = tc.round,
        };
        try std.testing.expectEqual(tc.want, continueExchange(obs));
    }
}

test "sync §3.7 r7: modern-clone continuation conditions (e)" {
    const Case = struct {
        op: Operation = .clone,
        first: bool = false,
        received: bool = false,
        seqno: i64 = 0,
        grew: bool = false,
        want: bool,
    };
    const cases = [_]Case{
        .{ .first = true, .want = true },
        .{ .received = true, .want = true },
        .{ .seqno = 5, .grew = true, .want = true },
        .{ .seqno = 5, .grew = false, .want = false }, // non-advancing peer, L1915-1924
        .{ .seqno = 0, .grew = true, .want = false },
        .{ .op = .pull, .received = true, .seqno = 5, .grew = true, .want = false },
    };
    for (cases) |tc| {
        const obs = RoundObservation{
            .operation = tc.op,
            .first_clone_round = tc.first,
            .artifact_received_this_round = tc.received,
            .clone_seqno_next = tc.seqno,
            .artifact_count_grew = tc.grew,
        };
        try std.testing.expectEqual(tc.want, continueExchange(obs));
    }
}

test "sync §3.7 r8: pending configuration exchange continues (f)" {
    var obs = RoundObservation{ .operation = .sync };
    obs.config_exchange_pending = true;
    try std.testing.expect(continueExchange(obs));
}

test "sync §3.7 r9: phantoms remaining alone do not continue (rejection)" {
    const obs = RoundObservation{ .operation = .pull, .phantom_table_nonempty = true };
    try std.testing.expect(!continueExchange(obs));
}

test "sync §3.7 r10: no pending work in either direction stops" {
    const obs = RoundObservation{ .operation = .sync };
    try std.testing.expect(!continueExchange(obs));
    try std.testing.expectEqual(ClientState.terminated, transition(.exchange, .round_terminates));
}

test "sync §3.4: the modern-clone operation card is clone 3 SEQNO [sync-3-4-r1]" {
    // L684 — "a normal sequence of sync rounds whose operation card is
    // clone 3 SEQNO." SEQNO 42 is arbitrary non-negative, distinct from
    // §8.1's worked `clone 3 7`.
    var buf: [32]u8 = undefined;
    try std.testing.expectEqualStrings("clone 3 42", try writeCloneCard(42, &buf));
    // clone is an ordinary dispatchable operator, not a special-cased path.
    const cc = classifyCard("clone 3 42");
    try std.testing.expectEqual(Operator.clone, cc.operator.?);
    try std.testing.expectEqual(Disposition.dispatch, cc.disposition);
}

test "sync §3.4: clone replies ride the operation-agnostic dispatch loop [sync-3-4-r2]" {
    // L688-689 — clone replies are processed by the "same card dispatch
    // loop" as other rounds. Sameness is structural: the dispatch
    // primitives take no Operation parameter, so no round can branch the
    // loop by operation; adding such a parameter would trip this guard.
    inline for (.{ classifyCard, isFatal, replyBodyStep, readLine }) |f| {
        inline for (@typeInfo(@TypeOf(f)).@"fn".params) |p| {
            if (p.type) |t| try std.testing.expect(t != Operation);
        }
    }
    // Runtime drive: a mixed clone-round reply body walks that one loop.
    const body = "igot H1\npragma send-catalog\ngimme H2\n";
    const want = [_]Operator{ .igot, .pragma, .gimme };
    var offset: usize = 0;
    for (want) |op| {
        try std.testing.expectEqual(ReplyBodyStep.card_ready, replyBodyStep(body, offset));
        const line = try readLine(body, offset);
        const cc = classifyCard(line.bytes);
        try std.testing.expectEqual(op, cc.operator.?);
        try std.testing.expectEqual(Disposition.dispatch, cc.disposition);
        try std.testing.expect(!isFatal(cc.disposition, .client));
        offset = line.next;
    }
    try std.testing.expectEqual(ReplyBodyStep.round_complete, replyBodyStep(body, offset));
}

test "sync §3.4: clone replies use the same termination rule as other rounds [sync-3-4-r3]" {
    // L688-689 ("same ... termination rule") + §3.7 (a)-(f): one function
    // decides continuation for all four operations. Shared condition (f)
    // (L790-792, a reqconfig deferred during clone or otherwise required)
    // applies uniformly; clone-specific (e) (L765-769) lives inside it.
    for ([_]Operation{ .pull, .push, .sync, .clone }) |op| {
        try std.testing.expect(!continueExchange(.{ .operation = op }));
        try std.testing.expect(continueExchange(.{ .operation = op, .config_exchange_pending = true }));
    }
    try std.testing.expect(continueExchange(.{ .operation = .clone, .first_clone_round = true }));
}

test "sync §3.8 r11: the client occupies exactly five named states" {
    const fields = @typeInfo(ClientState).@"enum".fields;
    try std.testing.expectEqual(@as(usize, 5), fields.len);
    const want = [_][]const u8{ "init", "clone_round0", "exchange", "terminated", "aborted" };
    for (want) |name| {
        var found = false;
        inline for (fields) |f| {
            if (std.mem.eql(u8, f.name, name)) found = true;
        }
        try std.testing.expect(found);
    }
}

test "sync §3.8 r12: INIT + modern clone -> CLONE_ROUND0" {
    try std.testing.expectEqual(ClientState.clone_round0, initialState(.clone));
}

test "sync §3.8 r13: INIT + push/pull/sync -> EXCHANGE" {
    const ops = [_]Operation{ .push, .pull, .sync };
    for (ops) |op| try std.testing.expectEqual(ClientState.exchange, initialState(op));
}

test "sync §3.8 r14: CLONE_ROUND0 reply processed -> EXCHANGE, error discarded" {
    try std.testing.expectEqual(ClientState.exchange, transition(.clone_round0, .clone_reply_processed));
    try std.testing.expectEqual(ClientErrorAction.discard, clientOnErrorCard(.clone_round0));
}

test "sync §3.8 r15: CLONE_ROUND0 transport/redirect/neg-seqno -> ABORTED" {
    try std.testing.expectEqual(ClientState.aborted, transition(.clone_round0, .clone_fatal));
    try std.testing.expect(cloneSeqnoIsFatal(-1));
}

test "sync §3.8 r16: EXCHANGE + a 3.7 condition -> EXCHANGE" {
    try std.testing.expectEqual(ClientState.exchange, transition(.exchange, .round_continues));
}

test "sync §3.8 r17: EXCHANGE + none of 3.7 -> TERMINATED" {
    try std.testing.expectEqual(ClientState.terminated, transition(.exchange, .round_terminates));
}

test "sync §3.8 r18: EXCHANGE + fatal protocol error -> ABORTED" {
    try std.testing.expectEqual(ClientState.aborted, transition(.exchange, .fatal_error));
}

test "sync §3.8 r19: nine fatal causes abort EXCHANGE (rejection)" {
    const causes = [_]FatalCause{
        .error_card,           .project_code_mismatch, .auth_failure,     .peer_version_refusal,
        .unknown_operator,     .malformed_content,     .over_length_line, .redirect_limit,
        .clone_seqno_negative,
    };
    try std.testing.expectEqual(@as(usize, 9), causes.len);
    for (causes) |c| try std.testing.expect(causeIsFatal(.exchange, c));
}

test "sync §3.8 r19: CLONE_ROUND0 tolerates error-card-delivered causes, aborts client-detected ones (rejection)" {
    const tolerated = [_]FatalCause{ .error_card, .project_code_mismatch, .auth_failure, .peer_version_refusal };
    for (tolerated) |c| try std.testing.expect(!causeIsFatal(.clone_round0, c));

    const fatal = [_]FatalCause{
        .unknown_operator, .malformed_content, .over_length_line, .redirect_limit, .clone_seqno_negative,
    };
    for (fatal) |c| try std.testing.expect(causeIsFatal(.clone_round0, c));

    try std.testing.expectEqual(@as(usize, 9), tolerated.len + fatal.len);
    try std.testing.expect(cloneSeqnoIsFatal(-1));
    try std.testing.expect(!cloneSeqnoIsFatal(0));
    try std.testing.expect(!cloneSeqnoIsFatal(5));
}

test "sync §3.9 r1: an unknown non-pragma card operator is a fatal reject" {
    // §3.9 L803-806 — client-detected, so fatal in every state, round 0
    // included: it never arrives as a server error card.
    try std.testing.expect(causeIsFatal(.exchange, .unknown_operator));
    try std.testing.expect(causeIsFatal(.clone_round0, .unknown_operator));
    const c = classifyCard("frobnicate x");
    try std.testing.expectEqual(Disposition.unknown_operator, c.disposition);
    try std.testing.expect(isFatal(c.disposition, .client));
    try std.testing.expect(isFatal(c.disposition, .server));
}

test "sync §3.9 r2: a malformed content card is a fatal reject" {
    // §3.9 L803-806 "an unknown or locally malformed reply card" — here a
    // content-bearing operator carrying surplus tokens (§5 L1013-1016).
    try std.testing.expect(causeIsFatal(.exchange, .malformed_content));
    try std.testing.expect(causeIsFatal(.clone_round0, .malformed_content));
    const c = classifyCard("file a b c d e f g");
    try std.testing.expectEqual(Disposition.malformed_content, c.disposition);
    try std.testing.expect(isFatal(c.disposition, .server));
}

test "sync §3.9 r3: the liberality set is exactly the four named points, strict complement fatal" {
    // §3.9 L797-807 — liberal ∪ strict partitions the posture; the four
    // limited to the cases enumerated at L797-803; the four sampled below each
    // aborts (L998-999). Each case asserts its backing mechanism tolerant.
    // (1) unknown pragma name is ignored (§10).
    const pragma_tokens = tokenizeCard("pragma no-such");
    try std.testing.expect(classifyPragma(pragma_tokens).name == null);
    try std.testing.expectEqual(Disposition.dispatch, classifyCard("pragma no-such").disposition);
    // (2) runs of Fossil whitespace between tokens accepted (§5).
    const spaced = tokenizeCard("push   a\t\tb");
    const canonical = tokenizeCard("push a b");
    try std.testing.expectEqual(canonical.len, spaced.len);
    for (0..canonical.len) |i| try std.testing.expectEqualStrings(canonical.items[i], spaced.items[i]);
    // (3) a cookie card from an unexpected server is tolerated (§3.6).
    const cookie = classifyCard("cookie a\\sb");
    try std.testing.expectEqual(Disposition.dispatch, cookie.disposition);
    try std.testing.expect(!isFatal(cookie.disposition, .server));
    // (4) an error card in the first clone round is discarded (§11.1).
    try std.testing.expectEqual(ClientErrorAction.discard, clientOnErrorCard(.clone_round0));
    // Strict complement — unknown operator and malformed content abort on
    // either side, and no fatal cause names a cookie or a pragma.
    try std.testing.expect(isFatal(.unknown_operator, .server));
    try std.testing.expect(isFatal(.malformed_content, .server));
    inline for (@typeInfo(FatalCause).@"enum".fields) |f| {
        try std.testing.expect(!std.mem.eql(u8, f.name, "cookie"));
        try std.testing.expect(!std.mem.eql(u8, f.name, "pragma"));
    }
}

fn scanCanonical(bytes: []const u8) !void {
    try std.testing.expect(bytes.len > 0);
    try std.testing.expect(!isFossilWhitespace(bytes[0]));
    try std.testing.expect(!isFossilWhitespace(bytes[bytes.len - 1]));
    for (bytes, 0..) |b, i| {
        if (!isFossilWhitespace(b)) continue;
        try std.testing.expectEqual(@as(u8, ' '), b); // separator is one space
        try std.testing.expect(i + 1 < bytes.len and !isFossilWhitespace(bytes[i + 1]));
    }
    try std.testing.expect(!tokenizeCard(bytes).surplus);
}

test "sync §3.9 r4: senders emit the canonical single-space form" {
    // §5.2 L944-946 — every card writer emits the form receivers rely on:
    // single-space separators, no surrounding or doubled whitespace.
    var buf: [128]u8 = undefined;
    try scanCanonical(try writeCloneCard(1, &buf));
    try scanCanonical(try writePushPullCard(.push, "SC", "PC", &buf));
    try scanCanonical(try writePushPullCard(.pull, "SC", "PC", &buf));
    try scanCanonical(writeSignalCard(.err, "abc", &buf));
    try scanCanonical(writeSignalCard(.message, "hi", &buf));
    try scanCanonical(writeCookieCard("tok", &buf));
    try scanCanonical(try writeClientVersion(.{ .version = 20000 }, &buf));
}

test "sync §3.9 r4: a receiver does not preserve surplus trailing tokens" {
    // §3.9 L808-811 — past the six-slot window the 7th+ token is dropped
    // and flags surplus; surplus is non-dispatch on both branches: a
    // content card is .malformed_content (§5 L1013-1016), a simple card is
    // .bad_command (§5.2 L989-995 else).
    const t = tokenizeCard("push a b c d e f g");
    try std.testing.expectEqual(@as(usize, max_tokens), t.len);
    try std.testing.expect(t.surplus);
    try std.testing.expectEqual(Disposition.malformed_content, classifyCard("file a b c d e f g").disposition);
    try std.testing.expectEqual(Disposition.bad_command, classifyCard("push a b c d e f g").disposition);
}

test "sync §3.9 r5: a receiver accepts Fossil-whitespace runs between tokens" {
    // §5.2 L957-959 — the compatibility extension: multi-byte separators
    // tokenize identically to the canonical single-space form.
    const spaced = tokenizeCard("push   a\t\tb");
    const canonical = tokenizeCard("push a b");
    try std.testing.expectEqual(canonical.len, spaced.len);
    for (0..canonical.len) |i| try std.testing.expectEqualStrings(canonical.items[i], spaced.items[i]);
}

test "sync §3.9 r6: an unexpected-server cookie card is tolerated" {
    // §3.9 L797-801 (§3.6) — a cookie card dispatches, is never fatal, and
    // no fatal cause names a cookie.
    const c = classifyCard("cookie a\\sb");
    try std.testing.expectEqual(Disposition.dispatch, c.disposition);
    try std.testing.expect(!isFatal(c.disposition, .server));
    inline for (@typeInfo(FatalCause).@"enum".fields) |f| {
        try std.testing.expect(!std.mem.eql(u8, f.name, "cookie"));
    }
}

test "sync §3.9 r7: an error card in the first clone round is discarded" {
    // §3.9 L797-802 (§11.1) — round 0 discards, every other state terminates.
    try std.testing.expectEqual(ClientErrorAction.discard, clientOnErrorCard(.clone_round0));
    try std.testing.expectEqual(ClientErrorAction.terminate, clientOnErrorCard(.exchange));
}

test "sync §3.9 r8: outside the four liberal points a receiver applies a rule or aborts" {
    // §3.9 L806-808 — an input that is none of the four liberal points and
    // not a valid card aborts; there is no tolerant fall-through.
    const c = classifyCard("frobnicate");
    try std.testing.expectEqual(Disposition.unknown_operator, c.disposition);
    try std.testing.expect(isFatal(c.disposition, .server));
}

test "sync §3.2 L376-377: client-version precedes content-requesting cards; checkRequestOrder rejects the inversion" {
    // §3.2 L376-377: "Every request MUST contain pragma client-version.  It
    // MUST precede content-requesting cards so the server can gate SHA3 at
    // handler time." checkRequestOrder enforces exactly that and nothing more.
    //
    // This previously argued that a since-removed §3.10 "restates §3.2 and
    // defers to it, so introduces no new order rule". The section is gone, so
    // the argument had no subject; the rule it deferred TO is the real one.
    const ok = [_]RequestCard{ .client_version, .content_request };
    try std.testing.expectEqual(RequestOrder.ok, checkRequestOrder(&ok));
    const bad = [_]RequestCard{ .content_request, .client_version };
    try std.testing.expectEqual(RequestOrder.version_after_content_request, checkRequestOrder(&bad));
}

test "sync §3.2.1 r2: the canonical Table-4 write order projects to a well-ordered request" {
    // §3.2.1 L428-469 — request rows in write order: pragma client-version;
    // operation (clone → content_request); login; gimme (→ content_request);
    // file/cfile; igot; reqconfig/config/uvfile; cookie. The projection is
    // well-ordered and client-version (index 0) precedes every
    // content_request (L1026-1027).
    const rows = [_]RequestCard{
        .client_version, .content_request, .other, .content_request,
        .other,          .other,           .other, .other,
    };
    try std.testing.expectEqual(RequestOrder.ok, checkRequestOrder(&rows));
    try std.testing.expectEqual(RequestCard.client_version, rows[0]);
    for (rows, 0..) |card, i| {
        if (card == .content_request) try std.testing.expect(i > 0);
    }
}

test "sync §3.2.1/§5.3 r3: every request body ends with a random-bytes comment line, not the timestamp subform" {
    // §3.2.1 L466-467 (Table 4) + §5.3 L1057-1058 — the request's closing
    // comment carries implementation random bytes; the timestamp/errors
    // subform (§5.3 L1041-1043) is the reply's. A representative nonce line is a
    // comment but not a timestamp comment.
    try std.testing.expect(isCommentLine("# 9c1f4a7e2b3d"));
    try std.testing.expectEqual(@as(?TimestampComment, null), parseTimestampComment("# 9c1f4a7e2b3d"));
}

test "sync §3.2.1/§8.3 r7: the login-card row defers to §6 presence and §8.3 round-0 suppression" {
    // §3.2.1 L431-433 — §6 owns login presence; the "unless clone round 0"
    // branch is §8.3's sendableAfterCloneRound0; the "unless cookie mode"
    // branch is §3.6 (a stored cookie replaces the login card). §3.2.1's
    // Round Message Layout login row carries the presence rule.
    const line = "login privuser ee71329ef84fb77b428c6f626ad0103a426e0652 c3ee0be781c9ebcfc6e24756cbb75e6319ca255b";
    try std.testing.expect(parseLoginCard(tokenizeCard(line)) != null);
    try std.testing.expect(!sendableAfterCloneRound0(.clone, 0));
    try std.testing.expect(sendableAfterCloneRound0(.clone, 1));
    try std.testing.expect(sendableAfterCloneRound0(.pull, 0));
}

test "sync §3.2.1 r4: the reply-card membership set resolves to known operators" {
    // §3.2 L399-403 (Table 5) — every named reply card resolves to a
    // catalog operator; the closing comment is a comment line.
    const ops = [_][]const u8{
        "pragma", "push",   "pull",   "igot",        "file",    "cfile",
        "gimme",  "uvfile", "uvigot", "clone_seqno", "message", "error",
    };
    for (ops) |op| try std.testing.expect(parseOperator(op) != null);
    try std.testing.expect(isCommentLine("# timestamp 2026-07-02T18:31:13 errors 0"));
}

test "sync §3.2.1 r4: the reply body is an unordered applicable combination, not a positional sequence" {
    // §3.2.1 Round Message Layout — a reply body is any applicable
    // combination, so each reply card classifies
    // to .dispatch independent of position; reversing the body changes no
    // card's disposition. There is deliberately no checkReplyOrder analogue
    // to the request write-order authority.
    const reply = [_][]const u8{ "pull SC PC", "igot h 1", "clone_seqno 5", "message ok" };
    for (reply) |line| try std.testing.expectEqual(Disposition.dispatch, classifyCard(line).disposition);
    var i: usize = reply.len;
    while (i > 0) {
        i -= 1;
        try std.testing.expectEqual(Disposition.dispatch, classifyCard(reply[i]).disposition);
    }
}

test "sync §5.3 r5: every reply body ends with a timestamp/error-count comment line" {
    // §3.2.1 L492-493 + §5.3 L1041-1043 — the reply's closing comment is the
    // `# timestamp DATETIME errors N` subform; DATETIME is the 19-byte fixed
    // form YYYY-MM-DDTHH:MM:SS (§5.3 L1045-1046).
    const parsed = parseTimestampComment("# timestamp 2026-07-02T18:31:13 errors 0").?;
    try std.testing.expectEqual(@as(u64, 0), parsed.errors);
    try std.testing.expectEqualStrings("2026-07-02T18:31:13", parsed.datetime);
    try std.testing.expectEqual(@as(usize, 19), parsed.datetime.len);
}

test "sync §3.2 r6: a reply push/pull advertisement is recorded but does not by itself select the operation" {
    // §3.2 L408 — the push/pull card is a pure record of the server's
    // identifiers; only a separate rule such as `message pull only *`
    // (→ .pull_only_silent) changes the selected operation.
    const rec = parsePushPull(tokenizeCard("pull SC PC")).?;
    try std.testing.expectEqualStrings("SC", rec.servercode);
    try std.testing.expectEqualStrings("PC", rec.projectcode);
    try std.testing.expectEqual(MessageAction.pull_only_silent, clientOnMessageCard(true, "pull only *"));
    try std.testing.expectEqual(MessageAction.display_optional, clientOnMessageCard(true, "pull SC PC"));
}

const test_hash_a = "a" ** 40;

const test_hash_b = "b" ** 40;

test "sync §3.1 r20: per-round tables reset at round boundaries" {
    var rt = RoundTables.init(std.testing.allocator);
    defer rt.deinit();
    try rt.recordPeerHas(test_hash_a);
    try rt.recordPeerLacks(test_hash_b);
    rt.reset();
    try std.testing.expectEqual(@as(usize, 0), rt.peer_has.count());
    try std.testing.expectEqual(@as(usize, 0), rt.peer_lacks.count());
}

test "sync §3.1 r21: per-round tables carry no state across rounds" {
    var rt = RoundTables.init(std.testing.allocator);
    defer rt.deinit();
    try rt.recordPeerHas(test_hash_a);
    try std.testing.expect(rt.peerHas(test_hash_a));
    rt.reset();
    try std.testing.expect(!rt.peerHas(test_hash_a));
}

test "sync §3.3 r26: peer-has records hashes the peer announced or sent" {
    var rt = RoundTables.init(std.testing.allocator);
    defer rt.deinit();
    try rt.recordPeerHas(test_hash_a);
    try std.testing.expect(rt.peerHas(test_hash_a));
    try std.testing.expect(!rt.peerHas(test_hash_b));
}

test "sync §3.3 r27: peer-lacks records hashes the peer requested with gimme" {
    var rt = RoundTables.init(std.testing.allocator);
    defer rt.deinit();
    try rt.recordPeerLacks(test_hash_a);
    try std.testing.expect(rt.peerLacks(test_hash_a));
    try std.testing.expect(!rt.peerLacks(test_hash_b));
}

fn roundTablesNoLeak(gpa: std.mem.Allocator) !void {
    var rt = RoundTables.init(gpa);
    defer rt.deinit();
    try rt.recordPeerHas(test_hash_a);
    try rt.recordPeerLacks(test_hash_b);
    // Repeat record of an already-held key must not leak the fresh dupe.
    try rt.recordPeerHas(test_hash_a);
    rt.reset();
}

test "sync §3.3: round tables record* under allocation failure" {
    try std.testing.checkAllAllocationFailures(std.testing.allocator, roundTablesNoLeak, .{});
}

// §A.1 L2470-2471 — the two hashes the server gimmes because the client holds
// them (the client announced igot for both, L4099-4100). The manifest is the
// 272-byte checkin (L4123); alpha.txt is the 26-byte "alpha content from
// client\n" (L4131-4132, L4153).
const a1_manifest_hash = "8e0933ed1ef9aa0051d185c8123f0b9ac79865e4b70098b877543fca0092c259";
const a1_send_hash = "b1c1b05fea4f52bd90b15fdffd13f5c91dce6505f498eeb79b4952c2ea8d4d15";
// §A.1 L2453 — the third artifact the client already holds (shared by clone),
// and L4097-4098 the two repository identifiers carried in the operation cards.
const a1_held_hash = "c5b2e0ecb779a7616b32553d973bc9c4ae5a07569a2cee18001386da6bd9ae57";
const a1_servercode = "da3e69ab0655a7dd2aba2520b2d3302a97c85787";
const a1_projectcode = "aa87ea41b0bb05ba528f7a3bdba700d7e23d6318";

test "sync §3.7(b)/§3.3: a gimme for held content exposes the hash in the send queue" {
    // §3.2 L412-413 — a push/sync client that holds a gimme'd hash queues it
    // in the per-session outbound send buffer (§3.1 L312-314).
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const rid = try ms.store().put(a1_send_hash, "alpha content from client\n");
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), "gimme " ++ a1_send_hash ++ "\n");
    try std.testing.expect(obs.content_queued);
    try std.testing.expectEqual(@as(usize, 1), client.send_queue.count());
    try std.testing.expect(client.send_queue.contains(rid));
}

test "sync §3.3 L532-535: a gimme for unheld content queues no send" {
    // The receiver does not hold HASH → it MUST silently skip: peer-lacks is
    // still recorded (L848), but the send buffer stays empty.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), "gimme " ++ a1_send_hash ++ "\n");
    try std.testing.expect(!obs.content_queued);
    try std.testing.expectEqual(@as(usize, 0), client.send_queue.count());
    // Negative space (§3.3 L511-514 — the phantom set is closed; only igot and
    // dangling deltas may extend it): an unheld gimme resolves via lookup, not
    // intern, so it mints no phantom — not in the client's set, not in obs, and
    // no entry appears in the store for the gimme'd hash.
    try std.testing.expect(!obs.phantom_created);
    try std.testing.expect(!obs.phantom_table_nonempty);
    try std.testing.expectEqual(@as(usize, 0), client.phantoms.count());
    try std.testing.expect(ms.store().lookup(a1_send_hash) == null);
}

test "sync §3.3 L533-535/§3.2 L412-413: a gimme for held content on a receive-only op is not queued" {
    // pull and clone are receive-only: the receiver holds HASH but the
    // operation direction does not permit sending it — no content, no queue.
    const ops = [_]Operation{ .pull, .clone };
    for (ops) |op| {
        var ms = repostate.MemStore.init(std.testing.allocator);
        defer ms.deinit();
        _ = try ms.store().put(a1_send_hash, "alpha content from client\n");
        var client = Client{ .operation = op, .state = .exchange };
        defer client.deinit(std.testing.allocator);
        const obs = try client.consumeReply(std.testing.allocator, ms.store(), "gimme " ++ a1_send_hash ++ "\n");
        try std.testing.expect(!obs.content_queued);
        try std.testing.expectEqual(@as(usize, 0), client.send_queue.count());
    }
}

test "sync §3.3 L532-535: clientSendsContent is true only for push and sync" {
    const Case = struct { op: Operation, want: bool };
    const cases = [_]Case{
        .{ .op = .pull, .want = false },
        .{ .op = .push, .want = true },
        .{ .op = .sync, .want = true },
        .{ .op = .clone, .want = false },
    };
    for (cases) |tc| try std.testing.expectEqual(tc.want, clientSendsContent(tc.op));
}

test "sync §3.7(b) L581-583: content_queued reflects sends appended this round, not the persistent buffer" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    _ = try ms.store().put(a1_send_hash, "alpha content from client\n");
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    // Round 1: gimme held content → one send appended this round.
    const r1 = try client.consumeReply(std.testing.allocator, ms.store(), "gimme " ++ a1_send_hash ++ "\n");
    try std.testing.expect(r1.content_queued);
    try std.testing.expectEqual(@as(usize, 1), client.send_queue.count());
    // Round 2: a gimme-free reply appends nothing this round; the per-round
    // flag is false, yet the per-session buffer persists (§3.1 L312-314).
    const r2 = try client.consumeReply(std.testing.allocator, ms.store(), "# timestamp 2026-07-02T18:31:13 errors 0\n");
    try std.testing.expect(!r2.content_queued);
    try std.testing.expectEqual(@as(usize, 1), client.send_queue.count());
}

test "sync §3.7(b)/§3.1 L312-314: drainSendQueue empties the per-session buffer and returns its rids" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const rid1 = try ms.store().put(a1_send_hash, "alpha content from client\n");
    const rid2 = try ms.store().put(a1_manifest_hash, "manifest payload\n");
    var client = Client{ .operation = .push, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    _ = try client.consumeReply(std.testing.allocator, ms.store(), "gimme " ++ a1_send_hash ++ "\ngimme " ++ a1_manifest_hash ++ "\n");
    try std.testing.expectEqual(@as(usize, 2), client.send_queue.count());
    var out: [4]repostate.Rid = undefined;
    const n = client.drainSendQueue(&out);
    try std.testing.expectEqual(@as(usize, 2), n);
    try std.testing.expectEqual(@as(usize, 0), client.send_queue.count());
    // Order-agnostic membership: both queued rids come back.
    var seen1 = false;
    var seen2 = false;
    for (out[0..n]) |r| {
        if (r == rid1) seen1 = true;
        if (r == rid2) seen2 = true;
    }
    try std.testing.expect(seen1 and seen2);
}

test "sync §3.7(b)/§A.1 L2491-2492: a queued gimme uploads its artifact end-to-end" {
    // §A.1 — the client holds alpha.txt (b1c1b05f…, "alpha content from
    // client\n", 26 bytes, L4153); on a push the server gimmes it (L4109), the
    // client queues it, then uploads it as the round-2 file card (L4131-4132).
    // The upload is modelled as a store-to-store transfer of the drained rid.
    const payload = "alpha content from client\n";
    try std.testing.expectEqual(@as(usize, 26), payload.len);
    var client_store = repostate.MemStore.init(std.testing.allocator);
    defer client_store.deinit();
    _ = try client_store.store().put(a1_send_hash, payload);
    var server_store = repostate.MemStore.init(std.testing.allocator);
    defer server_store.deinit();
    var client = Client{ .operation = .push, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    _ = try client.consumeReply(std.testing.allocator, client_store.store(), "gimme " ++ a1_send_hash ++ "\n");
    var out: [1]repostate.Rid = undefined;
    const n = client.drainSendQueue(&out);
    try std.testing.expectEqual(@as(usize, 1), n);
    // Emit the file card: read the drained rid's hash + content and store it
    // at the server, exactly the bytes the round-2 file card carries.
    const rid = out[0];
    const hash = client_store.store().hashOf(rid);
    const content = client_store.store().content(rid).?;
    _ = try server_store.store().put(hash, content);
    const server_rid = server_store.store().lookup(a1_send_hash).?;
    try std.testing.expectEqualStrings(payload, server_store.store().content(server_rid).?);
}

/// §5.1 L935 + §5.2 L944-945 — cards are LF-terminated canonical lines, so a
/// request is asserted by card-SET membership: `line` must appear as a complete
/// LF-framed line of `body`. (Sufficient for the file-card-free requests below.)
fn expectHasLine(body: []const u8, line: []const u8) !void {
    var it = std.mem.splitScalar(u8, body, '\n');
    while (it.next()) |l| {
        if (std.mem.eql(u8, l, line)) return;
    }
    return error.LineNotFound;
}

test "sync §A.1 L2448-2453: round-1 buildRequest announces every holding with no gimme" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    // §A.1 L2451-2453 — the three artifacts the client holds before the sync.
    _ = try ms.store().put(a1_manifest_hash, "manifest");
    _ = try ms.store().put(a1_send_hash, "alpha");
    _ = try ms.store().put(a1_held_hash, "ancestor");
    var client = Client{
        .operation = .sync,
        .self_servercode = a1_servercode,
        .project_code = a1_projectcode,
        .version = .{ .version = 22800, .datetime = .{ .date = 20260311, .time = 113146 } },
    };
    defer client.deinit(std.testing.allocator);
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(std.testing.allocator);
    const body = try client.buildRequest(std.testing.allocator, ms.store(), &out);

    // §3.2 r22 — the client-version pragma is the first line (§A.1 L2448). The
    // VERSION is what `announcedVersion` raises it to (submanifests §7.2 — zit
    // reads trees, so it announces the threshold); DATE and TIME are the
    // client's own, unchanged.
    try std.testing.expect(std.mem.startsWith(u8, body, "pragma client-version " ++
        std.fmt.comptimePrint("{d}", .{sync.submanifest_min_peer_version}) ++ " 20260311 113146\n"));
    // §A.1 L2449-2450 — a bidirectional sync carries both pull and push cards.
    try expectHasLine(body, "pull " ++ a1_servercode ++ " " ++ a1_projectcode);
    try expectHasLine(body, "push " ++ a1_servercode ++ " " ++ a1_projectcode);
    // §A.1 L2451-2453 — one igot per held artifact, and no gimme (no phantoms).
    try expectHasLine(body, "igot " ++ a1_manifest_hash);
    try expectHasLine(body, "igot " ++ a1_send_hash);
    try expectHasLine(body, "igot " ++ a1_held_hash);
    try std.testing.expect(std.mem.indexOf(u8, body, "gimme ") == null);
}

test "sync §3.2: a pull/push request still announces held artifacts with igot" {
    // The default path is unchanged: a receive-only pull and a send-only push
    // each announce every held artifact with igot (§3.2 L397-398), live under §8.5
    // L1480-1481 — the clone suppression must not touch a non-clone operation.
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    _ = try ms.store().put(a1_manifest_hash, "manifest");
    _ = try ms.store().put(a1_send_hash, "alpha");
    inline for (.{ Operation.pull, Operation.push }) |op| {
        var client = Client{
            .operation = op,
            .self_servercode = a1_servercode,
            .project_code = a1_projectcode,
        };
        defer client.deinit(gpa);
        var out: std.ArrayListUnmanaged(u8) = .empty;
        defer out.deinit(gpa);
        const body = try client.buildRequest(gpa, ms.store(), &out);
        try expectHasLine(body, "igot " ++ a1_manifest_hash);
        try expectHasLine(body, "igot " ++ a1_send_hash);
    }
}

test "sync §8.2: a clone request carries no igot cards" {
    // §8.2 L1587-1594 — a clone session enables no push direction, so a client
    // creates no server phantom by announcing and MUST NOT be required to: even a
    // mid-clone client already holding the artifacts paged in from earlier cfile
    // batches emits zero igot cards. A continuation round carries only its
    // `clone 3 SEQNO` operation card (plus pragma/gimme/randomness); a cleanup
    // round (§8.2 L1543-1547, SEQNO 0) carries no operation card and, despite the
    // L1422 "remaining-work" permission, still no igot.
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    // Artifacts the client received in earlier clone rounds; a pull/push/sync
    // would announce each with igot (see the preceding test).
    _ = try ms.store().put(a1_manifest_hash, "manifest");
    _ = try ms.store().put(a1_send_hash, "alpha");
    _ = try ms.store().put(a1_held_hash, "ancestor");
    // continuation round (SEQNO 5) and cleanup round (SEQNO 0) — neither igots.
    inline for (.{ 5, 0 }) |seqno| {
        var client = Client{ .operation = .clone, .state = .exchange, .clone_seqno = seqno };
        defer client.deinit(gpa);
        var out: std.ArrayListUnmanaged(u8) = .empty;
        defer out.deinit(gpa);
        const body = try client.buildRequest(gpa, ms.store(), &out);
        // No igot card in any position — not at body start, not after any LF.
        try std.testing.expect(!std.mem.startsWith(u8, body, "igot "));
        try std.testing.expect(std.mem.indexOf(u8, body, "\nigot ") == null);
        // None of the held hashes appears at all (no igot smuggled another way).
        try std.testing.expect(std.mem.indexOf(u8, body, a1_manifest_hash) == null);
        try std.testing.expect(std.mem.indexOf(u8, body, a1_send_hash) == null);
        try std.testing.expect(std.mem.indexOf(u8, body, a1_held_hash) == null);
    }
    // The continuation round still threads its cursor card (§8.2 L1583).
    var cont = Client{ .operation = .clone, .state = .exchange, .clone_seqno = 5 };
    defer cont.deinit(gpa);
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);
    try expectHasLine(try cont.buildRequest(gpa, ms.store(), &out), "clone 3 5");
}

test "sync §3.3 L537: an interned phantom emits gimme, then igot once content arrives" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    _ = try ms.store().intern(a1_send_hash); // a phantom: content absent (§3.3 r24)
    var client = Client{ .operation = .sync };
    defer client.deinit(std.testing.allocator);
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(std.testing.allocator);

    // Phantom present ⇒ the client requests it with gimme, and does not igot it.
    const r1 = try client.buildRequest(std.testing.allocator, ms.store(), &out);
    try expectHasLine(r1, "gimme " ++ a1_send_hash);
    try std.testing.expect(std.mem.indexOf(u8, r1, "igot " ++ a1_send_hash) == null);

    // §3.3 L537 — storing content dephantomizes: now igot, no gimme.
    _ = try ms.store().put(a1_send_hash, "alpha content from client\n");
    const r2 = try client.buildRequest(std.testing.allocator, ms.store(), &out);
    try expectHasLine(r2, "igot " ++ a1_send_hash);
    try std.testing.expect(std.mem.indexOf(u8, r2, "gimme " ++ a1_send_hash) == null);
}

test "sync §6.1 L1073-1081 / §6.2 §6.4: a named credential emits a verifiable login card, anonymous none" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    _ = try ms.store().put(a1_send_hash, "alpha");
    const secret: [40]u8 = "0123456789abcdef0123456789abcdef01234567".*;
    var named = Client{
        .operation = .sync,
        .self_servercode = a1_servercode,
        .project_code = a1_projectcode,
        .version = .{ .version = 22800, .datetime = .{ .date = 20260311, .time = 113146 } },
        .credential = .{ .user = "drh", .secret_hex = secret },
    };
    defer named.deinit(std.testing.allocator);
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(std.testing.allocator);
    const body = try named.buildRequest(std.testing.allocator, ms.store(), &out);

    // §6.2 — NONCE = SHA1(body bytes after the login LF); §6.4 SIGNATURE =
    // SHA1(NONCE || secret). Recompute over the actual tail and confirm the
    // emitted card matches, pinning the derivation to the RFC primitives.
    const login_start = std.mem.indexOf(u8, body, "\nlogin drh ").? + 1;
    const login_end = std.mem.indexOfScalarPos(u8, body, login_start, '\n').?;
    const tail = body[login_end + 1 ..];
    const nonce = computeNonce(tail);
    const signature = computeSignature(&nonce, &secret);
    var want_buf: [128]u8 = undefined;
    const want = try std.fmt.bufPrint(&want_buf, "login drh {s} {s}", .{ &nonce, &signature });
    try expectHasLine(body, want);

    // §6.1 L1079-1080 — an anonymous session emits no login card at all.
    var anon = Client{ .operation = .sync, .self_servercode = a1_servercode, .project_code = a1_projectcode, .version = .{ .version = 22800 } };
    defer anon.deinit(std.testing.allocator);
    var anon_out: std.ArrayListUnmanaged(u8) = .empty;
    defer anon_out.deinit(std.testing.allocator);
    const anon_body = try anon.buildRequest(std.testing.allocator, ms.store(), &anon_out);
    try std.testing.expect(std.mem.indexOf(u8, anon_body, "login ") == null);
}

test "sync §5.3 L1057-1058: round_comment closes the request body after the content cards" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    _ = try ms.store().put(a1_send_hash, "alpha");
    var client = Client{ .operation = .sync, .round_comment = "deadbeef" };
    defer client.deinit(std.testing.allocator);
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(std.testing.allocator);
    const body = try client.buildRequest(std.testing.allocator, ms.store(), &out);
    try std.testing.expect(std.mem.endsWith(u8, body, "\n# deadbeef\n"));
}

fn consumeGimmeQueuesSend(gpa: std.mem.Allocator) !void {
    // Held-content gimme under a push queues exactly one send. The store lives
    // on the base allocator so only consumeReply/deinit see the failing one
    // (mirrors roundTablesNoLeak), covering the send_queue.getOrPut allocation.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    _ = try ms.store().put(a1_send_hash, "alpha content from client\n");
    var client = Client{ .operation = .push, .state = .exchange };
    defer client.deinit(gpa);
    _ = try client.consumeReply(gpa, ms.store(), "gimme " ++ a1_send_hash ++ "\n");
    try std.testing.expectEqual(@as(usize, 1), client.send_queue.count());

    // §7.5 store path (private file with grant) under the same failing
    // allocator: exercises the store-gate branch and phantom remove.
    var private_ms = repostate.MemStore.init(std.testing.allocator);
    defer private_ms.deinit();
    var granted = Client{ .operation = .sync, .state = .exchange, .private_granted = true };
    defer granted.deinit(gpa);
    _ = try granted.consumeReply(gpa, private_ms.store(), "private\nfile " ++ test_hash_a ++ " 5\nHELLO");

    // §7.5 private-igot getPtr / public-igot getOrPut branches: the public igot
    // allocates the phantom entry (the failing allocation), the private igot
    // marks it via getPtr (allocation-free).
    var mark_ms = repostate.MemStore.init(std.testing.allocator);
    defer mark_ms.deinit();
    var marker = Client{ .operation = .sync, .state = .exchange };
    defer marker.deinit(gpa);
    _ = try marker.consumeReply(gpa, mark_ms.store(), "igot " ++ test_hash_a ++ "\nigot " ++ test_hash_a ++ " 1\n");
    try std.testing.expectEqual(@as(usize, 1), marker.phantoms.count());
}

test "sync §3.7(b)/§7.5: consumeReply queues a send and lands private content under allocation failure" {
    try std.testing.checkAllAllocationFailures(std.testing.allocator, consumeGimmeQueuesSend, .{});
}

// §11.2 — the encoded `pull only ` payload (SP -> \s) and its non-matching
// counterpart, as they arrive on the wire (App. B.8 message-card grammar).
const msg_pull_only = "message pull\\sonly\\s\n";
const msg_non_match = "message hello\n";
// A held hash the server can gimme back; an unheld hash an igot phantoms.
const held_gimme = "gimme " ++ a1_send_hash ++ "\n";
const unheld_igot = "igot " ++ a1_manifest_hash ++ "\n";

test "sync §11.2 r3: a message card does not abort or error the exchange" {
    // §11.2 preamble — "neither peer ... aborts the exchange in response to
    // this card alone". A well-formed two-token card, matching or non-matching,
    // dispatches without a fatal. Round termination under §3.7 is orthogonal;
    // the card never reaches `.aborted`. (Malformed arity aborts — covered by
    // the arity-failure tests below.)
    const replies = [_][]const u8{ msg_pull_only, msg_non_match };
    for (replies) |body| {
        var ms = repostate.MemStore.init(std.testing.allocator);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange };
        defer client.deinit(std.testing.allocator);
        _ = try client.consumeReply(std.testing.allocator, ms.store(), body);
        try std.testing.expect(client.state != .aborted);
    }
}

test "sync §11.2: a one-token message card is rejected as an arity failure" {
    // §10.2 L2118-2119 — "Exactly two visible tokens are required", so not "whitespace-
    // separated tokens". A bare `message` with no TEXT is a defined-operator
    // arity failure (§5.2 L976-978), which the client records as a local
    // warning and aborts on (§5.2 L1024-1027, §3.9 L804-806) — not a silent
    // continue. The "never aborts" disposition governs only a well-formed card.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    try std.testing.expectError(error.FatalProtocolError, client.consumeReply(std.testing.allocator, ms.store(), "message\n"));
    try std.testing.expectEqual(ClientState.aborted, client.state);
}

test "sync §11.2: a three-token message card is rejected as an arity failure" {
    // §10.2 L2118 — exactly two visible tokens. `message pull only` exposes
    // three visible tokens (operator + two args), a defined-operator arity
    // failure (§5.2 L976-978) that aborts (§5.2 L1024-1027). Distinct from a
    // §5 surplus (>6 tokens) rejection, which classifyCard fires pre-dispatch.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    try std.testing.expectError(error.FatalProtocolError, client.consumeReply(std.testing.allocator, ms.store(), "message pull only\n"));
    try std.testing.expectEqual(ClientState.aborted, client.state);
}

test "sync §11.2 r15: the message arm no longer returns NotImplemented" {
    // §11.2 REWORK — the .message dispatch arm carries the full disposition;
    // it must not fall into the out-of-scope NotImplemented fence.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    _ = try client.consumeReply(std.testing.allocator, ms.store(), msg_non_match);
}

test "sync §11.2 r2: consumeReply defossilizes TEXT before the pull only match" {
    // §11.2 preamble — decoding precedes matching. The raw token
    // `pull\sonly\sfor\snow` does not start with the literal prefix; only its
    // decoded form `pull only for now` matches and clears push.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    try std.testing.expect(client.pushActive());
    _ = try client.consumeReply(std.testing.allocator, ms.store(), "message pull\\sonly\\sfor\\snow\n");
    try std.testing.expect(!client.pushActive());
}

test "sync §11.2 r4/r5: a matching pull only * message silently clears push intent" {
    // §11.2 item 1 — push live + payload matches -> clear SYNC_PUSH, no abort.
    // The sans-IO layer performs no display, so r5 suppression holds trivially.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    _ = try client.consumeReply(std.testing.allocator, ms.store(), msg_pull_only);
    try std.testing.expect(!client.pushActive());
    try std.testing.expect(client.state != .aborted);
}

test "sync §11.2 r6: cleared push intent persists across rounds" {
    // §11.2 item 1 — "a later round MUST NOT reinstate push based on this card
    // alone". Round 1 (igot keeps the round alive) clears push; round 2's gimme
    // for held content therefore queues nothing.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    _ = try ms.store().put(a1_send_hash, "alpha content from client\n");
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    _ = try client.consumeReply(std.testing.allocator, ms.store(), unheld_igot ++ msg_pull_only);
    try std.testing.expectEqual(ClientState.exchange, client.state);
    try std.testing.expect(!client.pushActive());
    const r2 = try client.consumeReply(std.testing.allocator, ms.store(), held_gimme);
    try std.testing.expect(!r2.content_queued);
    try std.testing.expectEqual(@as(usize, 0), client.send_queue.count());
    try std.testing.expect(!client.pushActive());
}

test "sync §11.2 r9: a non-matching message changes no per-session flag" {
    // §11.2 item 2 — "Non-matching payloads MUST NOT change any per-session
    // flag". Push stays live, so a following-round gimme still queues.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    _ = try ms.store().put(a1_send_hash, "alpha content from client\n");
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    _ = try client.consumeReply(std.testing.allocator, ms.store(), unheld_igot ++ msg_non_match);
    try std.testing.expectEqual(ClientState.exchange, client.state);
    try std.testing.expect(client.pushActive());
    const r2 = try client.consumeReply(std.testing.allocator, ms.store(), held_gimme);
    try std.testing.expect(r2.content_queued);
    try std.testing.expectEqual(@as(usize, 1), client.send_queue.count());
    try std.testing.expect(client.pushActive());
}

test "sync §11.2 r10/r12: a gimme before the clearing message keeps its queued send" {
    // §11.2 item 3 — "A gimme HASH that appears earlier ... is handled before
    // the clear fires ... that append is retained". The clear rewinds nothing.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    _ = try ms.store().put(a1_send_hash, "alpha content from client\n");
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    _ = try client.consumeReply(std.testing.allocator, ms.store(), held_gimme ++ msg_pull_only);
    try std.testing.expectEqual(@as(usize, 1), client.send_queue.count());
    try std.testing.expect(!client.pushActive());
}

test "sync §11.2 r11: a gimme after the clearing message is not queued" {
    // §11.2 item 3a — "once SYNC_PUSH is clear it MUST NOT" append a responsive
    // content card for a later gimme in the same reply.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    _ = try ms.store().put(a1_send_hash, "alpha content from client\n");
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    _ = try client.consumeReply(std.testing.allocator, ms.store(), msg_pull_only ++ held_gimme);
    try std.testing.expectEqual(@as(usize, 0), client.send_queue.count());
    try std.testing.expect(!client.pushActive());
}

test "sync §11.2 r13: clearing push leaves pull-side obtain intent unaffected" {
    // §11.2 item 3 — "Per-session flags other than SYNC_PUSH are unaffected".
    // A later igot in the same reply still creates its pull-side phantom.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), msg_pull_only ++ unheld_igot);
    try std.testing.expect(obs.phantom_created);
    try std.testing.expectEqual(@as(usize, 1), client.phantoms.count());
    try std.testing.expect(!client.pushActive());
}

test "sync §11.2 r14: a matching message under pure pull falls through to advisory" {
    // §11.2 item 3e — SYNC_PUSH already clear (pure pull) excludes item 1; the
    // match is advisory, sets no flag, and never aborts.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .pull, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    try std.testing.expect(!client.pushActive());
    _ = try client.consumeReply(std.testing.allocator, ms.store(), msg_pull_only);
    try std.testing.expect(!client.push_cleared);
    try std.testing.expect(client.state != .aborted);
}

fn consumeMessageClearsPush(gpa: std.mem.Allocator) !void {
    // §11.2 r2 — the message arm's defossilize scratch is the new alloc site.
    // The store lives on the base allocator so only consumeReply sees the
    // failing one (mirrors consumeGimmeQueuesSend).
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    _ = try client.consumeReply(gpa, ms.store(), msg_pull_only);
    try std.testing.expect(!client.pushActive());
}

test "sync §11.2 r2: consumeReply handles a message card under allocation failure" {
    try std.testing.checkAllAllocationFailures(std.testing.allocator, consumeMessageClearsPush, .{});
}

fn nonWhitespaceByte(rand: std.Random) u8 {
    // Keep the fuzzed token to one whitespace-free field so it stays a
    // two-token card (§11.2 r1): no §5 surplus, so the abort path is never the
    // subject under test — only the defossilize + GLOB disposition is.
    while (true) {
        const b = rand.int(u8);
        if (!sync.isFossilWhitespace(b) and b != '\n') return b;
    }
}

test "sync §11.2 fuzz: a message card decodes+matches without ever aborting" {
    // Invariant (§11.2 item 1/2): after a single two-token message card, the
    // client clears push iff the DECODED payload matches `pull only ` — never
    // otherwise, and never a fatal abort. Fixed seed for exact replay.
    var prng = std.Random.DefaultPrng.init(0x11_2_FADE);
    const rand = prng.random();
    var tok: [40]u8 = undefined;
    var card: [128]u8 = undefined;
    var dec: [40]u8 = undefined;
    var round: usize = 0;
    while (round < 512) : (round += 1) {
        // n >= 1: an empty token would collapse `message ` to one visible token,
        // now a §11.2 arity abort — out of this invariant's two-token scope.
        const n = 1 + rand.uintLessThan(usize, tok.len);
        for (tok[0..n]) |*b| b.* = nonWhitespaceByte(rand);
        const prefix = "message ";
        @memcpy(card[0..prefix.len], prefix);
        @memcpy(card[prefix.len..][0..n], tok[0..n]);
        card[prefix.len + n] = '\n';
        const body = card[0 .. prefix.len + n + 1];
        const decoded = artifact.decodeEscapes(tok[0..n], &dec);
        var ms = repostate.MemStore.init(std.testing.allocator);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange };
        defer client.deinit(std.testing.allocator);
        _ = client.consumeReply(std.testing.allocator, ms.store(), body) catch |e| {
            try std.testing.expect(e == error.OutOfMemory);
            continue;
        };
        try std.testing.expect(client.state != .aborted);
        try std.testing.expectEqual(matchesPullOnly(decoded), client.push_cleared);
    }
}

test "sync §3.3 r25: igot creates a phantom only when unheld and obtainable" {
    const Case = struct { side: Side, op: Operation, already_holds: bool, want: IgotDisposition };
    const cases = [_]Case{
        .{ .side = .client, .op = .pull, .already_holds = false, .want = .create_phantom },
        .{ .side = .client, .op = .pull, .already_holds = true, .want = .ignore },
        .{ .side = .client, .op = .clone, .already_holds = false, .want = .create_phantom },
        .{ .side = .client, .op = .push, .already_holds = false, .want = .ignore },
        .{ .side = .server, .op = .push, .already_holds = false, .want = .create_phantom },
        .{ .side = .server, .op = .pull, .already_holds = false, .want = .ignore },
        .{ .side = .client, .op = .sync, .already_holds = false, .want = .create_phantom },
        .{ .side = .server, .op = .sync, .already_holds = false, .want = .create_phantom },
    };
    for (cases) |c| try std.testing.expectEqual(c.want, igotDisposition(c.side, c.op, c.already_holds));
}

test "sync §3.3 (a): peer-lacks hashes are excluded from gimme (rejection)" {
    try std.testing.expect(!gimmeEligible(.{ .peer_lacks = true, .shunned = false, .private = false, .private_sync_enabled = false }));
    try std.testing.expect(gimmeEligible(.{ .peer_lacks = false, .shunned = false, .private = false, .private_sync_enabled = false }));
}

test "sync §3.3 (b): shunned hashes are excluded from gimme (rejection)" {
    try std.testing.expect(!gimmeEligible(.{ .peer_lacks = false, .shunned = true, .private = false, .private_sync_enabled = false }));
}

test "sync §3.3 (c): private hashes excluded unless private sync enabled (rejection)" {
    try std.testing.expect(!gimmeEligible(.{ .peer_lacks = false, .shunned = false, .private = true, .private_sync_enabled = false }));
    try std.testing.expect(gimmeEligible(.{ .peer_lacks = false, .shunned = false, .private = true, .private_sync_enabled = true }));
}

test "sync §3.3: receiving gimme records the peer lacks the hash" {
    var rt = RoundTables.init(std.testing.allocator);
    defer rt.deinit();
    try rt.recordPeerLacks(test_hash_a);
    try std.testing.expect(rt.peerLacks(test_hash_a));
    try std.testing.expect(!gimmeEligible(.{ .peer_lacks = true, .shunned = false, .private = false, .private_sync_enabled = false }));
}

test "sync §3.3: held + sendable + disclosable content is sent" {
    try std.testing.expectEqual(GimmeResponse.send, gimmeResponse(true, true, true));
    try std.testing.expectEqual(GimmeResponse.silent_skip, gimmeResponse(true, true, false));
}

test "sync §3.3: held but direction forbids sending is a silent no-send (rejection)" {
    try std.testing.expectEqual(GimmeResponse.silent_skip, gimmeResponse(true, false, true));
    try std.testing.expectEqual(GimmeResponse.silent_skip, gimmeResponse(true, false, false));
}

test "sync §3.3: gimme for unheld content is silently skipped (rejection)" {
    try std.testing.expectEqual(GimmeResponse.silent_skip, gimmeResponse(false, true, true));
    try std.testing.expectEqual(GimmeResponse.silent_skip, gimmeResponse(false, false, false));
}

test "sync §3.3 r24: a phantom is a hash known with content absent (store contract)" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const store = ms.store();
    const rid = try store.intern(test_hash_a);
    try std.testing.expectEqual(@as(?[]const u8, null), store.content(rid));
    _ = try store.put(test_hash_a, "payload");
    try std.testing.expect(store.content(rid) != null);
}

test "sync §3.3 r36: storing content dephantomizes (store contract)" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const store = ms.store();
    const rid = try store.intern(test_hash_b);
    try std.testing.expectEqual(@as(?[]const u8, null), store.content(rid));
    _ = try store.put(test_hash_b, "content");
    try std.testing.expect(store.content(rid) != null);
}

test "sync §3.2 r22: a request missing client-version is rejected" {
    try std.testing.expectEqual(RequestOrder.missing_client_version, checkRequestOrder(&.{}));
    try std.testing.expectEqual(RequestOrder.missing_client_version, checkRequestOrder(&.{.content_request}));
    try std.testing.expectEqual(RequestOrder.ok, checkRequestOrder(&.{.client_version}));
}

test "sync §3.2 r23: client-version must precede content-requesting cards" {
    try std.testing.expectEqual(RequestOrder.version_after_content_request, checkRequestOrder(&.{ .content_request, .client_version }));
    try std.testing.expectEqual(RequestOrder.ok, checkRequestOrder(&.{ .client_version, .content_request }));
}

test "sync §3.2 r23: send-private/login before client-version is accepted" {
    try std.testing.expectEqual(RequestOrder.ok, checkRequestOrder(&.{ .other, .client_version, .content_request }));
}

test "sync §8.1: clone card carries VERSION SEQNO, VERSION decimal 3" {
    var buf: [32]u8 = undefined;
    try std.testing.expectEqualStrings("clone 3 1", try writeCloneCard(1, &buf));
    try std.testing.expectEqual(@as(u8, 3), clone_version);
}

test "sync §8.1: modern clone request and x-fossil-uncompressed reply (worked example)" {
    var buf: [32]u8 = undefined;
    try std.testing.expectEqualStrings("clone 3 7", try writeCloneCard(7, &buf));
    // Server-side cfile emission over storage-sequence order needs an
    // ArtifactStore iterator and is deferred; the reply media type it
    // uses is already on record here.
    const props = media_props.get(.x_fossil_uncompressed);
    try std.testing.expect(props.clone_reply_only);
    try std.testing.expect(!props.outer_compressed);
    try std.testing.expectEqualStrings("application/x-fossil-uncompressed", props.wire);
}

test "sync §8.1: clone version 3 orthogonal to the §7.6 release-integer gate" {
    var buf: [32]u8 = undefined;
    try std.testing.expect(@as(u32, clone_version) != sha3_min_peer_version);
    try std.testing.expectEqualStrings("clone 3 1", try writeCloneCard(1, &buf));
    // A release well below the SHA3 gate's floor still speaks clone 3.
    const sha3_hex = "a" ** 64;
    try std.testing.expectEqual(GateDecision.refuse, sha3PeerVersionGate(15000, sha3_hex));
}

test "sync §8.2: continuing clone request carries clone 3 NEXT" {
    var buf: [32]u8 = undefined;
    try std.testing.expectEqual(NextRequest.clone_card, nextRequestKind(true, 5));
    try std.testing.expectEqualStrings("clone 3 5", try writeCloneCard(5, &buf));
}

test "sync §8.2: client MUST NOT send a clone card after clone_seqno 0 (rejection)" {
    try std.testing.expectEqual(NextRequest.cleanup_round, nextRequestKind(true, 0));
    try std.testing.expect(NextRequest.clone_card != nextRequestKind(true, 0));
}

test "sync §8.2: a cleanup round omits the clone card; empty work terminates" {
    try std.testing.expectEqual(NextRequest.cleanup_round, nextRequestKind(true, 0));
    try std.testing.expectEqual(NextRequest.stop, nextRequestKind(false, 0));
}

test "sync §8.2: server MUST advance when NEXT>0 and RIDs>=NEXT unsent (rejection)" {
    const Case = struct { next: i64, unsent: bool, want: bool };
    const cases = [_]Case{
        .{ .next = 5, .unsent = true, .want = true },
        .{ .next = 10, .unsent = false, .want = false },
        .{ .next = 0, .unsent = true, .want = false },
    };
    for (cases) |c| try std.testing.expectEqual(c.want, cloneServerMustAdvance(c.next, c.unsent));
}

test "sync §8.2: modern clone loop (worked example)" {
    var buf: [32]u8 = undefined;
    try std.testing.expectEqualStrings("clone 3 1", try writeCloneCard(1, &buf));

    // NEXT=5: continue with another clone card.
    var next = try parseCloneSeqno(tokenizeCard("clone_seqno 5"));
    try std.testing.expectEqual(@as(i64, 5), next);
    try std.testing.expectEqual(NextRequest.clone_card, nextRequestKind(true, next));
    try std.testing.expectEqualStrings("clone 3 5", try writeCloneCard(@intCast(next), &buf));

    // NEXT=0: artifact transfer done; a cleanup round, not a clone card.
    next = try parseCloneSeqno(tokenizeCard("clone_seqno 0"));
    try std.testing.expectEqual(@as(i64, 0), next);
    try std.testing.expect(!cloneSeqnoIsFatal(next));
    try std.testing.expectEqual(NextRequest.cleanup_round, nextRequestKind(true, next));

    // No remaining work: stop.
    try std.testing.expectEqual(NextRequest.stop, nextRequestKind(false, 0));

    // A negative NEXT is the fatal path, never fed to nextRequestKind.
    next = try parseCloneSeqno(tokenizeCard("clone_seqno -1"));
    try std.testing.expectEqual(@as(i64, -1), next);
    try std.testing.expect(cloneSeqnoIsFatal(next));

    try std.testing.expectError(error.Malformed, parseCloneSeqno(tokenizeCard("clone_seqno x")));
}

test "sync §8.3: first clone round suppresses both body-login and cookie-login" {
    try std.testing.expect(!sendableAfterCloneRound0(.clone, 0));
    try std.testing.expect(sendableAfterCloneRound0(.clone, 1));
}

test "sync §8.3: first request is client-version then clone 3 1, no login (worked example)" {
    var clone_buf: [32]u8 = undefined;
    var version_buf: [64]u8 = undefined;
    try std.testing.expectEqualStrings("clone 3 1", try writeCloneCard(1, &clone_buf));
    const version_line = try writeClientVersion(.{ .version = 20000 }, &version_buf);
    try std.testing.expect(std.mem.startsWith(u8, version_line, "pragma client-version "));
    try std.testing.expect(!sendableAfterCloneRound0(.clone, 0));
}

test "sync §8.3: after clone_seqno 0 phantoms resolve via the ordinary §3.7 path" {
    // Section 8.2's "cleanup round" is, by definition, an ordinary
    // request (L1930-1934): once NEXT hits 0 the CLI runs it as a plain
    // pull round, so continuation is §3.7(a) unchanged, not a new rule.
    try std.testing.expectEqual(NextRequest.cleanup_round, nextRequestKind(true, 0));
    const pending = RoundObservation{
        .operation = .pull,
        .versioned_content_delivered = true,
        .phantom_table_nonempty = true,
    };
    try std.testing.expect(continueExchange(pending));
    const nothing_pending = RoundObservation{ .operation = .pull };
    try std.testing.expect(!continueExchange(nothing_pending));
}

test "sync §8.3/§9.1: client reads project code from a push or pull card in the reply" {
    try std.testing.expectEqualStrings("PC", parsePushPull(tokenizeCard("push SC PC")).?.projectcode);
    try std.testing.expectEqualStrings("PC", parsePushPull(tokenizeCard("pull SC PC")).?.projectcode);
    try std.testing.expectEqual(@as(?PushPullCard, null), parsePushPull(tokenizeCard("push SC")));
}

test "sync §9.1: push/pull/sync carry SERVERCODE PROJECTCODE; mismatch is fatal (rejection)" {
    var push_buf: [64]u8 = undefined;
    var pull_buf: [64]u8 = undefined;
    try std.testing.expectEqualStrings("push SC PC", try writePushPullCard(.push, "SC", "PC", &push_buf));
    try std.testing.expectEqualStrings("pull SC PC", try writePushPullCard(.pull, "SC", "PC", &pull_buf));
    // A combined sync carries both cards, each written by the one shared writer.
    try std.testing.expect(std.mem.startsWith(u8, push_buf[0..10], "push "));
    try std.testing.expect(std.mem.startsWith(u8, pull_buf[0..10], "pull "));

    try std.testing.expectEqual(SyncProjectCode.proceed, serverSyncProjectCodeDecision("PC", "PC"));
    try std.testing.expectEqual(SyncProjectCode.refuse_fatal, serverSyncProjectCodeDecision("PC", "XX"));
    try std.testing.expect(causeIsFatal(.exchange, .project_code_mismatch));
}

test "sync §9.2: private igot suppressed for peers dated before 2020-04-13 (rejection)" {
    try std.testing.expectEqual(@as(u32, 20200413), private_igot_min_peer_date);
    const Case = struct { date: ?u32, want: PrivateIgotAction };
    const cases = [_]Case{
        .{ .date = 20200412, .want = .omit },
        .{ .date = 20200413, .want = .announce_private }, // strict "before"
        .{ .date = 20200414, .want = .announce_private },
        .{ .date = 19991231, .want = .omit },
        .{ .date = null, .want = .announce_private },
    };
    for (cases) |c| try std.testing.expectEqual(c.want, privateIgotAction(c.date));
}

test "sync §4 r1: request-target is repo path unchanged; empty uses /, never /xfer" {
    const cases = [_]struct { in: []const u8, want: []const u8 }{
        .{ .in = "", .want = "/" },
        .{ .in = "/", .want = "/" },
        .{ .in = "/repo", .want = "/repo" },
        .{ .in = "/cgi-bin/fossil/x", .want = "/cgi-bin/fossil/x" },
    };
    for (cases) |c| {
        const got = syncRequestTarget(c.in);
        try std.testing.expectEqualStrings(c.want, got);
        try std.testing.expect(!std.mem.endsWith(u8, got, "/xfer"));
    }
}

test "sync §4 r2: x-fossil and -debug are valid inbound request Content-Types" {
    try std.testing.expect(media_props.get(.x_fossil).valid_request_type);
    try std.testing.expect(media_props.get(.x_fossil_debug).valid_request_type);
}

test "sync §4 r3: x-fossil-uncompressed is not a request media type" {
    try std.testing.expect(!media_props.get(.x_fossil_uncompressed).valid_request_type);
}

test "sync §4 r4: SSH transport adds X-Fossil-Transport: SSH, the only nonstandard header" {
    try std.testing.expect(needsSshTransportHeader(.ssh));
    try std.testing.expect(!needsSshTransportHeader(.http));
    try std.testing.expect(!needsSshTransportHeader(.https));
    try std.testing.expect(!needsSshTransportHeader(.file));
    try std.testing.expectEqualStrings("X-Fossil-Transport: SSH", ssh_transport_header);
}

test "sync §4 r5: a non-POST sync request is redirected home, cards not processed" {
    try std.testing.expectEqual(MethodDisposition.redirect_home, serverMethodDisposition(false));
    try std.testing.expectEqual(MethodDisposition.process, serverMethodDisposition(true));
}

test "sync §4 r7: a Content-Length reply is read exactly that many bytes" {
    try std.testing.expectEqual(ReplyFraming{ .exact = 300 }, replyFraming(300));
}

test "sync §4 r8: a reply with no Content-Length is read until close, cap 512 MiB" {
    try std.testing.expectEqual(ReplyFraming.until_close, replyFraming(null));
    try std.testing.expectEqual(@as(usize, 536_870_912), reply_body_cap);
}

test "sync §4 r9: a reply exceeding the 512 MiB cap is rejected" {
    try replyCapCheck(reply_body_cap);
    try std.testing.expectError(error.ReplyTooLarge, replyCapCheck(reply_body_cap + 1));
}

test "sync §4 r10: at most 19 HTTP redirects are followed for one exchange" {
    try std.testing.expectEqual(@as(usize, 19), max_redirects);
    try redirectAllowed(18);
}

test "sync §4 r11: the twentieth redirect aborts with a fatal redirect-limit error" {
    try std.testing.expectError(error.RedirectLimit, redirectAllowed(19));
    try std.testing.expect(causeIsFatal(.exchange, .redirect_limit));
}

test "sync §4 r12: a redirect must not downgrade HTTPS to plaintext HTTP" {
    try std.testing.expectError(error.InsecureRedirect, checkRedirectDowngrade(.https, .http));
    try checkRedirectDowngrade(.https, .https);
    try checkRedirectDowngrade(.http, .http);
}

test "sync §4 r13: SSH or local-file transports never follow a redirect" {
    try std.testing.expect(!redirectAllowedForTransport(.ssh));
    try std.testing.expect(!redirectAllowedForTransport(.file));
    try std.testing.expect(redirectAllowedForTransport(.http));
    try std.testing.expect(redirectAllowedForTransport(.https));
}

test "sync §4 r14: a redirect strips trailing /xfer segments repeatedly, origin path never consulted" {
    const cases = [_]struct { location: []const u8, want: []const u8 }{
        // clause 2 (repeated strip): /a/xfer/xfer -> /a/xfer -> /a, stop.
        .{ .location = "/a/xfer/xfer", .want = "/a" },
        // clause 1 (single MUST strip): /mirror/xfer -> /mirror, stop.
        .{ .location = "https://b.example/mirror/xfer", .want = "https://b.example/mirror" },
        // clause 3 (verbatim): no trailing /xfer segment.
        .{ .location = "/mirror/proj", .want = "/mirror/proj" },
        // clause 4 (origin path never consulted): the old suffix-match
        // code stripped /proj against an origin /repo/proj; the new rule
        // has no origin arg and /proj != /xfer, so it's left verbatim.
        .{ .location = "https://b.example/mirror/proj", .want = "https://b.example/mirror/proj" },
        // no path component present: nothing strippable.
        .{ .location = "https://b.example", .want = "https://b.example" },
        // clause 1 at path root: authority preserved by the path-start floor.
        .{ .location = "https://b.example/xfer", .want = "https://b.example" },
        // authority guard: "xfer" is the host, not a path segment.
        .{ .location = "https://xfer", .want = "https://xfer" },
    };
    for (cases) |c| {
        try std.testing.expectEqualStrings(c.want, stripRedirectXfer(c.location));
    }
}

test "sync §4.1 r15: x-fossil is the compressed all-requests/pull-push-sync-reply type" {
    const p = media_props.get(.x_fossil);
    try std.testing.expectEqualStrings("application/x-fossil", p.wire);
    try std.testing.expect(p.outer_compressed);
    try std.testing.expect(p.valid_request_type);
}

test "sync §4.1 r16: x-fossil-debug is uncompressed and not for production" {
    const p = media_props.get(.x_fossil_debug);
    try std.testing.expectEqualStrings("application/x-fossil-debug", p.wire);
    try std.testing.expect(!p.outer_compressed);
    try std.testing.expect(!p.production_allowed);
}

test "sync §4.1 r17: x-fossil-uncompressed is clone-reply-only, not outer-zlib" {
    const p = media_props.get(.x_fossil_uncompressed);
    try std.testing.expectEqualStrings("application/x-fossil-uncompressed", p.wire);
    try std.testing.expect(p.clone_reply_only);
    try std.testing.expect(!p.outer_compressed);
    try std.testing.expect(!p.valid_request_type);
}

test "sync §4.1: parseMediaType inverts the media_props table" {
    for (std.meta.tags(MediaType)) |v| {
        try std.testing.expectEqual(v, parseMediaType(media_props.get(v).wire).?);
    }
    try std.testing.expect(parseMediaType("text/plain") == null);
}

test "sync §4.2 r22: a round is one POST+response ending at body exhaustion" {
    const body = "cookie x\ngimme " ++ ("a" ** 40) ++ "\n";
    try std.testing.expectEqual(ReplyBodyStep.round_complete, replyBodyStep(body, body.len));
    try std.testing.expectEqual(ReplyBodyStep.card_ready, replyBodyStep(body, 0));
}

test "sync §4.2 r23: body exhaustion at a card boundary is not an error" {
    const body = "cookie x\n";
    try std.testing.expectEqual(ReplyBodyStep.round_complete, replyBodyStep(body, body.len));
    const partial = "cookie x";
    try std.testing.expectEqual(ReplyBodyStep.truncated, replyBodyStep(partial, 0));
}

test "sync §4.3: the cookie card is one opaque TEXT, in a request or a response" {
    var buf: [24]u8 = undefined;
    const written = writeCookieCard("s3cr3t", &buf);
    try std.testing.expectEqualStrings("cookie s3cr3t", written);

    const c = classifyCard(written);
    try std.testing.expectEqual(Disposition.dispatch, c.disposition);

    var store: CookieStore = .{};
    var decode_buf: [16]u8 = undefined;
    const decoded = parseSignalText(tokenizeCard(written), &decode_buf).?;
    store.store(decoded);
    try std.testing.expectEqualStrings("s3cr3t", store.value().?);

    // Same shape either direction: the server's emit and the client's
    // return are both exactly this card.
    var buf2: [24]u8 = undefined;
    try std.testing.expectEqualStrings(written, writeCookieCard(store.value().?, &buf2));
}

test "sync §A.5/§6.2 r5/§11.1-r2: computeNonce('example-request-body-bytes')" {
    // §6.2 L1085-1087 defines it: the login NONCE is the SHA-1 digest
    // of the message-body bytes that follow the card — exactly this vector.
    const nonce = computeNonce("example-request-body-bytes");
    try std.testing.expectEqualStrings("5dfa03bd8645938b4e312a4fcdad5ebe6d4d4840", &nonce);
}

test "sync §A.5/§6.3 r7: deriveSharedSecret(project,alice,secret-passphrase)" {
    const secret = deriveSharedSecret("c1b2a3d4e5f60718293a4b5c6d7e8f9012345678", "alice", "secret-passphrase");
    try std.testing.expectEqualStrings("d7a235115bf97d126c42140196c4fc6087312353", &secret);
}

test "sync §A.5/§6.4 r8: computeSignature(nonce,secret)" {
    const nonce = computeNonce("example-request-body-bytes");
    const secret = deriveSharedSecret("c1b2a3d4e5f60718293a4b5c6d7e8f9012345678", "alice", "secret-passphrase");
    const signature = computeSignature(&nonce, &secret);
    try std.testing.expectEqualStrings("e3dba1c99442823b76ae64a8afca940e0327f9f8", &signature);
}

test "sync §A.5: login card vectors" {
    // Direct, non-circular pin of the sync RFC §A.5 published worked example
    // (draft-fossil-sync-protocol-00.txt). Every literal below is transcribed
    // by hand from the RFC — the RFC bytes are the oracle, never zit's own hash:
    //   L2615 USER          = alice
    //   L2617 password      = secret-passphrase
    //   L2619 project code  = c1b2a3d4e5f60718293a4b5c6d7e8f9012345678
    //   L2638 NONCE         = 5dfa03bd8645938b4e312a4fcdad5ebe6d4d4840
    //   L2641 shared-secret = d7a235115bf97d126c42140196c4fc6087312353
    //   L2643 SIGNATURE     = e3dba1c99442823b76ae64a8afca940e0327f9f8
    const secret = deriveSharedSecret(
        "c1b2a3d4e5f60718293a4b5c6d7e8f9012345678",
        "alice",
        "secret-passphrase",
    );
    try std.testing.expectEqualStrings("d7a235115bf97d126c42140196c4fc6087312353", &secret);

    // Feed §A.5's PUBLISHED NONCE and shared-secret hex literals straight in —
    // not computeNonce/deriveSharedSecret output — so this pin of the §A.5
    // SIGNATURE is anchored on the RFC's own bytes, not on any zit computation.
    const signature = computeSignature(
        "5dfa03bd8645938b4e312a4fcdad5ebe6d4d4840",
        "d7a235115bf97d126c42140196c4fc6087312353",
    );
    try std.testing.expectEqualStrings("e3dba1c99442823b76ae64a8afca940e0327f9f8", &signature);
}

test "sync §6.4 r8/r13: computeSignature is 40-hex over an 80-byte input regardless of secret width" {
    const nonce = computeNonce("example-request-body-bytes");
    const secret_hex = deriveSharedSecret("c1b2a3d4e5f60718293a4b5c6d7e8f9012345678", "alice", "secret-passphrase");
    const sig_from_hex_secret = computeSignature(&nonce, &secret_hex);
    try std.testing.expectEqualStrings("e3dba1c99442823b76ae64a8afca940e0327f9f8", &sig_from_hex_secret);

    const raw_secret = "short-raw-cred"; // arbitrary-width stored credential, primary form
    const sig_from_raw_secret = computeSignature(&nonce, raw_secret);
    try std.testing.expectEqual(@as(usize, 40), sig_from_raw_secret.len);
    try std.testing.expect(!std.mem.eql(u8, &sig_from_hex_secret, &sig_from_raw_secret));
}

test "sync §6.4/§11.1-r3: distinct body tails yield distinct nonces (replay note)" {
    // §11.1 L2170-2171: independently generated rounds produce distinct
    // nonces. Its causal mechanism — the per-request random comment line of
    // §5.3 (L1057-1058) — is an RNG/CLI-layer concern deliberately out of this
    // sans-IO scope; the co-citation covers only the distinct-tail =>
    // distinct-nonce consequence, not the randomness source.
    const n1 = computeNonce("round one body bytes");
    const n2 = computeNonce("round two body bytes");
    try std.testing.expect(!std.mem.eql(u8, &n1, &n2));
}

test "sync §11.1-r1: neither password nor shared secret appears in the login body card" {
    // §11 L2158-2159: the mechanism transmits neither the user password nor
    // the shared secret; neither is carried on the wire. The §6.1 body card's
    // fields are USER/NONCE/SIGNATURE, and SIGNATURE = SHA-1(NONCE || secret)
    // is one-way — so neither the password nor the 40-hex secret can appear.
    const secret = deriveSharedSecret("c1b2a3d4e5f60718293a4b5c6d7e8f9012345678", "alice", "secret-passphrase");
    try std.testing.expectEqualStrings("d7a235115bf97d126c42140196c4fc6087312353", &secret); // §A.5 vector
    const nonce = computeNonce("example-request-body-bytes");
    const sig = computeSignature(&nonce, &secret);

    var card_buf: [128]u8 = undefined;
    const card = try std.fmt.bufPrint(&card_buf, "login alice {s} {s}", .{ &nonce, &sig });
    try std.testing.expect(std.mem.indexOf(u8, card, "secret-passphrase") == null);
    try std.testing.expect(std.mem.indexOf(u8, card, &secret) == null);
}

test "sync §6.1: login USER NONCE SIGNATURE parses (worked card, §A.3 L2643)" {
    const line = "login privuser ee71329ef84fb77b428c6f626ad0103a426e0652 c3ee0be781c9ebcfc6e24756cbb75e6319ca255b";
    const card = parseLoginCard(tokenizeCard(line)).?;
    try std.testing.expectEqualStrings("privuser", card.user);
    try std.testing.expectEqualStrings("ee71329ef84fb77b428c6f626ad0103a426e0652", card.nonce);
    try std.testing.expectEqualStrings("c3ee0be781c9ebcfc6e24756cbb75e6319ca255b", card.signature);
}

test "sync §6.1: login card rejected — wrong token count" {
    try std.testing.expect(parseLoginCard(tokenizeCard("login onlyuser")) == null);
    try std.testing.expect(parseLoginCard(tokenizeCard("login a b c d")) == null);
}

test "sync §6.1: login card rejected — NONCE not 40 lowercase hex" {
    const sig = "c3ee0be781c9ebcfc6e24756cbb75e6319ca255b";
    const cases = [_][]const u8{
        "login u ee71329ef84fb77b428c6f626ad0103a426e065 " ++ sig, // 39 chars, too short
        "login u ee71329ef84fb77b428c6f626ad0103a426e06520 " ++ sig, // 41 chars, too long
        "login u EE71329EF84FB77B428C6F626AD0103A426E0652 " ++ sig, // uppercase
        "login u ge71329ef84fb77b428c6f626ad0103a426e0652 " ++ sig, // non-hex 'g'
    };
    for (cases) |line| {
        try std.testing.expect(parseLoginCard(tokenizeCard(line)) == null);
    }
}

test "sync §6.1: login card rejected — SIGNATURE not 40 lowercase hex" {
    const nonce = "ee71329ef84fb77b428c6f626ad0103a426e0652";
    const cases = [_][]const u8{
        "login u " ++ nonce ++ " c3ee0be781c9ebcfc6e24756cbb75e6319ca25", // 38 chars, too short
        "login u " ++ nonce ++ " c3ee0be781c9ebcfc6e24756cbb75e6319ca255bff", // 42 chars, too long
        "login u " ++ nonce ++ " C3EE0BE781C9EBCFC6E24756CBB75E6319CA255B", // uppercase
        "login u " ++ nonce ++ " z3ee0be781c9ebcfc6e24756cbb75e6319ca255b", // non-hex 'z'
    };
    for (cases) |line| {
        try std.testing.expect(parseLoginCard(tokenizeCard(line)) == null);
    }
}

test "sync §6.1: login card rejected — empty/whitespace USER" {
    const nonce = "ee71329ef84fb77b428c6f626ad0103a426e0652";
    const sig = "c3ee0be781c9ebcfc6e24756cbb75e6319ca255b";
    // Constructed directly: tokenizeCard itself never yields an empty or
    // whitespace-bearing token, so this exercises the defensive check for
    // a Tokens value assembled from a non-tokenizeCard source.
    var empty_user: Tokens = .{ .len = 4 };
    empty_user.items = .{ "login", "", nonce, sig, undefined, undefined };
    try std.testing.expect(parseLoginCard(empty_user) == null);

    var ws_user: Tokens = .{ .len = 4 };
    ws_user.items = .{ "login", "us er", nonce, sig, undefined, undefined };
    try std.testing.expect(parseLoginCard(ws_user) == null);
}

test "sync §A.3/§6.5 r?: writeCookieLogin encodes SP->+ (privuser round-2 card, §A.3 L2664)" {
    const card: LoginCard = .{
        .user = "privuser",
        .nonce = "653828a8fc753c1685501de457c104f0b20938c9",
        .signature = "bec32fbc6a2589413ee6d1f47a4ae0098dafcad2",
    };
    var buf: [128]u8 = undefined;
    const cookie = writeCookieLogin(card, &buf).?;
    try std.testing.expectEqualStrings(
        "login+privuser+653828a8fc753c1685501de457c104f0b20938c9+bec32fbc6a2589413ee6d1f47a4ae0098dafcad2",
        cookie,
    );
}

test "sync §6.5: parseCookieLogin round-trips the §A.3 L2664 x-f-l-c value" {
    const value = "login+privuser+653828a8fc753c1685501de457c104f0b20938c9+bec32fbc6a2589413ee6d1f47a4ae0098dafcad2";
    var buf: [128]u8 = undefined;
    const card = parseCookieLogin(value, &buf).?;
    try std.testing.expectEqualStrings("privuser", card.user);
    try std.testing.expectEqualStrings("653828a8fc753c1685501de457c104f0b20938c9", card.nonce);
    try std.testing.expectEqualStrings("bec32fbc6a2589413ee6d1f47a4ae0098dafcad2", card.signature);
}

test "sync §6.5: parseCookieLogin rejects a malformed value" {
    var buf: [128]u8 = undefined;
    try std.testing.expect(parseCookieLogin("login+privuser+short+bec32fbc6a2589413ee6d1f47a4ae0098dafcad2", &buf) == null);
    try std.testing.expect(parseCookieLogin("login+privuser+%zz+bec32fbc6a2589413ee6d1f47a4ae0098dafcad2", &buf) == null);
}

test "sync §11.2-r5: writeCookieLogin never places the shared secret in the x-f-l-c cookie" {
    // §6.5 L1163 (+ §11 preamble L2158-2159): the shared secret is structurally never
    // stored in a cookie. The x-f-l-c fields are login+USER+NONCE+SIGNATURE —
    // the 40-hex secret and the password are not among them. Distinct surface
    // from §11.1-r1: cookie transport, not the body card.
    const secret = deriveSharedSecret("c1b2a3d4e5f60718293a4b5c6d7e8f9012345678", "alice", "secret-passphrase");
    const nonce = computeNonce("example-request-body-bytes");
    const sig = computeSignature(&nonce, &secret);
    const card: LoginCard = .{ .user = "alice", .nonce = &nonce, .signature = &sig };

    var buf: [128]u8 = undefined;
    const cookie = writeCookieLogin(card, &buf).?;
    try std.testing.expect(std.mem.indexOf(u8, cookie, &secret) == null);
    try std.testing.expect(std.mem.indexOf(u8, cookie, "secret-passphrase") == null);
}

test "sync §6.4 step1: nobody/anonymous succeed unconditionally" {
    const dummy = "0" ** 40;
    const nobody: LoginCard = .{ .user = "nobody", .nonce = dummy, .signature = dummy };
    try std.testing.expectEqual(LoginResult.special_case, serverVerifyLogin(nobody, "x", "x", "x"));
    const anon: LoginCard = .{ .user = "anonymous", .nonce = dummy, .signature = dummy };
    try std.testing.expectEqual(LoginResult.special_case, serverVerifyLogin(anon, "x", "x", "x"));
}

test "sync §6.4 step2: primary signature check passes (stored bytes as secret) with valid tail-hash" {
    const body_tail = "some request body tail bytes";
    const nonce = computeNonce(body_tail);
    const stored_secret = "arbitrary-stored-credential-bytes";
    const signature = computeSignature(&nonce, stored_secret);
    const card: LoginCard = .{ .user = "alice", .nonce = &nonce, .signature = &signature };
    try std.testing.expectEqual(LoginResult.ok, serverVerifyLogin(card, stored_secret, "unused-project-code", body_tail));
}

test "sync §6.4 step3: stored-credential fallback re-derives only when primary fails and stored len != 40" {
    const body_tail = "another body tail";
    const nonce = computeNonce(body_tail);
    const project_code = "c1b2a3d4e5f60718293a4b5c6d7e8f9012345678";
    const login = "alice";
    const cleartext_password = "secret-passphrase"; // stored credential, len != 40
    const shared_secret = deriveSharedSecret(project_code, login, cleartext_password);
    const signature = computeSignature(&nonce, &shared_secret);
    const card: LoginCard = .{ .user = login, .nonce = &nonce, .signature = &signature };
    // The primary form (cleartext_password treated directly as the raw
    // secret) does not match; only the §6.3 fallback re-derivation does.
    try std.testing.expectEqual(LoginResult.ok, serverVerifyLogin(card, cleartext_password, project_code, body_tail));
}

test "sync §6.4: login rejected — signature mismatch" {
    const body_tail = "reject sig body";
    const nonce = computeNonce(body_tail);
    const stored_secret = "cred-bytes";
    const wrong_signature = "0" ** 40;
    const card: LoginCard = .{ .user = "alice", .nonce = &nonce, .signature = wrong_signature };
    try std.testing.expectEqual(LoginResult.reject, serverVerifyLogin(card, stored_secret, "project", body_tail));
}

test "sync §6.4: login rejected — tail-hash mismatch" {
    const original_tail = "original body tail bytes";
    const tampered_tail = "tampered body tail bytes!!";
    const nonce = computeNonce(original_tail);
    const stored_secret = "cred-bytes";
    const signature = computeSignature(&nonce, stored_secret);
    const card: LoginCard = .{ .user = "alice", .nonce = &nonce, .signature = &signature };
    // The server recomputes NONCE from the body tail it actually
    // received, which is the tampered one — signature is valid but the
    // recomputed tail hash no longer matches the carried NONCE.
    try std.testing.expectEqual(LoginResult.reject, serverVerifyLogin(card, stored_secret, "project", tampered_tail));
}

test "sync §6.4: fallback NOT attempted when stored len == 40" {
    const body_tail = "fallback-gate body";
    const nonce = computeNonce(body_tail);
    const project_code = "project-code";
    const login = "alice";
    const password_40 = "x" ** 40; // exactly 40 bytes: a cleartext-shaped credential
    const would_be_fallback_secret = deriveSharedSecret(project_code, login, password_40);
    const signature = computeSignature(&nonce, &would_be_fallback_secret);
    const card: LoginCard = .{ .user = login, .nonce = &nonce, .signature = &signature };
    // If the fallback were (incorrectly) attempted despite stored.len ==
    // 40, this signature would verify. §6.4 step 3 gates the fallback on
    // stored.len != 40, so the primary-only check must reject.
    try std.testing.expectEqual(LoginResult.reject, serverVerifyLogin(card, password_40, project_code, body_tail));
}

test "sync §11.1-r4: a byte-identical login verifies .ok twice (no nonce cache, no freshness check)" {
    // §11.1 L2171-2174: the server maintains no nonce cache and applies no
    // timestamp freshness check, so a complete request can be replayed
    // byte-for-byte and still pass. serverVerifyLogin is a pure function of its
    // inputs — identical inputs return .ok both times, pinning that property.
    const body_tail = "some request body tail bytes";
    const nonce = computeNonce(body_tail);
    const stored_secret = "arbitrary-stored-credential-bytes";
    const sig = computeSignature(&nonce, stored_secret);
    const card: LoginCard = .{ .user = "alice", .nonce = &nonce, .signature = &sig };

    const first = serverVerifyLogin(card, stored_secret, "unused-project-code", body_tail);
    const second = serverVerifyLogin(card, stored_secret, "unused-project-code", body_tail);
    try std.testing.expectEqual(LoginResult.ok, first);
    try std.testing.expectEqual(LoginResult.ok, second);
}

test "sync §6.5 r1: cookie gate requires peer VERSION >= 22700 (Fossil 2.27)" {
    const dt: DateTime = .{ .date = cookie_gate_min_date, .time = cookie_gate_min_time };
    const cases = [_]struct { version: u32, want: bool }{
        .{ .version = 22699, .want = false },
        .{ .version = 22700, .want = true },
        .{ .version = 22701, .want = true },
    };
    for (cases) |c| {
        const peer: Peer = .{ .version = c.version, .datetime = dt };
        try std.testing.expectEqual(c.want, peerGatedForCookie(peer));
    }
}

test "sync §6.5 r2: cookie gate requires timestamp on or after 2025-07-27 11:05:00" {
    const cases = [_]struct { date: u32, time: u32, want: bool }{
        .{ .date = 20250726, .time = 999999, .want = false },
        .{ .date = 20250727, .time = 110459, .want = false },
        .{ .date = 20250727, .time = 110500, .want = true },
        .{ .date = 20250727, .time = 110501, .want = true },
        .{ .date = 20250728, .time = 0, .want = true },
    };
    for (cases) |c| {
        const peer: Peer = .{ .version = cookie_gate_min_version, .datetime = .{ .date = c.date, .time = c.time } };
        try std.testing.expectEqual(c.want, peerGatedForCookie(peer));
    }
}

test "sync §6.5 r3: both conditions met -> cookie transport" {
    const named = Identity{ .named = "alice" };
    const peer: Peer = .{ .version = 22700, .datetime = .{ .date = cookie_gate_min_date, .time = cookie_gate_min_time } };
    try std.testing.expectEqual(LoginTransport.cookie, loginTransport(named, peer, .pull, 1));
    try std.testing.expect(loginTransport(named, peer, .pull, 1).emitsCookieHeader());
}

test "sync §6.5 r4: either condition unmet -> body-embedded login fallback" {
    const named = Identity{ .named = "alice" };
    const cases = [_]Peer{
        .{ .version = 22699, .datetime = .{ .date = cookie_gate_min_date, .time = cookie_gate_min_time } }, // low version
        .{ .version = cookie_gate_min_version, .datetime = .{ .date = 20250726, .time = 235959 } }, // early date
        .{ .version = cookie_gate_min_version, .datetime = .{ .date = cookie_gate_min_date, .time = 110459 } }, // early time
    };
    for (cases) |peer| {
        try std.testing.expectEqual(LoginTransport.body, loginTransport(named, peer, .pull, 1));
        try std.testing.expect(loginTransport(named, peer, .pull, 1).emitsBodyLogin());
    }
}

test "sync §6.5 r5: unobserved server-version (null timestamp) falls back to body" {
    const named = Identity{ .named = "alice" };
    const peer = Peer.clientInit(23000);
    try std.testing.expect(peer.datetime == null);
    try std.testing.expect(!peerGatedForCookie(peer));
    try std.testing.expectEqual(LoginTransport.body, loginTransport(named, peer, .pull, 1));
}

test "sync §6.5 r6: ungated peer never emits the x-f-l-c cookie header" {
    const named = Identity{ .named = "alice" };
    const cases = [_]Peer{
        .{ .version = 22699, .datetime = .{ .date = cookie_gate_min_date, .time = cookie_gate_min_time } },
        .{ .version = cookie_gate_min_version, .datetime = .{ .date = 20250101, .time = 0 } },
        Peer.clientInit(23000),
    };
    for (cases) |peer| {
        try std.testing.expect(!loginTransport(named, peer, .pull, 1).emitsCookieHeader());
    }
}

test "sync §6.5 r7: cookie login plus body login is a §11.1 duplicate_login error (rejection)" {
    const cases = [_]struct { cookie: bool, body: usize, want: bool }{
        .{ .cookie = true, .body = 1, .want = true }, // r7: cookie + body login together
        .{ .cookie = false, .body = 2, .want = true }, // §6.1 two body logins
        .{ .cookie = true, .body = 0, .want = false },
        .{ .cookie = false, .body = 1, .want = false },
        .{ .cookie = false, .body = 0, .want = false },
    };
    for (cases) |c| {
        try std.testing.expectEqual(c.want, duplicateLogin(c.cookie, c.body));
    }
    try std.testing.expectEqual(ErrorReset.reset, serverErrorReset(.duplicate_login));
}

test "sync §6.5 r8: server accepts a body login irrespective of cookie gating" {
    try std.testing.expect(!duplicateLogin(false, 1));

    const body_tail = "r8 body tail bytes";
    const nonce = computeNonce(body_tail);
    const stored_secret = "r8-stored-credential";
    const signature = computeSignature(&nonce, stored_secret);
    const card: LoginCard = .{ .user = "alice", .nonce = &nonce, .signature = &signature };
    // serverVerifyLogin takes no version/gate input: acceptance of a body
    // login is structurally ungated, even for a peer that would also
    // qualify for the cookie transport.
    try std.testing.expectEqual(LoginResult.ok, serverVerifyLogin(card, stored_secret, "project", body_tail));
}

test "sync §6.5 r9: cookie-login mode suppresses the body login card" {
    const named = Identity{ .named = "alice" };
    const peer: Peer = .{ .version = 22700, .datetime = .{ .date = cookie_gate_min_date, .time = cookie_gate_min_time } };
    const transport = loginTransport(named, peer, .pull, 1);
    try std.testing.expectEqual(LoginTransport.cookie, transport);
    try std.testing.expect(!transport.emitsBodyLogin());
}

test "sync §6.2 [RFC3174] §7.3: computeNonce('abc') == a9993e364706816aba3e25717850c26c9cd0d89d (absolute anchor)" {
    // §6.2 L1085-1087: NONCE = SHA1(body bytes after the login-card LF).
    // computeNonce('abc') is therefore SHA1('abc'), which RFC3174 §7.3 TEST1
    // pins absolutely.
    const nonce = computeNonce("abc");
    try std.testing.expectEqualStrings("a9993e364706816aba3e25717850c26c9cd0d89d", &nonce);
}

test "sync §6.2: the nonce excludes the login line (same tail, different login -> same nonce)" {
    // §6.2 L1085-1087: only the bytes AFTER the login-card LF feed the nonce.
    // Two requests that differ SOLELY in their login line but share the same
    // tail must yield the same nonce; hashing the whole request (login line
    // included) yields a different value, proving the login line is excluded.
    const tail = "the shared body tail bytes after the login line";
    const req_a = "login alice " ++ ("a" ** 40) ++ " " ++ ("b" ** 40) ++ "\n" ++ tail;
    const req_b = "login bob " ++ ("c" ** 40) ++ " " ++ ("d" ** 40) ++ "\n" ++ tail;
    try std.testing.expect(!std.mem.eql(u8, req_a, req_b)); // the two requests genuinely differ
    const tail_a = req_a[std.mem.indexOfScalar(u8, req_a, '\n').? + 1 ..];
    const tail_b = req_b[std.mem.indexOfScalar(u8, req_b, '\n').? + 1 ..];
    try std.testing.expectEqualStrings(tail, tail_a);
    try std.testing.expectEqualStrings(tail, tail_b);
    const nonce_a = computeNonce(tail_a);
    const nonce_b = computeNonce(tail_b);
    try std.testing.expectEqualStrings(&nonce_a, &nonce_b); // same tail -> same nonce despite different logins
    // Hashing the full request (login line + tail) instead would differ.
    try std.testing.expect(!std.mem.eql(u8, &nonce_a, &computeNonce(req_a)));
    try std.testing.expect(!std.mem.eql(u8, &nonce_a, &computeNonce(req_b)));
}

test "sync §6.2: nonce is sensitive to body-tail suffix perturbation (changed tail -> changed nonce)" {
    // §6.2 L1085-1087: the nonce covers the tail through end of body, so any
    // change to those bytes changes the nonce.
    const base = computeNonce("body tail bytes");
    const changed = computeNonce("body tail bytesX");
    try std.testing.expect(!std.mem.eql(u8, &base, &changed));
}

test "sync §6.1: loginTransport is .none for an anonymous identity (no login card)" {
    // §6.1 L1079-1080: a login card MUST NOT be emitted for anonymous.
    const gated: Peer = .{ .version = 22700, .datetime = .{ .date = cookie_gate_min_date, .time = cookie_gate_min_time } };
    const id = Identity.fromLoginName("anonymous");
    try std.testing.expectEqual(LoginTransport.none, loginTransport(id, gated, .pull, 1));
    try std.testing.expect(!loginTransport(id, gated, .pull, 1).emitsBodyLogin());
    try std.testing.expect(!loginTransport(id, gated, .pull, 1).emitsCookieHeader());
}

test "sync §6.1: loginTransport is .none for a nobody identity (no login card)" {
    // §6.1 L1079-1080: a login card MUST NOT be emitted for nobody.
    const gated: Peer = .{ .version = 22700, .datetime = .{ .date = cookie_gate_min_date, .time = cookie_gate_min_time } };
    const id = Identity.fromLoginName("nobody");
    try std.testing.expectEqual(LoginTransport.none, loginTransport(id, gated, .pull, 1));
}

test "sync §8.3: loginTransport is .none for a named identity on clone round 0 (neither login form)" {
    // §6.1 L1080-1081 + §8.3 L1598-1599: the first round of a protocol-version-3 clone omits
    // both body-login and cookie-login, even for a named, gated peer.
    const gated: Peer = .{ .version = 22700, .datetime = .{ .date = cookie_gate_min_date, .time = cookie_gate_min_time } };
    const named = Identity{ .named = "alice" };
    try std.testing.expectEqual(LoginTransport.none, loginTransport(named, gated, .clone, 0));
    // A later clone round (round 1) resumes normal transport (cookie here).
    try std.testing.expectEqual(LoginTransport.cookie, loginTransport(named, gated, .clone, 1));
}

test "sync §6.1: writeLogin emits `login USER NONCE SIGNATURE` with single-SP separators over a fossilized USER" {
    // §6.1 L1075-1077: the body-card form is `login USER NONCE SIGNATURE`.
    // USER is emitted verbatim as the already-fossilized token, so an
    // escaped whitespace (backslash-s) stays a single token.
    const nonce = "653828a8fc753c1685501de457c104f0b20938c9";
    const sig = "bec32fbc6a2589413ee6d1f47a4ae0098dafcad2";
    var buf: [128]u8 = undefined;
    const plain = try writeLogin(.{ .user = "alice", .nonce = nonce, .signature = sig }, &buf);
    try std.testing.expectEqualStrings("login alice " ++ "653828a8fc753c1685501de457c104f0b20938c9" ++ " " ++ "bec32fbc6a2589413ee6d1f47a4ae0098dafcad2", plain);
    var buf2: [128]u8 = undefined;
    const escaped = try writeLogin(.{ .user = "user\\sname", .nonce = nonce, .signature = sig }, &buf2);
    try std.testing.expectEqualStrings("login user\\sname " ++ "653828a8fc753c1685501de457c104f0b20938c9" ++ " " ++ "bec32fbc6a2589413ee6d1f47a4ae0098dafcad2", escaped);
}

test "sync §B: cookie value — ~ passes unescaped, SP -> +, $ / : are percent-encoded" {
    // §B L2843-2844: pct-user unreserved = ALPHA / DIGIT / - . _ ~ ; every
    // other byte percent-encoded. So `~` passes, SP -> '+', and `$ / :`
    // become %24 %2F %3A. (The RFC3986 unreserved set is normative here — see
    // rfc_ambiguity re: a claimed wider fossil-canonical alphabet.)
    const nonce = "653828a8fc753c1685501de457c104f0b20938c9";
    const sig = "bec32fbc6a2589413ee6d1f47a4ae0098dafcad2";
    var buf: [160]u8 = undefined;
    const cookie = writeCookieLogin(.{ .user = "a~b$c/d:e f", .nonce = nonce, .signature = sig }, &buf).?;
    try std.testing.expectEqualStrings("login+a~b%24c%2Fd%3Ae+f+" ++ nonce ++ "+" ++ sig, cookie);
}

test "sync §B: cookie value — * becomes %2A" {
    const nonce = "653828a8fc753c1685501de457c104f0b20938c9";
    const sig = "bec32fbc6a2589413ee6d1f47a4ae0098dafcad2";
    var buf: [160]u8 = undefined;
    const cookie = writeCookieLogin(.{ .user = "a*b", .nonce = nonce, .signature = sig }, &buf).?;
    try std.testing.expectEqualStrings("login+a%2Ab+" ++ nonce ++ "+" ++ sig, cookie);
}

test "sync §B: cookie value — backslash becomes %5C" {
    const nonce = "653828a8fc753c1685501de457c104f0b20938c9";
    const sig = "bec32fbc6a2589413ee6d1f47a4ae0098dafcad2";
    var buf: [160]u8 = undefined;
    const cookie = writeCookieLogin(.{ .user = "a\\b", .nonce = nonce, .signature = sig }, &buf).?;
    try std.testing.expectEqualStrings("login+a%5Cb+" ++ nonce ++ "+" ++ sig, cookie);
}

test "sync §B: cookie value — a non-ASCII byte becomes uppercase %XX" {
    const nonce = "653828a8fc753c1685501de457c104f0b20938c9";
    const sig = "bec32fbc6a2589413ee6d1f47a4ae0098dafcad2";
    var buf: [160]u8 = undefined;
    // 0xC3 is the lead byte of a UTF-8 'Ã'; it must encode as uppercase %C3.
    const cookie = writeCookieLogin(.{ .user = "a\xc3b", .nonce = nonce, .signature = sig }, &buf).?;
    try std.testing.expectEqualStrings("login+a%C3b+" ++ nonce ++ "+" ++ sig, cookie);
}

test "sync §B: cookie decode accepts case-insensitive %XX but rejects %20 in a separator slot" {
    // §B L2842-2845: url-login-value = "login+" pct-user "+" nonce "+"
    // signature, with pct-encoded = "%" HEXDIG-CI HEXDIG-CI. The field
    // separator is the literal "+" only; the sole leniency the grammar
    // licenses is HEXDIG-CI (a pct-encoded byte spelled in either hex case).
    // A lowercase escape therefore decodes exactly as its uppercase-hex
    // canonical form. Using %20 where a "+" separator belongs is NOT a
    // grammar separator and MUST be rejected.
    const nonce = "653828a8fc753c1685501de457c104f0b20938c9";
    const sig = "bec32fbc6a2589413ee6d1f47a4ae0098dafcad2";
    // '*' is not unreserved (§B L2844), so writeCookieLogin percent-encodes
    // it with uppercase hex: %2A.
    var canon_buf: [160]u8 = undefined;
    const canonical = writeCookieLogin(.{ .user = "a*b", .nonce = nonce, .signature = sig }, &canon_buf).?;
    try std.testing.expectEqualStrings("login+a%2Ab+" ++ nonce ++ "+" ++ sig, canonical);
    var a: [160]u8 = undefined;
    const from_canon = parseCookieLogin(canonical, &a).?;

    // The same value with the escape spelled in lowercase hex (%2a) decodes
    // to the identical card -- HEXDIG-CI is case-insensitive.
    const lower = "login+a%2ab+" ++ nonce ++ "+" ++ sig;
    var b: [160]u8 = undefined;
    const from_lower = parseCookieLogin(lower, &b).?;
    try std.testing.expectEqualStrings("a*b", from_lower.user);
    try std.testing.expectEqualStrings(from_canon.user, from_lower.user);
    try std.testing.expectEqualStrings(from_canon.nonce, from_lower.nonce);
    try std.testing.expectEqualStrings(from_canon.signature, from_lower.signature);

    // %20 in the separator positions is not the grammar's "+" separator; the
    // flat form-urldecode leniency that accepted it was not RFC-derived.
    const sep_as_pct20 = "login%20a%2ab%20" ++ nonce ++ "%20" ++ sig;
    var c: [160]u8 = undefined;
    try std.testing.expect(parseCookieLogin(sep_as_pct20, &c) == null);
}

test "sync §6.1/§6.4 fuzz: deriveSharedSecret/computeNonce/computeSignature never crash and return 40 lowercase-hex bytes" {
    // Seeded PRNG over bounded, precondition-respecting inputs: the three §6
    // digest funnels always produce exactly 40 lowercase-hex bytes and never
    // trip a bound. Reproduces exactly from the fixed seed.
    var prng = std.Random.DefaultPrng.init(0x5116_C0DE);
    const rand = prng.random();
    const hex_lower = "0123456789abcdef";
    var round: usize = 0;
    while (round < 500) : (round += 1) {
        // login: 1..16 bytes of lowercase letters (non-empty, no whitespace).
        var login_buf: [16]u8 = undefined;
        const login_len = 1 + rand.uintLessThan(usize, login_buf.len);
        for (login_buf[0..login_len]) |*c| c.* = 'a' + rand.uintLessThan(u8, 26);
        const login = login_buf[0..login_len];

        var pc_buf: [40]u8 = undefined;
        for (&pc_buf) |*c| c.* = rand.int(u8);
        var pw_buf: [64]u8 = undefined;
        const pw_len = rand.uintLessThan(usize, pw_buf.len + 1);
        for (pw_buf[0..pw_len]) |*c| c.* = rand.int(u8);

        const secret = deriveSharedSecret(&pc_buf, login, pw_buf[0..pw_len]);

        var tail_buf: [256]u8 = undefined;
        const tail_len = rand.uintLessThan(usize, tail_buf.len + 1);
        for (tail_buf[0..tail_len]) |*c| c.* = rand.int(u8);
        const nonce = computeNonce(tail_buf[0..tail_len]);

        // nonce_hex is itself 40 lowercase hex; secret is a 40-hex string.
        const sig = computeSignature(&nonce, &secret);

        for ([_][]const u8{ &secret, &nonce, &sig }) |digest| {
            try std.testing.expectEqual(@as(usize, 40), digest.len);
            for (digest) |c| try std.testing.expect(std.mem.indexOfScalar(u8, hex_lower, c) != null);
        }
    }
}

test "sync §6.6 r1: the server assigns capability letters o/i/g/x; other bytes carry none" {
    try std.testing.expectEqual(Capability.read, capabilityFromLetter('o').?);
    try std.testing.expectEqual(Capability.write, capabilityFromLetter('i').?);
    try std.testing.expectEqual(Capability.clone, capabilityFromLetter('g').?);
    try std.testing.expectEqual(Capability.private, capabilityFromLetter('x').?);
    for ("aszA\x00") |letter| try std.testing.expectEqual(@as(?Capability, null), capabilityFromLetter(letter));

    const full = capabilitySetFromLetters("oigx");
    try std.testing.expect(full.contains(.read) and full.contains(.write) and full.contains(.clone) and full.contains(.private));
    try std.testing.expectEqual(@as(usize, 0), capabilitySetFromLetters("").count());
    // unknown byte 'Q' is skipped, not phantom-inserted; 'o' is still held.
    try std.testing.expect(capabilitySetFromLetters("oQ").contains(.read));
    try std.testing.expectEqual(@as(usize, 1), capabilitySetFromLetters("oQ").count());
}

test "sync §6.6 r2: Read (o) permits pull" {
    try std.testing.expectEqual(Capability.read, capabilityFromLetter('o').?);
    try std.testing.expectEqual(Capability.read, requiredCapability(.pull).?);
    try std.testing.expectEqual(CapabilityDecision.process, serverCapabilityDecision(capabilitySetFromLetters("o"), .pull));
}

test "sync §6.6 r3: Write (i) permits push" {
    try std.testing.expectEqual(Capability.write, capabilityFromLetter('i').?);
    try std.testing.expectEqual(Capability.write, requiredCapability(.push).?);
    try std.testing.expectEqual(CapabilityDecision.process, serverCapabilityDecision(capabilitySetFromLetters("i"), .push));
}

test "sync §6.6 r4: Clone (g) permits clone" {
    try std.testing.expectEqual(Capability.clone, capabilityFromLetter('g').?);
    try std.testing.expectEqual(Capability.clone, requiredCapability(.clone).?);
    try std.testing.expectEqual(CapabilityDecision.process, serverCapabilityDecision(capabilitySetFromLetters("g"), .clone));
}

test "sync §6.6 r5: Private (x) permits private-content transfer" {
    try std.testing.expectEqual(Capability.private, capabilityFromLetter('x').?);
    const caps = capabilitySetFromLetters("x");
    try std.testing.expect(caps.contains(.private));
    // realized by the existing §7.5/§9.5 gates, not re-keyed here.
    try std.testing.expectEqual(SendPrivateDecision.enable, serverSendPrivateDecision(caps.contains(.private)));
    try std.testing.expectEqual(PrivateReceipt.store, privateReceipt(caps.contains(.private)));
}

test "sync §6.6 r6: administrator capabilities permit configuration (no letter)" {
    try std.testing.expectEqual(@as(?Capability, null), capabilityFromLetter('a'));
    try std.testing.expectEqual(@as(?Capability, null), capabilityFromLetter('s'));
    // admin's configuration/user-management gate is §9.3's decider.
    try std.testing.expectEqual(ConfigPushDecision.apply, serverConfigPushDecision(true));
    try std.testing.expectEqual(ConfigPushDecision.reject, serverConfigPushDecision(false));
}

test "sync §6.6 r7: the server checks the capability appropriate to each processed card" {
    try std.testing.expectEqual(Capability.read, requiredCapability(.pull).?);
    try std.testing.expectEqual(Capability.write, requiredCapability(.push).?);
    try std.testing.expectEqual(Capability.clone, requiredCapability(.clone).?);
    try std.testing.expectEqual(@as(?Capability, null), requiredCapability(.sync));

    const full = capabilitySetFromLetters("oig");
    const ops = [_]Operation{ .pull, .push, .clone };
    for (ops) |op| try std.testing.expectEqual(CapabilityDecision.process, serverCapabilityDecision(full, op));
}

test "sync §6.6 r8: an unauthorized pull (no o) is rejected with an error card" {
    try std.testing.expectEqual(CapabilityDecision.reject, serverCapabilityDecision(capabilitySetFromLetters("ig"), .pull));
    try std.testing.expectEqual(CapabilityDecision.reject, serverCapabilityDecision(CapabilitySet.empty, .pull));
}

test "sync §6.6 r8: an unauthorized push (no i) is rejected with an error card" {
    try std.testing.expectEqual(CapabilityDecision.reject, serverCapabilityDecision(capabilitySetFromLetters("og"), .push));
}

test "sync §6.6 r8: an unauthorized clone (no g) is rejected with an error card" {
    try std.testing.expectEqual(CapabilityDecision.reject, serverCapabilityDecision(capabilitySetFromLetters("oi"), .clone));
}

test "sync §6.4: a login grant is additive — union never clears a baseline bit" {
    // §6.4 L1128-1130: a successful login card can only add capability bits
    // on top of the baseline, never remove any; the two contributors combine
    // by union. Rows are (baseline_letters, grant_letters, expected_letters).
    const Row = struct { baseline: []const u8, grant: []const u8, expected: []const u8 };
    const rows = [_]Row{
        .{ .baseline = "o", .grant = "i", .expected = "oi" }, // disjoint grant adds without clearing
        .{ .baseline = "oi", .grant = "o", .expected = "oi" }, // grant subset of baseline -> unchanged
        .{ .baseline = "og", .grant = "", .expected = "og" }, // empty grant leaves baseline exactly
        .{ .baseline = "", .grant = "x", .expected = "x" }, // empty baseline -> grant only
        .{ .baseline = "oigx", .grant = "oigx", .expected = "oigx" }, // idempotent full set
    };
    for (rows) |row| {
        const base = capabilitySetFromLetters(row.baseline);
        const grant = capabilitySetFromLetters(row.grant);
        const got = establishedCapabilities(base, grant);
        const expected = capabilitySetFromLetters(row.expected);
        // (a) additive: every baseline bit survives the union (grant never clears one).
        var it = base.iterator();
        while (it.next()) |cap| try std.testing.expect(got.contains(cap));
        // (b) grant applied: the grant is a subset of the established set.
        try std.testing.expect(got.supersetOf(grant));
        // (c) exactness: the established set equals the RFC-transcribed expectation.
        try std.testing.expect(got.eql(expected));
        try std.testing.expectEqual(expected.count(), got.count());
    }
}

test "sync §6.6: capabilitySetFromLetters on garbage never crashes and stays bounded" {
    // §6.6 L1189-1191: of the enumerated letters, only o/i/g/x name a transfer
    // capability; arbitrary wire bytes (NUL and >0x7f legal, all []const u8)
    // must be skipped, so the derived set is always bounded by {o,i,g,x}.
    var prng = std.Random.DefaultPrng.init(0x6666_c0de);
    const rand = prng.random();
    const full = capabilitySetFromLetters("oigx");
    var buf: [64]u8 = undefined;
    for (0..4096) |_| {
        const len = rand.uintAtMost(usize, 64); // per-iteration length in 0..=64
        rand.bytes(buf[0..len]);
        const set = capabilitySetFromLetters(buf[0..len]);
        try std.testing.expect(set.count() <= 4); // bounded by the four known caps
        try std.testing.expect(full.supersetOf(set)); // only known caps ever appear
    }
}

test "sync §7.1-r1: file card parses in both two- and four-argument forms" {
    const three = try parseFileCard(tokenizeCard("file " ++ test_hash_a ++ " 42"));
    try std.testing.expectEqualStrings(test_hash_a, three.hash);
    try std.testing.expectEqual(@as(?[]const u8, null), three.deltasrc);
    try std.testing.expectEqual(@as(u32, 42), three.size);

    const four = try parseFileCard(tokenizeCard("file " ++ test_hash_a ++ " " ++ test_hash_b ++ " 42"));
    try std.testing.expectEqualStrings(test_hash_a, four.hash);
    try std.testing.expectEqualStrings(test_hash_b, four.deltasrc.?);
    try std.testing.expectEqual(@as(u32, 42), four.size);
}

test "sync §7.1-r2: file SIZE counts payload bytes; payload may contain 0x0a" {
    const body = "file " ++ test_hash_a ++ " 5\nAB\nCDtail";
    const line = try readLine(body, 0);
    const card = try parseFileCard(tokenizeCard(line.bytes));
    try std.testing.expectEqual(@as(u32, 5), card.size);
    const payload = try consumeContentPayload(body, line.next, card.size);
    try std.testing.expectEqualStrings("AB\nCD", payload.bytes);
}

test "sync §7.1-r3: file payload end located by count, not separator scan (rejection)" {
    // A naive LF-scan would stop at "AB" (2 bytes); byte-count framing
    // must instead consume the full 5-byte payload "AB\nCD" — the
    // embedded LF at offset 2 is not a terminator.
    const body = "file " ++ test_hash_a ++ " 5\nAB\nCD";
    const line = try readLine(body, 0);
    const card = try parseFileCard(tokenizeCard(line.bytes));
    const payload = try consumeContentPayload(body, line.next, card.size);
    try std.testing.expectEqualStrings("AB\nCD", payload.bytes);
    try std.testing.expect(payload.bytes.len != 2);
}

test "sync §7.1-r4: no trailing separator; next byte begins the next card" {
    const body = "file " ++ test_hash_a ++ " 3\nXYZgimme " ++ test_hash_b;
    const line = try readLine(body, 0);
    const card = try parseFileCard(tokenizeCard(line.bytes));
    const payload = try consumeContentPayload(body, line.next, card.size);
    try std.testing.expectEqual(line.next + 3, payload.next);
    try std.testing.expectEqualStrings("gimme " ++ test_hash_b, body[payload.next..]);
}

test "sync §7.1-r5: two-arg form is full content, four-arg carries a delta basis" {
    const full = try parseFileCard(tokenizeCard("file " ++ test_hash_a ++ " 10"));
    try std.testing.expectEqual(@as(?[]const u8, null), full.deltasrc);
    const delta_form = try parseFileCard(tokenizeCard("file " ++ test_hash_a ++ " " ++ test_hash_b ++ " 10"));
    try std.testing.expectEqualStrings(test_hash_b, delta_form.deltasrc.?);
}

const a1_round2_file_cards = @embedFile("rfc_vectors/sync_a1_round2_file_cards.txt");

test "sync §7.1: worked example — first file card frames the 272-byte manifest (§A.1 L2483-2490)" {
    const body = a1_round2_file_cards;
    const line = try readLine(body, 0);
    const card = try parseFileCard(tokenizeCard(line.bytes));
    try std.testing.expectEqualStrings("8e0933ed1ef9aa0051d185c8123f0b9ac79865e4b70098b877543fca0092c259", card.hash);
    try std.testing.expectEqual(@as(?[]const u8, null), card.deltasrc);
    // RFC L3029-3031: "seven lines of Fossil manifest notation, totalling
    // 272 bytes including terminating LFs" — 26+26+77+67+35+6+35 = 272.
    try std.testing.expectEqual(@as(u32, 272), card.size);
    const payload = try consumeContentPayload(body, line.next, card.size);
    try std.testing.expectEqualStrings(
        "C client:\\sadd\\salpha.txt\n" ++
            "D 2026-07-02T18:30:46.860\n" ++
            "F alpha.txt b1c1b05fea4f52bd90b15fdffd13f5c91dce6505f498eeb79b4952c2ea8d4d15\n" ++
            "P c5b2e0ecb779a7616b32553d973bc9c4ae5a07569a2cee18001386da6bd9ae57\n" ++
            "R 6b3e7488d36fc39ea8093fdea87c3641\n" ++
            "U drh\n" ++
            "Z ed00cdc67449c3949587aa83b3fd8a67\n",
        payload.bytes,
    );
    // No trailing separator: the very next byte starts the second file card.
    const next_line = try readLine(body, payload.next);
    try std.testing.expectEqualStrings(
        "file b1c1b05fea4f52bd90b15fdffd13f5c91dce6505f498eeb79b4952c2ea8d4d15 26",
        next_line.bytes,
    );
}

test "sync §7.1: worked example — second file card frames the 26-byte payload (§A.1 L2491-2492)" {
    const body = a1_round2_file_cards;
    const first_line = try readLine(body, 0);
    const first_card = try parseFileCard(tokenizeCard(first_line.bytes));
    const first_payload = try consumeContentPayload(body, first_line.next, first_card.size);

    const line = try readLine(body, first_payload.next);
    const card = try parseFileCard(tokenizeCard(line.bytes));
    try std.testing.expectEqualStrings("b1c1b05fea4f52bd90b15fdffd13f5c91dce6505f498eeb79b4952c2ea8d4d15", card.hash);
    // RFC L3032-3034: "the text alpha content from client followed by a
    // single LF" — 25 text bytes + 1 LF = 26.
    try std.testing.expectEqual(@as(u32, 26), card.size);
    const payload = try consumeContentPayload(body, line.next, card.size);
    try std.testing.expectEqualStrings("alpha content from client\n", payload.bytes);
    // No trailing separator: the next byte starts the following gimme card.
    const next_line = try readLine(body, payload.next);
    try std.testing.expectEqualStrings(
        "gimme 4361252d0f450879dcd2e0b6a42b95ffe8a41d8e948130253ae8eefa7f4b79ea",
        next_line.bytes,
    );
}

test "sync §7.2-r1: cfile card parses in both three- and four-value forms" {
    const three = try parseCfileCard(tokenizeCard("cfile " ++ test_hash_a ++ " 100 40"));
    try std.testing.expectEqualStrings(test_hash_a, three.hash);
    try std.testing.expectEqual(@as(?[]const u8, null), three.deltasrc);
    try std.testing.expectEqual(@as(u32, 100), three.uncompressed_size);
    try std.testing.expectEqual(@as(u32, 40), three.compressed_size);

    const four = try parseCfileCard(tokenizeCard("cfile " ++ test_hash_a ++ " " ++ test_hash_b ++ " 100 40"));
    try std.testing.expectEqualStrings(test_hash_b, four.deltasrc.?);
    try std.testing.expectEqual(@as(u32, 100), four.uncompressed_size);
    try std.testing.expectEqual(@as(u32, 40), four.compressed_size);
}

test "sync §7.2-r2: CSIZE counts payload bytes; USIZE is the reconstructed size" {
    const card = try parseCfileCard(tokenizeCard("cfile " ++ test_hash_a ++ " 1000 12"));
    try std.testing.expectEqual(@as(u32, 1000), card.uncompressed_size);
    try std.testing.expectEqual(@as(u32, 12), card.compressed_size);
}

test "sync §7.2-r3: payload framed by CSIZE, never USIZE (rejection)" {
    // USIZE (999999) vastly exceeds the body; a framer that mistakenly
    // used USIZE would overrun. `consumeContentPayload` sees only the
    // caller-selected size argument — passing CSIZE succeeds, passing
    // USIZE (proving what would happen if the wrong field reached the
    // framer) is Truncated.
    const body = "cfile " ++ test_hash_a ++ " 999999 5\nHELLOtail";
    const line = try readLine(body, 0);
    const card = try parseCfileCard(tokenizeCard(line.bytes));
    const payload = try consumeContentPayload(body, line.next, card.compressed_size);
    try std.testing.expectEqualStrings("HELLO", payload.bytes);
    try std.testing.expectError(ContentFrameError.Truncated, consumeContentPayload(body, line.next, card.uncompressed_size));
}

test "sync §7.2-r4: no trailing separator after the cfile payload" {
    const body = "cfile " ++ test_hash_a ++ " 100 3\nXYZgimme " ++ test_hash_b;
    const line = try readLine(body, 0);
    const card = try parseCfileCard(tokenizeCard(line.bytes));
    const payload = try consumeContentPayload(body, line.next, card.compressed_size);
    try std.testing.expectEqual(line.next + 3, payload.next);
    try std.testing.expectEqualStrings("gimme " ++ test_hash_b, body[payload.next..]);
}

test "sync §7.2-r5: USIZE parsed for syntax but not a frame boundary" {
    // USIZE (3) mismatches the 5 wire bytes CSIZE actually declares; the
    // card still parses and the payload still frames — current receivers
    // never cross-check USIZE against the received payload.
    const body = "cfile " ++ test_hash_a ++ " 3 5\nHELLOtail";
    const line = try readLine(body, 0);
    const card = try parseCfileCard(tokenizeCard(line.bytes));
    try std.testing.expectEqual(@as(u32, 3), card.uncompressed_size);
    const payload = try consumeContentPayload(body, line.next, card.compressed_size);
    try std.testing.expectEqual(@as(usize, 5), payload.bytes.len);
}

test "sync §7.2-r6: cfile payload framed verbatim; framer neither decompresses nor re-compresses" {
    // The payload is opaque stored-container bytes (a fabricated 4-byte
    // big-endian length header plus zlib-stream-shaped bytes, per §4.1's
    // container — the exact codec is out of scope here); the framer is a
    // byte-count slice, not a codec, so bytes in equal bytes out.
    const stored_container = [_]u8{ 0x00, 0x00, 0x00, 0x05, 0x78, 0x9c, 0x01, 0x02 };
    const body = "cfile " ++ test_hash_a ++ " 5 8\n" ++ stored_container;
    const line = try readLine(body, 0);
    const card = try parseCfileCard(tokenizeCard(line.bytes));
    const payload = try consumeContentPayload(body, line.next, card.compressed_size);
    try std.testing.expectEqualSlices(u8, &stored_container, payload.bytes);
}

test "sync §7.2-r7: x-fossil-uncompressed outer body means the payload is not double-compressed" {
    // §4.1: x-fossil-uncompressed's outer body is not zlib-wrapped
    // (outer_compressed == false); consumeContentPayload applies no
    // compression of its own, so the cfile payload it frames from that
    // outer body is exactly the stored-container bytes — never wrapped
    // a second time.
    try std.testing.expect(!media_props.get(.x_fossil_uncompressed).outer_compressed);
    const stored_container = [_]u8{ 0x00, 0x00, 0x00, 0x02, 0x78, 0x9c };
    const body = "cfile " ++ test_hash_a ++ " 2 6\n" ++ stored_container;
    const line = try readLine(body, 0);
    const card = try parseCfileCard(tokenizeCard(line.bytes));
    const payload = try consumeContentPayload(body, line.next, card.compressed_size);
    try std.testing.expectEqualSlices(u8, &stored_container, payload.bytes);
}

test "sync §7.1/§7.2: content payload whose span exceeds the body is Truncated (rejection)" {
    const file_body = "file " ++ test_hash_a ++ " 100\nshort";
    const file_line = try readLine(file_body, 0);
    const file_card = try parseFileCard(tokenizeCard(file_line.bytes));
    try std.testing.expectError(ContentFrameError.Truncated, consumeContentPayload(file_body, file_line.next, file_card.size));

    const cfile_body = "cfile " ++ test_hash_a ++ " 5 100\nshort";
    const cfile_line = try readLine(cfile_body, 0);
    const cfile_card = try parseCfileCard(tokenizeCard(cfile_line.bytes));
    try std.testing.expectError(ContentFrameError.Truncated, consumeContentPayload(cfile_body, cfile_line.next, cfile_card.compressed_size));
}

test "sync §7.1/§7.2: wrong arity or non-numeric size is Malformed (rejection)" {
    const file_cases = [_][]const u8{
        "file " ++ test_hash_a, // arity 2: missing SIZE
        "file " ++ test_hash_a ++ " x", // non-numeric SIZE
        "file " ++ test_hash_a ++ " " ++ test_hash_b ++ " x", // non-numeric SIZE, four-arg
        "file " ++ test_hash_a ++ " 1 2 3", // arity 5: neither 3 nor 4
    };
    for (file_cases) |line| {
        try std.testing.expectError(CardParseError.Malformed, parseFileCard(tokenizeCard(line)));
    }

    const cfile_cases = [_][]const u8{
        "cfile " ++ test_hash_a, // arity 2
        "cfile " ++ test_hash_a ++ " 10", // arity 3: missing CSIZE
        "cfile " ++ test_hash_a ++ " x 10", // non-numeric USIZE
        "cfile " ++ test_hash_a ++ " 10 x", // non-numeric CSIZE
        "cfile " ++ test_hash_a ++ " " ++ test_hash_b ++ " x 10", // non-numeric USIZE, five-arg
        "cfile " ++ test_hash_a ++ " 1 2 3 4", // arity 6: neither 4 nor 5
    };
    for (cfile_cases) |line| {
        try std.testing.expectError(CardParseError.Malformed, parseCfileCard(tokenizeCard(line)));
    }
}

test "sync §7.4-r2: file card naming an unheld DELTASRC is accepted, not rejected" {
    const card = try parseFileCard(tokenizeCard("file " ++ test_hash_a ++ " " ++ test_hash_b ++ " 10"));
    try std.testing.expectEqualStrings(test_hash_b, card.deltasrc.?);
}

test "sync §7.4-r2: cfile card naming an unheld DELTASRC is accepted, not rejected" {
    const card = try parseCfileCard(tokenizeCard("cfile " ++ test_hash_a ++ " " ++ test_hash_b ++ " 100 40"));
    try std.testing.expectEqualStrings(test_hash_b, card.deltasrc.?);
}

test "sync §7.4-r2: creating a phantom for the DELTASRC (store contract, §3.3)" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const store = ms.store();
    const card = try parseFileCard(tokenizeCard("file " ++ test_hash_a ++ " " ++ test_hash_b ++ " 10"));
    const rid = try store.intern(card.deltasrc.?);
    try std.testing.expectEqual(@as(?[]const u8, null), store.content(rid));
}

test "sync §7.4-r1: delta against an unheld source is retained, not applied" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const store = ms.store();
    const card = try parseFileCard(tokenizeCard("file " ++ test_hash_a ++ " " ++ test_hash_b ++ " 10"));
    const rid = try store.intern(card.deltasrc.?); // §7.4-r2 phantom, prerequisite for retention

    var queue = PendingDeltaQueue{};
    defer queue.deinit(std.testing.allocator);
    const delta_bytes = "opaque delta payload, not yet decoded";
    try queue.retain(std.testing.allocator, card.hash, card.deltasrc.?, .{ .raw = delta_bytes });

    try std.testing.expectEqual(@as(usize, 1), queue.itemCount());
    try std.testing.expectEqualStrings(card.hash, queue.items.items[0].?.hash);
    try std.testing.expectEqualStrings(card.deltasrc.?, queue.items.items[0].?.deltasrc);
    try std.testing.expectEqualStrings(delta_bytes, queue.items.items[0].?.payload.raw);
    // Reconstruction is deferred: the source is still a phantom and HASH was
    // never interned, so neither side has stored content yet.
    try std.testing.expectEqual(@as(?[]const u8, null), store.content(rid));
}

test "sync §7.4/§4: the retention ceiling is derived from the reply body cap, not an arbitrary constant" {
    // The ceiling is `reply_body_cap / min_delta_card_bytes` — the maximum
    // number of delta cards a conformant reply body could ever carry. It must be
    // finite (never unbounded allocation) and vastly larger than a real clone's
    // dangling-delta count (fossil-scm walled the old fixed 256 at 257).
    try std.testing.expectEqual(
        sync.reply_body_cap / PendingDeltaQueue.min_delta_card_bytes,
        PendingDeltaQueue.max_pending,
    );
    // Tightness: the ceiling times the minimum card size does not exceed the
    // body cap, and one more card would — so the derivation is exact, not slack.
    try std.testing.expect(PendingDeltaQueue.max_pending * PendingDeltaQueue.min_delta_card_bytes <= sync.reply_body_cap);
    try std.testing.expect((PendingDeltaQueue.max_pending + 1) * PendingDeltaQueue.min_delta_card_bytes > sync.reply_body_cap);
    try std.testing.expect(PendingDeltaQueue.max_pending > 1_000_000); // a real clone can never approach it
}

test "sync §7.4-r1: retention rejects past its ceiling with a clean QueueFull, never OOM (bound)" {
    // A synthetic over-bound retain: lower the ceiling so the negative-space edge
    // is reachable without materializing the millions of entries the real
    // `max_pending` would require. Retaining up to `cap` succeeds; the next one
    // returns QueueFull rather than allocating past the bound.
    var queue = PendingDeltaQueue{ .cap = 3 };
    defer queue.deinit(std.testing.allocator);
    var round: usize = 0;
    while (round < queue.cap) : (round += 1) {
        try queue.retain(std.testing.allocator, test_hash_a, test_hash_b, .{ .raw = "d" });
    }
    try std.testing.expectEqual(queue.cap, queue.itemCount());
    try std.testing.expectError(PendingDeltaQueue.RetainError.QueueFull, queue.retain(std.testing.allocator, test_hash_a, test_hash_b, .{ .raw = "d" }));
    try std.testing.expectEqual(queue.cap, queue.itemCount()); // rejection left the queue unchanged
}

test "sync §7.4-r1/r3: many dangling deltas past the old fixed cap are all retained then reconstructed to fixpoint" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const store = ms.store();

    // §7.4: fossil legitimately sends a delta before its basis. A real clone
    // fans out far more than 256 dangling deltas against a single absent basis
    // (fossil-scm walled at 257, lumosql at ~315). The queue MUST retain all of
    // them, not reject the 257th.
    const n = 1000;
    const base = "shared basis artifact bytes for the whole delta fan-out";
    const base_hash = test_hash_b;
    _ = try store.intern(base_hash); // §7.4-r2 phantom for the not-yet-arrived basis

    var queue = PendingDeltaQueue{};
    defer queue.deinit(std.testing.allocator);

    var deltas: [n][]const u8 = undefined;
    var hashes: [n][40]u8 = undefined;
    var made: usize = 0;
    defer for (deltas[0..made]) |d| std.testing.allocator.free(d);
    for (0..n) |k| {
        var tgt_buf: [80]u8 = undefined;
        const tgt = try std.fmt.bufPrint(&tgt_buf, "target artifact number {d} reconstructed from the shared base", .{k});
        const d = try delta.create(std.testing.allocator, base, tgt);
        deltas[k] = d;
        made += 1;
        _ = try std.fmt.bufPrint(&hashes[k], "{x:0>40}", .{k}); // distinct 40-hex target name
        try queue.retain(std.testing.allocator, &hashes[k], base_hash, .{ .raw = d });
    }
    try std.testing.expectEqual(@as(usize, n), queue.itemCount()); // all retained, none rejected

    // The basis arrives: a single completeFor must reconstruct every target.
    _ = try store.put(base_hash, base);
    try queue.completeFor(std.testing.allocator, store, base_hash, .unrestricted);
    try std.testing.expectEqual(@as(usize, 0), queue.itemCount());

    for (0..n) |k| {
        var tgt_buf: [80]u8 = undefined;
        const tgt = try std.fmt.bufPrint(&tgt_buf, "target artifact number {d} reconstructed from the shared base", .{k});
        const rid = try store.intern(&hashes[k]);
        try std.testing.expectEqualStrings(tgt, store.content(rid).?);
    }
}

test "sync §7.2/§7.4: a clone client retains a received full and delta cfile verbatim; wire() re-serves the exact containers" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    const s = ms.store();

    const base = "the base artifact bytes with a long shared run so the forward delta is compact and its raw size differs from the reconstructed target";
    const target = base ++ " with a small appended mutation";
    const base_container = try zlibContainerEncode(gpa, base, .default);
    defer gpa.free(base_container);
    const raw = try delta.create(gpa, base, target);
    defer gpa.free(raw);
    const delta_container = try zlibContainerEncode(gpa, raw, .default);
    defer gpa.free(delta_container);

    const base_hash = test_hash_a;
    const target_hash = "c" ** 40; // the delta cfile's DELTASRC is base_hash

    // A clone reply in §8.2 RID order: the full base cfile precedes the delta
    // cfile, so the client reconstructs the delta immediately against the held
    // basis and retains the received delta container verbatim.
    var body: std.ArrayList(u8) = .empty;
    defer body.deinit(gpa);
    try body.appendSlice(gpa, "clone_seqno 0\npush 1111111111111111 aa87ea41b0bb05ba528f7a3bdba700d7e23d6318\n");
    const bh = try std.fmt.allocPrint(gpa, "cfile {s} {d} {d}\n", .{ base_hash, base.len, base_container.len });
    defer gpa.free(bh);
    try body.appendSlice(gpa, bh);
    try body.appendSlice(gpa, base_container);
    try body.append(gpa, '\n'); // §5.1 optional trailing-LF padding
    const dh = try std.fmt.allocPrint(gpa, "cfile {s} {s} {d} {d}\n", .{ target_hash, base_hash, target.len, delta_container.len });
    defer gpa.free(dh);
    try body.appendSlice(gpa, dh);
    try body.appendSlice(gpa, delta_container);
    try body.append(gpa, '\n');

    var client = Client{ .operation = .clone, .state = .clone_round0 };
    defer client.deinit(gpa);
    _ = try client.consumeReply(gpa, s, body.items);

    // §7.2 — the base retained its received full container verbatim.
    const base_rid = s.lookup(base_hash).?;
    const bwf = s.wire(base_rid).?;
    try std.testing.expect(bwf.delta == null);
    try std.testing.expectEqualSlices(u8, base_container, bwf.container);

    // §7.2 — the delta retained its received container verbatim, named its basis,
    // and carries the reconstructed-target USIZE (delta §3.2), not the raw size.
    const target_rid = s.lookup(target_hash).?;
    const twf = s.wire(target_rid).?;
    try std.testing.expect(twf.delta != null);
    try std.testing.expectEqualSlices(u8, delta_container, twf.container);
    try std.testing.expectEqualStrings(base_hash, twf.delta.?.src_hash);
    try std.testing.expectEqual(@as(u64, target.len), twf.delta.?.target_size);

    // §7.4 — content() still reconstructs the original target (delta ∘ base).
    try std.testing.expectEqualStrings(target, s.content(target_rid).?);
}

test "sync §7.2/§7.4: a dangling reverse-delta cfile retains its §4.1 container through the queue; completion re-serves it verbatim" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    const s = ms.store();

    // A fossil reverse delta: the OLDER revision is stored as a delta of the
    // NEWER one, so the delta reconstructs OLDER from NEWER.
    const newer = "the NEWER artifact bytes with a long shared run so the reverse delta reconstructing the older revision stays a compact copy payload";
    const older = "the NEWER artifact bytes with a long shared run so the reverse delta reconstructing the older revision stays a compact copy payload, cut";
    const newer_container = try zlibContainerEncode(gpa, newer, .default);
    defer gpa.free(newer_container);
    const raw = try delta.create(gpa, newer, older); // OLDER = delta ∘ NEWER
    defer gpa.free(raw);
    const delta_container = try zlibContainerEncode(gpa, raw, .default);
    defer gpa.free(delta_container);

    const newer_hash = test_hash_a;
    const older_hash = "c" ** 40;

    // §8.2 clone order is ascending age: the OLDER revision's reverse-delta cfile
    // arrives BEFORE its NEWER basis, so it is dangling and retained. Pre-slice-A
    // the queue dropped the container and the artifact completed as full form;
    // now the container survives to a verbatim §7.2 re-serve.
    var body: std.ArrayList(u8) = .empty;
    defer body.deinit(gpa);
    try body.appendSlice(gpa, "clone_seqno 0\npush 1111111111111111 aa87ea41b0bb05ba528f7a3bdba700d7e23d6318\n");
    const dh = try std.fmt.allocPrint(gpa, "cfile {s} {s} {d} {d}\n", .{ older_hash, newer_hash, older.len, delta_container.len });
    defer gpa.free(dh);
    try body.appendSlice(gpa, dh);
    try body.appendSlice(gpa, delta_container);
    try body.append(gpa, '\n');
    const bh = try std.fmt.allocPrint(gpa, "cfile {s} {d} {d}\n", .{ newer_hash, newer.len, newer_container.len });
    defer gpa.free(bh);
    try body.appendSlice(gpa, bh);
    try body.appendSlice(gpa, newer_container);
    try body.append(gpa, '\n');

    var client = Client{ .operation = .clone, .state = .clone_round0 };
    defer client.deinit(gpa);
    _ = try client.consumeReply(gpa, s, body.items);

    // The dangling delta completed: the OLDER revision retained its received
    // container VERBATIM (not a full re-compression), named its basis, carries
    // the reconstructed-target USIZE, and reconstructs the original bytes.
    const older_rid = s.lookup(older_hash).?;
    const wf = s.wire(older_rid).?;
    try std.testing.expect(wf.delta != null);
    try std.testing.expectEqualSlices(u8, delta_container, wf.container);
    try std.testing.expectEqualStrings(newer_hash, wf.delta.?.src_hash);
    try std.testing.expectEqual(@as(u64, older.len), wf.delta.?.target_size);
    try std.testing.expectEqualStrings(older, s.content(older_rid).?);
}

test "sync §7.2: a clone stores received full and delta cfiles wire-backed, holding no memoised plaintext" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    const s = ms.store();

    const base = "the base artifact bytes with a long shared run so the forward delta stays a compact copy payload and differs from the reconstructed target";
    const target = base ++ " with a small appended mutation";
    const base_container = try zlibContainerEncode(gpa, base, .default);
    defer gpa.free(base_container);
    const raw = try delta.create(gpa, base, target);
    defer gpa.free(raw);
    const delta_container = try zlibContainerEncode(gpa, raw, .default);
    defer gpa.free(delta_container);

    const base_hash = test_hash_a;
    const target_hash = "c" ** 40; // the delta cfile's DELTASRC is base_hash

    // §8.2 RID order: the full base cfile precedes the delta cfile, so the delta's
    // basis is held on arrival (the clone wire-backed fast path).
    var body: std.ArrayList(u8) = .empty;
    defer body.deinit(gpa);
    try body.appendSlice(gpa, "clone_seqno 0\npush 1111111111111111 aa87ea41b0bb05ba528f7a3bdba700d7e23d6318\n");
    const bh = try std.fmt.allocPrint(gpa, "cfile {s} {d} {d}\n", .{ base_hash, base.len, base_container.len });
    defer gpa.free(bh);
    try body.appendSlice(gpa, bh);
    try body.appendSlice(gpa, base_container);
    try body.append(gpa, '\n');
    const dh = try std.fmt.allocPrint(gpa, "cfile {s} {s} {d} {d}\n", .{ target_hash, base_hash, target.len, delta_container.len });
    defer gpa.free(dh);
    try body.appendSlice(gpa, dh);
    try body.appendSlice(gpa, delta_container);
    try body.append(gpa, '\n');

    var client = Client{ .operation = .clone, .state = .clone_round0 };
    defer client.deinit(gpa);
    _ = try client.consumeReply(gpa, s, body.items);

    // Peak stays O(wire): the clone-receive path retained only the §4.1
    // containers; NO artifact's inflated plaintext is memoised in the store —
    // the regression this whole slice removes (a 257 MB repo once held ~14 GB).
    try std.testing.expectEqual(@as(usize, 0), ms.plaintextBytesHeld());
    // §7.4 — content() still reconstructs each artifact on demand (memoising it
    // only then), so the plaintext is available lazily, not held eagerly.
    try std.testing.expectEqualStrings(base, s.content(s.lookup(base_hash).?).?);
    try std.testing.expectEqualStrings(target, s.content(s.lookup(target_hash).?).?);
    try std.testing.expect(ms.plaintextBytesHeld() > 0); // materialised by the reads above
}

test "sync §7.4: a completed dangling reverse-delta is stored wire-backed, holding no memoised plaintext" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    const s = ms.store();

    // A fossil reverse delta: the OLDER revision is stored as a delta of the NEWER
    // one, and (§8.2 ascending age) arrives BEFORE its basis — dangling, retained
    // in the pending queue, then completed when the NEWER full cfile lands.
    const newer = "the NEWER artifact bytes with a long shared run so the reverse delta reconstructing the older revision stays a compact copy payload";
    const older = "the NEWER artifact bytes with a long shared run so the reverse delta reconstructing the older revision stays a compact copy payload, cut";
    const newer_container = try zlibContainerEncode(gpa, newer, .default);
    defer gpa.free(newer_container);
    const raw = try delta.create(gpa, newer, older); // OLDER = delta ∘ NEWER
    defer gpa.free(raw);
    const delta_container = try zlibContainerEncode(gpa, raw, .default);
    defer gpa.free(delta_container);

    const newer_hash = test_hash_a;
    const older_hash = "c" ** 40;

    var body: std.ArrayList(u8) = .empty;
    defer body.deinit(gpa);
    try body.appendSlice(gpa, "clone_seqno 0\npush 1111111111111111 aa87ea41b0bb05ba528f7a3bdba700d7e23d6318\n");
    const dh = try std.fmt.allocPrint(gpa, "cfile {s} {s} {d} {d}\n", .{ older_hash, newer_hash, older.len, delta_container.len });
    defer gpa.free(dh);
    try body.appendSlice(gpa, dh);
    try body.appendSlice(gpa, delta_container);
    try body.append(gpa, '\n');
    const bh = try std.fmt.allocPrint(gpa, "cfile {s} {d} {d}\n", .{ newer_hash, newer.len, newer_container.len });
    defer gpa.free(bh);
    try body.appendSlice(gpa, bh);
    try body.appendSlice(gpa, newer_container);
    try body.append(gpa, '\n');

    var client = Client{ .operation = .clone, .state = .clone_round0 };
    defer client.deinit(gpa);
    _ = try client.consumeReply(gpa, s, body.items);

    // The dangling delta completed through the queue and was stored WIRE-BACKED —
    // no reconstructed plaintext was memoised for it or its basis (peak O(wire)).
    try std.testing.expectEqual(@as(usize, 0), ms.plaintextBytesHeld());
    try std.testing.expectEqualStrings(older, s.content(s.lookup(older_hash).?).?);
    try std.testing.expectEqualStrings(newer, s.content(s.lookup(newer_hash).?).?);
}

test "sync §7.4-r3: storing the deferred source completes the reconstruction" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const store = ms.store();

    const source = "the source artifact's stored bytes, pre-delta";
    const target = "the source artifact's stored bytes, post-delta, longer";
    const delta_bytes = try delta.create(std.testing.allocator, source, target);
    defer std.testing.allocator.free(delta_bytes);

    const card = try parseFileCard(tokenizeCard("file " ++ test_hash_a ++ " " ++ test_hash_b ++ " 1"));
    _ = try store.intern(card.deltasrc.?); // §7.4-r2 phantom before the source arrives

    var queue = PendingDeltaQueue{};
    defer queue.deinit(std.testing.allocator);
    try queue.retain(std.testing.allocator, card.hash, card.deltasrc.?, .{ .raw = delta_bytes });

    // The source artifact is subsequently stored (§7.4-r3's trigger).
    const source_rid = try store.put(card.deltasrc.?, source);
    try queue.completeFor(std.testing.allocator, store, card.deltasrc.?, .unrestricted);

    try std.testing.expectEqual(@as(usize, 0), queue.itemCount());
    try std.testing.expectEqualStrings(source, store.content(source_rid).?);
    const hash_rid = try store.intern(card.hash);
    try std.testing.expectEqualStrings(target, store.content(hash_rid).?);
}

test "sync §7.4-r3: completion cascades through a delta chain to fixpoint (C<-A<-B)" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const store = ms.store();
    const test_hash_c = "c" ** 40;

    const b_bytes = "artifact B: the eventual on-wire source, base of the chain";
    const a_bytes = "artifact A: reconstructed from B and itself the basis for C, longer";
    const c_bytes = "artifact C: reconstructed from A, the tail of the chain, longer still";
    const d_a = try delta.create(std.testing.allocator, b_bytes, a_bytes); // A from B
    defer std.testing.allocator.free(d_a);
    const d_c = try delta.create(std.testing.allocator, a_bytes, c_bytes); // C from A
    defer std.testing.allocator.free(d_c);

    // C and A both arrive before B: C names A as basis, A names B as basis.
    _ = try store.intern(test_hash_a); // §7.4-r2 phantom for A (C's source)
    _ = try store.intern(test_hash_b); // §7.4-r2 phantom for B (A's source)
    var queue = PendingDeltaQueue{};
    defer queue.deinit(std.testing.allocator);
    try queue.retain(std.testing.allocator, test_hash_c, test_hash_a, .{ .raw = d_c });
    try queue.retain(std.testing.allocator, test_hash_a, test_hash_b, .{ .raw = d_a });

    // B is stored; completing for B must cascade A -> C to fixpoint, else C is a
    // permanent phantom (§7.4-r3) and §3.7 convergence breaks.
    _ = try store.put(test_hash_b, b_bytes);
    try queue.completeFor(std.testing.allocator, store, test_hash_b, .unrestricted);

    try std.testing.expectEqual(@as(usize, 0), queue.itemCount());
    try std.testing.expectEqualStrings(a_bytes, store.content(try store.intern(test_hash_a)).?);
    try std.testing.expectEqualStrings(c_bytes, store.content(try store.intern(test_hash_c)).?);
}

test "sync §7.4-r3: completeFor only completes entries whose deltasrc matches" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const store = ms.store();

    const source_b = "source b bytes";
    const target_a = "target a bytes, reconstructed from source b";
    const delta_ab = try delta.create(std.testing.allocator, source_b, target_a);
    defer std.testing.allocator.free(delta_ab);
    const other_source = "c" ** 40;
    const delta_ac = "opaque delta against a different, still-unheld source";

    var queue = PendingDeltaQueue{};
    defer queue.deinit(std.testing.allocator);
    try queue.retain(std.testing.allocator, test_hash_a, test_hash_b, .{ .raw = delta_ab });
    try queue.retain(std.testing.allocator, test_hash_a, other_source, .{ .raw = delta_ac });

    _ = try store.put(test_hash_b, source_b);
    try queue.completeFor(std.testing.allocator, store, test_hash_b, .unrestricted);

    try std.testing.expectEqual(@as(usize, 1), queue.itemCount());
    const survivor = for (queue.items.items) |maybe| {
        if (maybe) |entry| break entry;
    } else unreachable; // itemCount == 1 guarantees a live slot
    try std.testing.expectEqualStrings(other_source, survivor.deltasrc);
    const rid_a = try store.intern(test_hash_a);
    try std.testing.expectEqualStrings(target_a, store.content(rid_a).?);
}

fn completeForNoLeak(allocator: std.mem.Allocator) !void {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const store = ms.store();
    const source = "no-leak source bytes";
    const target = "no-leak target bytes, a bit longer than the source";
    const delta_bytes = try delta.create(std.testing.allocator, source, target);
    defer std.testing.allocator.free(delta_bytes);

    var queue = PendingDeltaQueue{};
    defer queue.deinit(allocator);
    try queue.retain(allocator, test_hash_a, test_hash_b, .{ .raw = delta_bytes });
    _ = try store.put(test_hash_b, source);
    try queue.completeFor(allocator, store, test_hash_b, .unrestricted);
}

test "sync §7.4-r3: completeFor propagates allocation failure without leaking" {
    try std.testing.checkAllAllocationFailures(std.testing.allocator, completeForNoLeak, .{});
}

fn manyDeltaRetainCompleteNoLeak(allocator: std.mem.Allocator) !void {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const store = ms.store();
    const base = "the shared basis these many deltas all reconstruct from";
    _ = try store.intern(test_hash_b);

    const m = 24; // enough to force list growth (past its initial inline capacity) under injected OOM
    var deltas: [m][]const u8 = undefined;
    var hashes: [m][40]u8 = undefined;
    var made: usize = 0;
    defer for (deltas[0..made]) |d| std.testing.allocator.free(d);

    var queue = PendingDeltaQueue{};
    defer queue.deinit(allocator);
    for (0..m) |k| {
        var tgt_buf: [64]u8 = undefined;
        const tgt = try std.fmt.bufPrint(&tgt_buf, "target {d} for the OOM sweep, longer than base", .{k});
        const d = try delta.create(std.testing.allocator, base, tgt);
        deltas[k] = d;
        made += 1;
        _ = try std.fmt.bufPrint(&hashes[k], "{x:0>40}", .{k});
        try queue.retain(allocator, &hashes[k], test_hash_b, .{ .raw = d });
    }
    _ = try store.put(test_hash_b, base);
    try queue.completeFor(allocator, store, test_hash_b, .unrestricted);
}

test "sync §7.4-r1/r3: growable retain + cascade completeFor propagate allocation failure without leaking" {
    try std.testing.checkAllAllocationFailures(std.testing.allocator, manyDeltaRetainCompleteNoLeak, .{});
}

test "sync §3.3 r27: gimme parses HASH; wrong arity or hash length is malformed" {
    try std.testing.expectEqualStrings(test_hash_a, (try parseGimmeCard(tokenizeCard("gimme " ++ test_hash_a))).hash);
    const sha3 = "c" ** 64;
    try std.testing.expectEqualStrings(sha3, (try parseGimmeCard(tokenizeCard("gimme " ++ sha3))).hash);
    try std.testing.expectError(CardParseError.Malformed, parseGimmeCard(tokenizeCard("gimme")));
    try std.testing.expectError(CardParseError.Malformed, parseGimmeCard(tokenizeCard("gimme deadbeef00")));
    try std.testing.expectError(CardParseError.Malformed, parseGimmeCard(tokenizeCard("gimme " ++ test_hash_a ++ " x")));
}

test "sync §7.5 r3: private content transfers only with send-private accepted AND the receiving user holds Private" {
    // Send half — §3.3 gimmeEligible (:2697): a private phantom is
    // gimme-eligible for the round only once private sync is enabled.
    try std.testing.expect(!gimmeEligible(.{ .peer_lacks = false, .shunned = false, .private = true, .private_sync_enabled = false }));
    try std.testing.expect(gimmeEligible(.{ .peer_lacks = false, .shunned = false, .private = true, .private_sync_enabled = true }));
    // Receive half — the receiving user must separately hold Private.
    try std.testing.expectEqual(PrivateReceipt.discard, privateReceipt(false));
    try std.testing.expectEqual(PrivateReceipt.store, privateReceipt(true));
}

test "sync §7.5 r4: a receiver stores a private artifact only when the user holds Private" {
    try std.testing.expectEqual(PrivateReceipt.store, privateReceipt(true));
}

test "sync §7.5 r5: a private artifact without the capability is discarded without error (rejection)" {
    try std.testing.expectEqual(PrivateReceipt.discard, privateReceipt(false));
}

test "sync §7.5 r7: a delta on a private basis to an unauthorized peer forces full content" {
    const Case = struct { basis_private: bool, peer_has_private: bool, want: DeltaBasisSend };
    const cases = [_]Case{
        .{ .basis_private = true, .peer_has_private = false, .want = .force_full },
        .{ .basis_private = true, .peer_has_private = true, .want = .delta_ok },
        .{ .basis_private = false, .peer_has_private = false, .want = .delta_ok },
        .{ .basis_private = false, .peer_has_private = true, .want = .delta_ok },
    };
    for (cases) |c| try std.testing.expectEqual(c.want, deltaBasisSend(c.basis_private, c.peer_has_private));
}

test "sync §7.5 r8: a private file/cfile pushed without the Private capability is rejected as not-authorized (rejection)" {
    try std.testing.expectEqual(SendPrivateDecision.reject, serverSendPrivateDecision(false));
    try std.testing.expectEqual(SendPrivateDecision.enable, serverSendPrivateDecision(true));
    try std.testing.expectEqual(ErrorReset.append, serverErrorReset(.unauthorized_send_private));
    try std.testing.expectEqualStrings("not authorized to sync private content", send_private_unauthorized_text);
}

test "sync §7.5 r9: a private igot HASH 1 from an unprivileged peer is not an error" {
    try std.testing.expect(!receivePrivateIgot(true).is_error);
    try std.testing.expect(!receivePrivateIgot(false).is_error);
}

test "sync §7.5 r10: the receiver does not create a phantom for a private igot HASH" {
    try std.testing.expect(!receivePrivateIgot(true).create_phantom);
    try std.testing.expect(!receivePrivateIgot(false).create_phantom);
}

test "sync §7.5 r11: an already-held phantom is marked private on a private igot HASH" {
    try std.testing.expect(receivePrivateIgot(true).mark_phantom_private);
    try std.testing.expect(!receivePrivateIgot(false).mark_phantom_private);
}

test "sync §7.5 r12: the receiver records the peer holds HASH and continues" {
    try std.testing.expect(receivePrivateIgot(true).record_peer_has);
    try std.testing.expect(receivePrivateIgot(false).record_peer_has);
}

test "sync §9.2: igot HASH is public; igot HASH 1 is the private-flag form" {
    // §A.3 Scenario 4 round-1 reply: L2652 (public igot HASH) and
    // L2655 (private igot HASH 1).
    const pub_card = try parseIgotCard(tokenizeCard("igot 23813573dea5ec687052a067866751f481e58ef4c478939ac601587a43f50b35"));
    try std.testing.expect(!pub_card.private);
    try std.testing.expectEqualStrings("23813573dea5ec687052a067866751f481e58ef4c478939ac601587a43f50b35", pub_card.hash);

    const priv_card = try parseIgotCard(tokenizeCard("igot 393953a9803595823fb560ddbe333767840a5ad89748a12fc2cd00f533c6df8a 1"));
    try std.testing.expect(priv_card.private);
    try std.testing.expectEqualStrings("393953a9803595823fb560ddbe333767840a5ad89748a12fc2cd00f533c6df8a", priv_card.hash);
}

test "sync §9.2: an igot flag other than 1 is malformed; wrong arity is malformed (rejection)" {
    const bad_flags = [_][]const u8{ "igot HASH 2", "igot HASH 0", "igot HASH x" };
    for (bad_flags) |line| try std.testing.expectError(CardParseError.Malformed, parseIgotCard(tokenizeCard(line)));
    try std.testing.expectError(CardParseError.Malformed, parseIgotCard(tokenizeCard("igot")));
    try std.testing.expectError(CardParseError.Malformed, parseIgotCard(tokenizeCard("igot HASH 1 extra")));
}

test "sync §7.7 L1434-1437 vs L1465-1466 r1: admission and integrity are distinct mechanisms (not conflated)" {
    // Under .sha1 policy a 40-hex name is admitted regardless of content —
    // admission never inspects bytes, so a corrupt payload still admits.
    var buf: [64]u8 = undefined;
    const good_content = "distinct-axes-content";
    const sha1_name = artifact.artifactName(.sha1, good_content, &buf);
    var name_copy: [40]u8 = undefined;
    @memcpy(&name_copy, sha1_name);
    try std.testing.expectEqual(Admission.admit, admitInbound(.sha1, .none, &name_copy));
    try std.testing.expect(!artifact.verify(&name_copy, "corrupted-different-bytes"));

    // Under .shun_sha1 a valid SHA3 name both admits AND verifies — the two
    // decisions are separable, not fused into one predicate.
    var buf2: [64]u8 = undefined;
    const sha3_name = artifact.artifactName(.sha3_256, good_content, &buf2);
    var name3_copy: [64]u8 = undefined;
    @memcpy(&name3_copy, sha3_name);
    try std.testing.expectEqual(Admission.admit, admitInbound(.shun_sha1, .none, &name3_copy));
    try std.testing.expect(artifact.verify(&name3_copy, good_content));
}

test "sync §7.7 L1428-1432/L1436-1437 r2: shun-sha1 rejects an inbound 40-hex SHA1 name before storage (rejection)" {
    const sha1_name = "a" ** 40;
    const sha3_name = "a" ** 64;
    try std.testing.expectEqual(Admission.reject, admitInbound(.shun_sha1, .none, sha1_name));
    try std.testing.expectEqual(Admission.admit, admitInbound(.shun_sha1, .none, sha3_name));
}

test "sync §7.7 L1434-1437: the admission PREDICATE takes no card-type and no clone input — the whole asymmetry lives at the call sites" {
    // "Admission applies to the received target HASH." The decision function's
    // ONLY inputs are the repository's policy, the repository's table, and the
    // name; it has no `Operator` and no clone-context parameter, so `file` and
    // `cfile` cannot be given different answers HERE.
    //
    // That is deliberately NOT the same claim as "the gate applies identically
    // at both sites": L1437-1440 has a non-clone `file` discard and a clone
    // `file` BYPASS, while L1440-1444 has a `cfile` discard even in clone. The
    // three call-site behaviours are pinned by the L1437-1440 / L1440 / L1440-1444
    // tests further down this file; what is pinned here is that the shared
    // predicate carries none of it.
    const params = @typeInfo(@TypeOf(admitInbound)).@"fn".params;
    try std.testing.expectEqual(@as(usize, 3), params.len);
    try std.testing.expectEqual(artifact.HashPolicy, params[0].type.?);
    try std.testing.expectEqual(sync.ShunSet, params[1].type.?);
    try std.testing.expectEqual([]const u8, params[2].type.?);

    const sha1_name = "a" ** 40;
    try std.testing.expectEqual(Admission.reject, admitInbound(.shun_sha1, .none, sha1_name));
}

test "sync §7.7 L1434-1435: an out-of-domain name WIDTH is answered `admit`, not asserted — a peer chooses this token" {
    // `parseFileCard`/`parseCfileCard` validate arity and the numeric fields but
    // NOT the hash width, so a peer can put any token in the HASH position and it
    // reaches the predicate verbatim. Neither half can fire on such a name:
    // shun-sha1 keys on a SHA1 NAME, and L1435-1437 keys the table on the
    // COMPLETE name. An assert here would be a peer-triggered panic in a
    // Debug/ReleaseSafe build (and would vanish in a release one).
    var shun = FixtureShunSet{ .names = &.{s77_target} };
    for ([_][]const u8{ "", "abc", "-", "a" ** 39, "a" ** 41, "a" ** 65 }) |odd| {
        try std.testing.expectEqual(Admission.admit, admitInbound(.shun_sha1, shun.shunSet(), odd));
    }
}

test "sync §7.7 L1434-1435 / §7.1: a `file` card whose HASH is not a hash width reaches the receiver without panicking" {
    // The wire route into the guard above: a short HASH token frames a perfectly
    // legal payload, so nothing upstream of admission rejects it.
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var shun = FixtureShunSet{ .names = &.{s77_target} };
    var client = Client{
        .operation = .pull,
        .state = .exchange,
        .hash_policy = .shun_sha1,
        .shun = shun.shunSet(),
    };
    defer client.deinit(gpa);
    _ = try client.consumeReply(gpa, ms.store(), "file abc 3\nxyz");
    // Admitted, so the store layer saw it — whatever it already did with an
    // out-of-domain name is unchanged by §7.7.
    try std.testing.expect(ms.store().lookup("abc") != null);
}

test "sync §7.7 L1428-1432 distinction: admission is governed by the configured hash policy, not content" {
    const sha1_name = "a" ** 40;
    const admitting_policies = [_]artifact.HashPolicy{ .sha1, .auto, .sha3, .sha3_only };
    for (admitting_policies) |p| {
        try std.testing.expectEqual(Admission.admit, admitInbound(p, .none, sha1_name));
    }
    try std.testing.expectEqual(Admission.reject, admitInbound(.shun_sha1, .none, sha1_name));
}

test "sync §7.7 L1465-1466 r5: integrity recomputes over reconstructed bytes, not the wire delta payload" {
    const source = "the quick brown fox jumps over the lazy dog, repeatedly and at length";
    const target = "the quick brown fox jumps over the lazy dog, repeatedly and at length, plus more";
    const d = try delta.create(std.testing.allocator, source, target);
    defer std.testing.allocator.free(d);

    var buf: [64]u8 = undefined;
    const name = artifact.artifactName(.sha3_256, target, &buf);
    var name_copy: [64]u8 = undefined;
    @memcpy(&name_copy, name);

    const reconstructed = try delta.apply(std.testing.allocator, source, d);
    defer std.testing.allocator.free(reconstructed);

    try std.testing.expect(artifact.verify(&name_copy, reconstructed));
    try std.testing.expect(!artifact.verify(&name_copy, d)); // wire delta bytes, not reconstructed content
}

test "sync §7.7 L1466-1471 r6: pre-storage integrity on non-clone file cards; commit-time on all paths" {
    try std.testing.expectEqual(IntegritySchedule{ .pre_store = true, .at_commit = true }, integritySchedule(.file, false));
    try std.testing.expect(!integritySchedule(.file, true).pre_store);

    const ops = [_]Operator{ .file, .cfile };
    const paths = [_]bool{ false, true };
    for (ops) |op| {
        for (paths) |clone| {
            try std.testing.expect(integritySchedule(op, clone).at_commit);
        }
    }
}

test "sync §7.7 L1467-1471 r7: cfile pre-storage integrity does not apply in transfer; commit-time still applies" {
    try std.testing.expectEqual(IntegritySchedule{ .pre_store = false, .at_commit = true }, integritySchedule(.cfile, false));
    try std.testing.expectEqual(IntegritySchedule{ .pre_store = false, .at_commit = true }, integritySchedule(.cfile, true));
}

test "sync §7.3: uvfile parse takes no hash-policy dependency" {
    // A HASH token that is neither 40 nor 64 lowercase-hex (not a legal
    // artifact-hash form) still parses: §7.3-r1 places uvfile outside
    // [FossilArtifact]'s hash-policy rules; only verifyUvContent (r13)
    // ever judges HASH against content.
    const card = try parseUvfileCard(tokenizeCard("uvfile a.txt 100 NOT-A-HASH 0 4"));
    try std.testing.expectEqualStrings("NOT-A-HASH", card.hash);
}

test "sync §7.3: uvfile/uvigot/uvgimme card grammar fields" {
    // §7.3-r2: uvfile-card = "uvfile" SP fossilized SP decimal SP
    // uv-hash-field SP decimal SP decimal LF.
    const file = try parseUvfileCard(tokenizeCard("uvfile a.txt 1700000000 " ++ test_hash_a ++ " 12 0"));
    try std.testing.expectEqualStrings("a.txt", file.name);
    try std.testing.expectEqual(@as(i64, 1700000000), file.mtime);
    try std.testing.expectEqualStrings(test_hash_a, file.hash);
    try std.testing.expectEqual(@as(u32, 12), file.size);
    try std.testing.expectEqual(@as(u32, 0), file.flags);

    // uvigot-card = "uvigot" SP fossilized SP decimal SP uv-hash-field
    // SP decimal LF.
    const igot = try parseUvigotCard(tokenizeCard("uvigot a.txt 1700000000 " ++ test_hash_a ++ " 12"));
    try std.testing.expectEqualStrings("a.txt", igot.name);
    try std.testing.expectEqual(@as(i64, 1700000000), igot.mtime);
    try std.testing.expectEqualStrings(test_hash_a, igot.hash);
    try std.testing.expectEqual(@as(u32, 12), igot.size);

    // uvgimme-card = "uvgimme" SP fossilized LF.
    const gimme = try parseUvgimmeCard(tokenizeCard("uvgimme a.txt"));
    try std.testing.expectEqualStrings("a.txt", gimme.name);
}

test "sync §7.3: NAME is kept fossilize-escaped, not decoded" {
    // "a\sb" is the fossilize encoding of "a b" (§4.3, \s = SP); the
    // uvfile layer keeps the raw escaped token (r3) -- decoding is the
    // caller's job via artifact.decodeEscapes, not this parser's.
    const card = try parseUvfileCard(tokenizeCard("uvfile a\\sb 100 - 0 1"));
    try std.testing.expectEqualStrings("a\\sb", card.name);
}

test "sync §7.3: MTIME is decimal seconds since the epoch" {
    const card = try parseUvfileCard(tokenizeCard("uvfile a.txt 1700000000 - 0 1"));
    try std.testing.expectEqual(@as(i64, 1700000000), card.mtime);
}

test "sync §7.3: HASH is the content hash or literal -" {
    const with_hash = try parseUvfileCard(tokenizeCard("uvfile a.txt 100 " ++ test_hash_a ++ " 0 0"));
    try std.testing.expectEqualStrings(test_hash_a, with_hash.hash);
    const deletion = try parseUvfileCard(tokenizeCard("uvfile a.txt 100 - 0 1"));
    try std.testing.expectEqualStrings("-", deletion.hash);
}

test "sync §7.3: SIZE is the following payload byte count" {
    const body = "uvfile a.txt 100 " ++ test_hash_a ++ " 5 0\nhellotail";
    const line = try readLine(body, 0);
    const card = try parseUvfileCard(tokenizeCard(line.bytes));
    try std.testing.expectEqual(@as(u32, 5), card.size);
    const payload = try consumeUvfilePayload(body, line.next, card);
    try std.testing.expectEqualStrings("hello", payload.bytes);
}

test "sync §7.3 L1244-1246: deletion is decided by HASH == \"-\", not the 0x0001 bit" {
    const cases = [_]struct { line: []const u8, want: bool }{
        // HASH="-", 0x0001 CLEAR: still a deletion -- HASH decides, not the bit.
        .{ .line = "uvfile a 100 - 0 0", .want = true },
        // HASH="-", 0x0001 set (the sender-side metadata marker).
        .{ .line = "uvfile a 100 - 0 1", .want = true },
        // real HASH, 0x0001 set: NOT a deletion -- the bit alone never decides.
        .{ .line = "uvfile a 100 " ++ test_hash_a ++ " 0 1", .want = false },
        // ordinary content, no flags.
        .{ .line = "uvfile a 100 " ++ test_hash_a ++ " 42 0", .want = false },
    };
    for (cases) |c| {
        const card = try parseUvfileCard(tokenizeCard(c.line));
        try std.testing.expectEqual(c.want, card.isDeletion());
    }
}

test "sync §7.3 L1239-1240: FLAGS bit 0x0004 marks omitted content" {
    const omit = try parseUvfileCard(tokenizeCard("uvfile a.txt 100 " ++ test_hash_a ++ " 42 4"));
    try std.testing.expect(!omit.isDeletion());
    try std.testing.expect(omit.contentOmitted());

    const both = try parseUvfileCard(tokenizeCard("uvfile a.txt 100 - 0 5"));
    try std.testing.expect(both.isDeletion());
    try std.testing.expect(both.contentOmitted());
}

test "sync §7.3: uvfile payload-presence keys on the 0x0001/0x0004 FLAGS bits, not the HASH" {
    // A real-HASH card carrying the 0x0001 bit is non-compliant, but the
    // receiver frames the payload from the FLAGS bits: payload follows iff
    // SIZE>0 and (FLAGS & 0x0005)==0 (the deletion and omitted-content bits).
    // 0x0001 is set here, so NO payload follows and the bytes after the line
    // are the next card — matching the reference receiver. Delete-vs-update is
    // a separate decision that still keys on HASH (isDeletion stays false).
    const card = try parseUvfileCard(tokenizeCard("uvfile f 100 " ++ test_hash_a ++ " 42 1"));
    try std.testing.expect(!card.isDeletion());
    try std.testing.expect(!uvfilePayloadFollows(card));
    const body = "uvfile f 100 " ++ test_hash_a ++ " 42 1\n" ++ "next card";
    const line = try readLine(body, 0);
    const payload = try consumeUvfilePayload(body, line.next, card);
    try std.testing.expectEqual(@as(usize, 0), payload.bytes.len);
    try std.testing.expectEqual(line.next, payload.next);
}

test "sync §7.3: deletion uses HASH -, SIZE 0, deletion flag" {
    const card = try parseUvfileCard(tokenizeCard("uvfile f 100 - 0 1"));
    try std.testing.expectEqualStrings("-", card.hash);
    try std.testing.expectEqual(@as(u32, 0), card.size);
    try std.testing.expect(card.isDeletion());
    try std.testing.expect(!uvfilePayloadFollows(card));
}

test "sync §7.3: omitted-content transfer carries no payload despite SIZE" {
    const card = try parseUvfileCard(tokenizeCard("uvfile f 100 " ++ test_hash_a ++ " 42 4"));
    try std.testing.expect(!uvfilePayloadFollows(card));
    const body = "uvfile f 100 " ++ test_hash_a ++ " 42 4\nnext line";
    const line = try readLine(body, 0);
    const payload = try consumeUvfilePayload(body, line.next, card);
    try std.testing.expectEqual(@as(usize, 0), payload.bytes.len);
    try std.testing.expectEqual(line.next, payload.next);
}

test "sync §7.3: SIZE>0 no flags yields exactly SIZE payload bytes" {
    const body = "uvfile f 100 " ++ test_hash_a ++ " 5 0\nhelloXX";
    const line = try readLine(body, 0);
    const card = try parseUvfileCard(tokenizeCard(line.bytes));
    try std.testing.expect(uvfilePayloadFollows(card));
    const payload = try consumeUvfilePayload(body, line.next, card);
    try std.testing.expectEqualStrings("hello", payload.bytes);
}

test "sync §7.3: byte after payload begins the next card line" {
    const body = "uvfile f 100 " ++ test_hash_a ++ " 5 0\nhellouvgimme x";
    const line = try readLine(body, 0);
    const card = try parseUvfileCard(tokenizeCard(line.bytes));
    const payload = try consumeUvfilePayload(body, line.next, card);
    try std.testing.expectEqual(line.next + 5, payload.next);
    try std.testing.expectEqualStrings("uvgimme x", body[payload.next..]);
}

test "sync §7.3: omitted content leaves next card immediately after LF" {
    const body = "uvfile f 100 - 0 5\nuvgimme x";
    const line = try readLine(body, 0);
    const card = try parseUvfileCard(tokenizeCard(line.bytes));
    const payload = try consumeUvfilePayload(body, line.next, card);
    try std.testing.expectEqual(@as(usize, 0), payload.bytes.len);
    try std.testing.expectEqual(line.next, payload.next);
    try std.testing.expectEqualStrings("uvgimme x", body[payload.next..]);
}

test "sync §7.3: HASH is verified against bytes before storing (rejection)" {
    // RFC 3174 §7.3 test vector: SHA-1("abc") = a9993e364706816aba3e25717850c26c9cd0d89d.
    const sha1_abc = "a9993e364706816aba3e25717850c26c9cd0d89d";
    try std.testing.expect(verifyUvContent(sha1_abc, "abc"));
    try std.testing.expect(!verifyUvContent(sha1_abc, "wrong content"));
    try std.testing.expect(!verifyUvContent("not-a-legal-length-hash", "abc"));
}

test "sync §7.3: omitted card updates metadata only when status permits" {
    // §8.5.1 status 2 (older local, same hash): a metadata-only touch
    // is permitted from an omitted-content card, since it never claims
    // to carry file bytes (r14).
    const local: UvRow = .{ .name = "f", .mtime = 100, .hash = "same" };
    const newer_card: UvfileCard = .{ .name = "f", .mtime = 200, .hash = "same", .size = 42, .flags = uvfile_flag_omitted };
    try std.testing.expectEqual(UvLanding.land, uvLandDecision(local, newer_card, .{}, true));

    // Identical state: no update even though write-permitted.
    const identical_card: UvfileCard = .{ .name = "f", .mtime = 100, .hash = "same", .size = 42, .flags = uvfile_flag_omitted };
    try std.testing.expectEqual(UvLanding.no_op, uvLandDecision(local, identical_card, .{}, true));
}

test "sync §7.3: uvigot is the payload-free announcement form" {
    const card = try parseUvigotCard(tokenizeCard("uvigot f 100 " ++ test_hash_a ++ " 12"));
    try std.testing.expectEqualStrings("f", card.name);
    try std.testing.expectEqual(@as(i64, 100), card.mtime);
    try std.testing.expectEqualStrings(test_hash_a, card.hash);
    try std.testing.expectEqual(@as(u32, 12), card.size);
}

test "sync §7.3: a deletion is announced or transferred with HASH -" {
    const igot = try parseUvigotCard(tokenizeCard("uvigot f 100 - 0"));
    try std.testing.expectEqualStrings("-", igot.hash);
    const file = try parseUvfileCard(tokenizeCard("uvfile f 100 - 0 1"));
    try std.testing.expectEqualStrings("-", file.hash);
}

test "sync §7.3: uvgimme NAME requests a named unversioned file" {
    const card = try parseUvgimmeCard(tokenizeCard("uvgimme p/q%20r"));
    try std.testing.expectEqualStrings("p/q%20r", card.name);
}

test "sync §7.3: only a write-permitted receiver may land a uvfile (rejection)" {
    const card: UvfileCard = .{ .name = "f", .mtime = 200, .hash = "new", .size = 3, .flags = 0 };
    try std.testing.expectEqual(UvLanding.not_authorized, uvLandDecision(null, card, .{}, false));
    try std.testing.expectEqual(UvLanding.land, uvLandDecision(null, card, .{}, true));
}

test "sync §7.3: an accepted change clears the cached catalog hash" {
    try std.testing.expect(uvLandingClearsCatalog(.land));
    try std.testing.expect(!uvLandingClearsCatalog(.no_op));
    try std.testing.expect(!uvLandingClearsCatalog(.not_authorized));
}

test "sync §7.3: under-arity uvfile card is malformed" {
    try std.testing.expectError(CardParseError.Malformed, parseUvfileCard(tokenizeCard("uvfile f 100 - 0")));
}

test "sync §5.2/§7.3: uvfile ignores visible tokens after its six parser slots" {
    // §5.2 L964-967 / §7.3-r2 — `uvfile` consumes the fixed six-slot parser
    // window. Visible tokens beyond FLAGS do not make a valid prefix malformed.
    const card = try parseUvfileCard(tokenizeCard("uvfile n 1 - 0 0 ignored after flags"));
    try std.testing.expectEqualStrings("n", card.name);
    try std.testing.expectEqual(@as(i64, 1), card.mtime);
    try std.testing.expectEqualStrings("-", card.hash);
    try std.testing.expectEqual(@as(u32, 0), card.size);
    try std.testing.expectEqual(@as(u32, 0), card.flags);
}

test "sync §7.3: non-numeric MTIME/SIZE/FLAGS is malformed" {
    const cases = [_][]const u8{
        "uvfile f x - 0 0", // non-numeric MTIME
        "uvfile f 100 - x 0", // non-numeric SIZE
        "uvfile f 100 - 0 x", // non-numeric FLAGS
    };
    for (cases) |line| {
        try std.testing.expectError(CardParseError.Malformed, parseUvfileCard(tokenizeCard(line)));
    }
}

test "sync §7.3: uvigot wrong arity is malformed" {
    try std.testing.expectError(CardParseError.Malformed, parseUvigotCard(tokenizeCard("uvigot f 100 -")));
    try std.testing.expectError(CardParseError.Malformed, parseUvigotCard(tokenizeCard("uvigot f 100 - 0 extra")));
}

test "sync §7.3: uvgimme wrong arity is malformed" {
    try std.testing.expectError(CardParseError.Malformed, parseUvgimmeCard(tokenizeCard("uvgimme")));
    try std.testing.expectError(CardParseError.Malformed, parseUvgimmeCard(tokenizeCard("uvgimme f extra")));
}

test "sync §7.3/§7.5 receive: uvigot and uvgimme preserve a real private marker for later content" {
    const gpa = std.testing.allocator;

    // A real private card, not seeded internal state, must remain pending
    // across a valid but unsupported uvigot and bind the subsequent file.
    {
        var ms = repostate.MemStore.init(gpa);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange, .private_granted = false };
        defer client.deinit(gpa);

        // One round: the uv card between the marker and the content it must
        // not consume. (A uv-only reply is now an EMPTY round — the §3.7
        // clean terminal — so a split-body variant would end the session.)
        const obs = try client.consumeReply(gpa, ms.store(), "private\nuvigot a\\sb 1 - 0\nfile " ++ test_hash_a ++ " 5\nHELLO");
        try std.testing.expectEqual(@as(u64, 1), client.uv_skipped); // #58: counted, not errored
        try std.testing.expect(obs.private_content_discarded);
        try std.testing.expect(!obs.private_content_delivered);
        try std.testing.expect(ms.store().lookup(test_hash_a) == null);
        try std.testing.expect(!client.private_pending);
    }

    // The same marker survives uvgimme and binds a later cfile. The invalid
    // container proves the private discard path is observable before decoding.
    {
        const garbage = "not a fossil zlib container at all";
        const cfile = try buildCfileBody(gpa, test_hash_a, 99, garbage.len, garbage);
        defer gpa.free(cfile);
        var ms = repostate.MemStore.init(gpa);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange, .private_granted = false };
        defer client.deinit(gpa);

        const combined = try std.mem.concat(gpa, u8, &.{ "private\nuvgimme a\\sb\n", cfile });
        defer gpa.free(combined);
        const obs = try client.consumeReply(gpa, ms.store(), combined);
        try std.testing.expectEqual(@as(u64, 0), client.uv_skipped); // a request loses nothing here
        try std.testing.expect(obs.private_content_discarded);
        try std.testing.expect(!obs.private_content_delivered);
        try std.testing.expect(ms.store().lookup(test_hash_a) == null);
        try std.testing.expect(!client.private_pending);
    }
}
test "sync §7.3 receive: uvigot deletion with nonzero SIZE aborts before unsupported boundary" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);

    try std.testing.expectError(
        Client.ConsumeError.FatalProtocolError,
        client.consumeReply(gpa, ms.store(), "uvigot f 1 - 1\n"),
    );
    try std.testing.expectEqual(ClientState.aborted, client.state);
}
test "sync §7.3 receive: private followed by uvfile reaches the unsupported boundary and leaves the marker pending" {
    const gpa = std.testing.allocator;
    const empty_hash = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);

    _ = try client.consumeReply(gpa, ms.store(), "private\nuvfile a\\sb 1 " ++ empty_hash ++ " 0 0\n");
    try std.testing.expectEqual(@as(u64, 1), client.uv_skipped);
    // A reply of only-declined cards is an EMPTY round: the §3.7 clean
    // terminal, not an abort — the session ends having transferred all
    // versioned content (none here).
    try std.testing.expectEqual(ClientState.terminated, client.state);
    try std.testing.expect(client.private_pending);
    try std.testing.expect(ms.store().lookup(empty_hash) == null);
}
test "sync §7.3 receive: non-ABNF numeric spellings abort before unsupported boundary" {
    const gpa = std.testing.allocator;
    const cases = [_][]const u8{
        "uvfile f +1 - 0 0\n", // MTIME
        "uvfile f 1 - -0 0\n", // SIZE
        "uvfile f 1 - 0 +0\n", // FLAGS
        "uvfile f 1 - 0 1_0\n", // FLAGS
        "uvigot f +1 - 0\n", // MTIME
        "uvigot f 1 - -0\n", // SIZE
    };
    for (cases) |body| {
        var ms = repostate.MemStore.init(gpa);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange };
        defer client.deinit(gpa);

        try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(gpa, ms.store(), body));
        try std.testing.expectEqual(ClientState.aborted, client.state);
    }
}
test "sync §7.3 receive: oversized FLAGS preserve omitted-content framing" {
    const gpa = std.testing.allocator;
    const empty_hash = "da39a3ee5e6b4b0d3255bfef95601890afd80709";

    // 2^128 + 4 is an arbitrary-length decimal whose defined low bits are
    // 0x0004. Despite the nonzero SIZE, omitted content leaves no payload to
    // frame, so a valid uvfile reaches the deliberate unsupported boundary.
    {
        var ms = repostate.MemStore.init(gpa);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange };
        defer client.deinit(gpa);

        _ = try client.consumeReply(gpa, ms.store(), "uvfile f 1 " ++ empty_hash ++ " 1 340282366920938463463374607431768211460\n");
        try std.testing.expectEqual(@as(u64, 1), client.uv_skipped);
        try std.testing.expectEqual(ClientState.terminated, client.state); // empty round = clean stop
    }

    // 2^128 has no defined low bit. With the same nonzero SIZE and no
    // payload, framing must reject it as truncated before the unsupported
    // boundary; accepting an oversized token alone is insufficient.
    {
        var ms = repostate.MemStore.init(gpa);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange };
        defer client.deinit(gpa);

        try std.testing.expectError(
            Client.ConsumeError.FatalProtocolError,
            client.consumeReply(gpa, ms.store(), "uvfile f 1 " ++ empty_hash ++ " 1 340282366920938463463374607431768211456\n"),
        );
        try std.testing.expectEqual(ClientState.aborted, client.state);
    }
}

test "sync §7.3 receive: uvfile frames content and accepts omitted reserved and extra fields" {
    const gpa = std.testing.allocator;
    const empty_hash = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
    const abc_hash = "a9993e364706816aba3e25717850c26c9cd0d89d";
    const cases = [_][]const u8{
        "uvfile f 1 " ++ abc_hash ++ " 3 0\nabc",
        "uvfile f 1 - 0 0\n",
        "uvfile f 1 " ++ abc_hash ++ " 3 4\n",
        "uvfile f 1 " ++ abc_hash ++ " 3 8\nabc",
        "uvfile f 1 " ++ empty_hash ++ " 0 0 ignored after flags\n",
    };
    for (cases) |body| {
        var ms = repostate.MemStore.init(gpa);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange };
        defer client.deinit(gpa);

        _ = try client.consumeReply(gpa, ms.store(), body);
        try std.testing.expectEqual(@as(u64, 1), client.uv_skipped); // #58: counted, never an abort
        try std.testing.expectEqual(ClientState.terminated, client.state); // empty round = clean stop
        try std.testing.expectEqual(@as(usize, 0), client.send_queue.count());
        try std.testing.expect(ms.store().lookup(abc_hash) == null);
    }
}

test "sync §7.3 receive: malformed UV cards abort before the unsupported boundary" {
    const gpa = std.testing.allocator;
    const abc_hash = "a9993e364706816aba3e25717850c26c9cd0d89d";
    const cases = [_][]const u8{
        "uvfile f nope " ++ abc_hash ++ " 0 0\n",
        "uvfile f 1 not-a-hash 0 0\n",
        "uvigot f 1 not-a-hash 0\n",
        "uvfile f 1 - 3 4\n",
        "uvfile f 1 " ++ abc_hash ++ " 4 0\nabc",
        "uvfile f 1 " ++ abc_hash ++ " 3 0\nxyz",
        "uvfile f 1 " ++ abc_hash ++ " 4294967296 0\n",
        "uvigot f 1 - 0 extra\n",
        "uvgimme f extra\n",
    };
    for (cases) |body| {
        var ms = repostate.MemStore.init(gpa);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange };
        defer client.deinit(gpa);

        try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(gpa, ms.store(), body));
        try std.testing.expectEqual(ClientState.aborted, client.state);
    }
}
test "sync §7.3/§4.3 receive: a non-canonically fossilized NAME is not rejected" {
    // §7.3 L1226 — NAME is "encoded using the fossilize character-escaping
    // scheme"; the section enumerates its receiver rejections (negative
    // FLAGS, HASH - with SIZE>0, missing write permission) and licenses NONE
    // for a non-canonical NAME encoding. artifact §4.3 L657-663 makes the defossilize
    // decoder error-free on an unrecognized escape (\q -> q, no error), and
    // §5.2 L946 requires decode-before-use, not a canonical-form check.
    // `bad\q` is therefore a valid NAME and must reach the unsupported
    // boundary, not abort.
    const gpa = std.testing.allocator;
    const cases = [_]struct { body: []const u8, skipped: u64 }{
        .{ .body = "uvgimme bad\\q\n", .skipped = 0 },
        .{ .body = "uvfile bad\\q 1 - 0 0\n", .skipped = 1 },
        .{ .body = "uvigot bad\\q 1 - 0\n", .skipped = 1 },
    };
    for (cases) |case| {
        var ms = repostate.MemStore.init(gpa);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange };
        defer client.deinit(gpa);

        _ = try client.consumeReply(gpa, ms.store(), case.body);
        try std.testing.expectEqual(case.skipped, client.uv_skipped);
        try std.testing.expectEqual(ClientState.terminated, client.state); // empty round = clean stop
    }
}
test "sync §8.5.1: send table init loads non-null-hash rows, mtimeOnly false, catalog-sent false" {
    const rows = [_]UvRow{
        .{ .name = "a.txt", .mtime = 1, .hash = "h1" },
        .{ .name = "b.txt", .mtime = 2, .hash = null },
    };
    var table = try UvSendTable.initFromLocal(std.testing.allocator, &rows);
    defer table.deinit(std.testing.allocator);
    try std.testing.expectEqual(@as(usize, 1), table.count());
    try std.testing.expectEqual(@as(?bool, false), table.mtimeOnly("a.txt"));
    try std.testing.expect(!table.contains("b.txt"));

    const s: UvSession = .{};
    try std.testing.expectEqual(false, s.hash_sent);
}

test "sync §8.5.1: catalog hash of an empty catalog is SHA-1(\"\")" {
    const hash = uvCatalogHash(&.{});
    try std.testing.expectEqualStrings("da39a3ee5e6b4b0d3255bfef95601890afd80709", &hash);
}

test "sync §8.5.1: null-hash and deleted rows are excluded from the catalog hash" {
    const deleted_only = [_]UvRow{
        .{ .name = "a.txt", .mtime = 1, .hash = null },
        .{ .name = "b.txt", .mtime = 2, .hash = null },
    };
    const h1 = uvCatalogHash(&deleted_only);
    try std.testing.expectEqualStrings("da39a3ee5e6b4b0d3255bfef95601890afd80709", &h1);

    const with_one_live = [_]UvRow{
        .{ .name = "a.txt", .mtime = 1, .hash = null },
        .{ .name = "b.txt", .mtime = 2, .hash = "h2" },
    };
    const h2 = uvCatalogHash(&with_one_live);
    try std.testing.expect(!std.mem.eql(u8, &h1, &h2));
}

test "sync §8.5.1: server suppresses duplicate catalog emission within one request" {
    var s: ServerUvRequest = .{};
    try std.testing.expectEqual(UvResponse.push_ok, serverUvCatalog(&s, false, true, true));
    try std.testing.expectEqual(UvResponse.none, serverUvCatalog(&s, false, true, true));

    var s2: ServerUvRequest = .{};
    try std.testing.expectEqual(UvResponse.push_ok, serverUvCatalog(&s2, false, true, true));
}

test "sync §8.5.1: status 0 when no local row exists" {
    try std.testing.expectEqual(UvStatus.absent, uvStatus(null, 100, "abc"));
}

test "sync §8.5.1: LOCAL-HASH is - for a null local hash; HASH-CMP compares it bytewise" {
    const row: UvRow = .{ .name = "a", .mtime = 100, .hash = null };
    try std.testing.expectEqual(UvStatus.identical, uvStatus(row, 100, "-"));
    try std.testing.expectEqual(UvStatus.remote_wins, uvStatus(row, 100, "abc"));
}

test "sync §8.5.1: HASH-CMP 0 yields status 3 + MTIME-CMP" {
    const Case = struct { local_mtime: i64, remote_mtime: i64, want: UvStatus };
    const cases = [_]Case{
        .{ .local_mtime = 100, .remote_mtime = 200, .want = .older },
        .{ .local_mtime = 150, .remote_mtime = 150, .want = .identical },
        .{ .local_mtime = 300, .remote_mtime = 200, .want = .newer },
    };
    for (cases) |c| {
        const row: UvRow = .{ .name = "a", .mtime = c.local_mtime, .hash = "same-hash" };
        try std.testing.expectEqual(c.want, uvStatus(row, c.remote_mtime, "same-hash"));
    }
}

test "sync §8.5.1: status 1 when MTIME-CMP<0, or MTIME-CMP 0 and HASH-CMP<0" {
    const older: UvRow = .{ .name = "a", .mtime = 100, .hash = "aaa" };
    try std.testing.expectEqual(UvStatus.remote_wins, uvStatus(older, 200, "zzz"));
    const tie: UvRow = .{ .name = "a", .mtime = 100, .hash = "aaa" };
    try std.testing.expectEqual(UvStatus.remote_wins, uvStatus(tie, 100, "zzz"));
}

test "sync §8.5.1: status 5 otherwise (local content or deletion overrides)" {
    const newer: UvRow = .{ .name = "a", .mtime = 300, .hash = "zzz" };
    try std.testing.expectEqual(UvStatus.local_wins, uvStatus(newer, 200, "aaa"));
    const tie: UvRow = .{ .name = "a", .mtime = 100, .hash = "zzz" };
    try std.testing.expectEqual(UvStatus.local_wins, uvStatus(tie, 100, "aaa"));
}

test "sync §8.5.1: revert mode maps status 4 to 2 and status 5 to 1" {
    const newer: UvRow = .{ .name = "a", .mtime = 300, .hash = "same" };
    const d4 = uvDecide(newer, 200, "same", .{ .revert = true }, false);
    try std.testing.expectEqual(UvStatus.older, d4.status);

    const local_wins: UvRow = .{ .name = "a", .mtime = 300, .hash = "zzz" };
    const d5 = uvDecide(local_wins, 200, "aaa", .{ .revert = true }, false);
    try std.testing.expectEqual(UvStatus.remote_wins, d5.status);
}

test "sync §8.5.1: dry-run suppresses local changes and reply messages" {
    const absent = uvDecide(null, 100, "abc", .{ .dry_run = true }, false);
    try std.testing.expectEqual(UvLocalAction.none, absent.local);

    const remote_row: UvRow = .{ .name = "a", .mtime = 100, .hash = "aaa" };
    const wins = uvDecide(remote_row, 200, "zzz", .{ .dry_run = true }, false);
    try std.testing.expectEqual(UvLocalAction.none, wins.local);

    const older_row: UvRow = .{ .name = "a", .mtime = 100, .hash = "same" };
    const o = uvDecide(older_row, 200, "same", .{ .dry_run = true }, false);
    try std.testing.expectEqual(UvLocalAction.none, o.local);
}

test "sync §8.5.1: send-table apply removes, or sets mtimeOnly true/false" {
    var table: UvSendTable = .{};
    defer table.deinit(std.testing.allocator);
    try table.apply(std.testing.allocator, "a.txt", .mtime_only);
    try std.testing.expectEqual(@as(?bool, true), table.mtimeOnly("a.txt"));
    try table.apply(std.testing.allocator, "a.txt", .keep_full);
    try std.testing.expectEqual(@as(?bool, false), table.mtimeOnly("a.txt"));
    try table.apply(std.testing.allocator, "a.txt", .remove);
    try std.testing.expect(!table.contains("a.txt"));
    try std.testing.expect(table.isEmpty());
}

fn uvSendTableNoLeak(gpa: std.mem.Allocator) !void {
    const rows = [_]UvRow{
        .{ .name = "a.txt", .mtime = 1, .hash = "h1" },
        .{ .name = "b.txt", .mtime = 2, .hash = null },
    };
    var table = try UvSendTable.initFromLocal(gpa, &rows);
    defer table.deinit(gpa);
    try table.apply(gpa, "a.txt", .mtime_only);
    try table.apply(gpa, "c.txt", .keep_full);
    try table.apply(gpa, "a.txt", .remove);
}

test "sync §8.5.1: send table init and apply survive allocation failure" {
    try std.testing.checkAllAllocationFailures(std.testing.allocator, uvSendTableNoLeak, .{});
}

test "sync §8.5.1: uvgimme answered uvfile with content else uvigot" {
    try std.testing.expectEqual(UvGimmeReply.uvfile, uvGimmeReply(true));
    try std.testing.expectEqual(UvGimmeReply.uvigot, uvGimmeReply(false));
}

test "sync §8.5.1: uvfile landing rechecks the same status function" {
    // remote wins (status 0, absent locally) + write-permitted -> land.
    const absent_card: UvfileCard = .{ .name = "f", .mtime = 100, .hash = test_hash_a, .size = 3, .flags = 0 };
    try std.testing.expectEqual(UvLanding.land, uvLandDecision(null, absent_card, .{}, true));

    // local wins (status 5) -> no_op even though write-permitted.
    const local: UvRow = .{ .name = "f", .mtime = 300, .hash = "zzz" };
    const local_wins_card: UvfileCard = .{ .name = "f", .mtime = 200, .hash = "aaa", .size = 3, .flags = 0 };
    try std.testing.expectEqual(UvLanding.no_op, uvLandDecision(local, local_wins_card, .{}, true));

    // write not permitted -> not_authorized regardless of status.
    try std.testing.expectEqual(UvLanding.not_authorized, uvLandDecision(null, absent_card, .{}, false));
}

test "sync §9.2.1 r1: sync defines no branch/tag/leaf/closed/trunk wire operator" {
    // L2285-2287: "The sync protocol MUST NOT treat an igot, gimme, file, or
    // cfile card as an independent branch/tag operation, and it does not
    // define separate wire cards for those views."
    const view_tokens = [_][]const u8{ "branch", "tag", "leaf", "closed", "trunk" };
    for (view_tokens) |token| {
        try std.testing.expectEqual(@as(?Operator, null), parseOperator(token));
    }
    const carrier_tokens = [_][]const u8{ "igot", "gimme", "file", "cfile" };
    const carrier_ops = [_]Operator{ .igot, .gimme, .file, .cfile };
    for (carrier_tokens, carrier_ops) |token, want| {
        try std.testing.expectEqual(@as(?Operator, want), parseOperator(token));
    }
}

test "sync §9.2.1 r2: a view answer requires the carrying artifact to be crosslinked (stored)" {
    // L2307-2309: "may rely on ... answers for the synchronized scope only
    // after the artifacts that carry the relevant structural cards have
    // been stored."
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var rs = repostate.RepoState.init(std.testing.allocator, ms.store());
    defer rs.deinit();

    try std.testing.expectError(error.NotFound, rs.currentTrunk("trunk"));
    try std.testing.expectError(error.NotFound, rs.resolveName("trunk"));

    const rid = try rs.crosslink(.{ .hash = test_hash_a, .mtime = 1, .tags = &.{
        .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" },
    } });
    try std.testing.expectEqual(rid, try rs.currentTrunk("trunk"));
}

test "sync §9.2.1 r3: a delta base retained as a pending phantom does not error the view" {
    // L2310-2311: "required delta bases have been resolved or retained as
    // pending phantoms."
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var rs = repostate.RepoState.init(std.testing.allocator, ms.store());
    defer rs.deinit();
    const store = ms.store();

    // test_hash_a (the primary parent / delta base) is never crosslinked or
    // stored — it stays a pending phantom.
    const child = try rs.crosslink(.{ .hash = test_hash_b, .mtime = 5, .parents = &.{test_hash_a}, .tags = &.{
        .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" },
    } });
    const parent_rid = try store.intern(test_hash_a);
    try std.testing.expectEqual(@as(?[]const u8, null), store.content(parent_rid));
    try std.testing.expectEqual(child, try rs.currentTrunk("trunk"));
}

test "sync §9.2.1 r4: storing artifact bytes without crosslink processing yields no view" {
    // L2311-2312: "and repository crosslink/tag-state processing has run
    // for the stored artifacts." Bytes stored (r2 satisfied) but crosslink
    // never called: still no view — necessity witness distinct from r2.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var rs = repostate.RepoState.init(std.testing.allocator, ms.store());
    defer rs.deinit();
    const store = ms.store();

    _ = try store.put(test_hash_a, "manifest bytes, never crosslinked");
    try std.testing.expectError(error.NotFound, rs.resolveName("trunk"));
    try std.testing.expectError(error.NotFound, rs.currentTrunk("trunk"));

    const rid = try rs.crosslink(.{ .hash = test_hash_a, .mtime = 1, .tags = &.{
        .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" },
    } });
    try std.testing.expectEqual(rid, try rs.currentTrunk("trunk"));
}

test "sync §9.2.1 r5: reliable view holds only when stored, base-settled, and crosslink all hold" {
    // L2307-2312, the full conjunction: the single case where all three
    // preconditions hold is the meaningful, positive-space view.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var rs = repostate.RepoState.init(std.testing.allocator, ms.store());
    defer rs.deinit();

    const child = try rs.crosslink(.{ .hash = test_hash_b, .mtime = 5, .parents = &.{test_hash_a}, .tags = &.{
        .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" },
    } });
    try std.testing.expect(rs.isLeaf(child));
    try std.testing.expectEqual(child, try rs.currentTrunk("trunk"));
    try std.testing.expectEqual(try rs.resolveName("trunk"), try rs.currentTrunk("trunk"));
}

test "sync §9.2.1 r6: an announced-only hash proves no final leaf/closed/trunk view (rejection)" {
    // L2312-2314: "Before that point, the wire stream only proves that the
    // peer announced or sent artifact hashes; it does not prove a final
    // open-leaf, leaf, closed-marker, or current-trunk view."
    const test_hash_c = "c" ** 40;
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var rs = repostate.RepoState.init(std.testing.allocator, ms.store());
    defer rs.deinit();
    const store = ms.store();

    const rid = try store.intern(test_hash_c);
    try std.testing.expect(!rs.isLeaf(rid));
    try std.testing.expect(!rs.isOpenLeaf(rid));
    try std.testing.expectError(error.NotFound, rs.resolveName("trunk"));
}

test "sync §9.2.1 r7: each peer derives current-trunk from its own main-branch setting" {
    // L2300-2303: "the protocol transports only the artifact inputs; each
    // peer derives current-trunk locally, using its own main-branch
    // setting."
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var rs = repostate.RepoState.init(std.testing.allocator, ms.store());
    defer rs.deinit();

    _ = try rs.crosslink(.{ .hash = test_hash_a, .mtime = 1, .tags = &.{
        .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" },
    } });
    _ = try rs.crosslink(.{ .hash = test_hash_b, .mtime = 2, .tags = &.{
        .{ .type = .propagate, .name = "sym-release", .target = null, .value = "release" },
    } });
    try std.testing.expectEqual(try rs.resolveName("trunk"), try rs.currentTrunk("trunk"));
    try std.testing.expectEqual(try rs.resolveName("release"), try rs.currentTrunk("release"));
}

test "sync §9.2.1 r8: two main-branch settings resolve different current-trunks (worked example)" {
    // L2303-2305: "Current-trunk is therefore not a pure wire-synchronized
    // value: two synchronized peers can resolve different current-trunks
    // when their main-branch settings differ."
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var rs = repostate.RepoState.init(std.testing.allocator, ms.store());
    defer rs.deinit();

    _ = try rs.crosslink(.{ .hash = test_hash_a, .mtime = 1, .tags = &.{
        .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" },
    } });
    _ = try rs.crosslink(.{ .hash = test_hash_b, .mtime = 2, .tags = &.{
        .{ .type = .propagate, .name = "sym-release", .target = null, .value = "release" },
    } });
    try std.testing.expect(try rs.currentTrunk("trunk") != try rs.currentTrunk("release"));
}

test "sync §9.2.1 r9: an incomplete-closure view is not a sync error and does not force a round (rejection)" {
    // L2316-2322: "Later receipt of missing artifacts can change ...
    // current-trunk answers ... This is not a separate sync error; it
    // follows from the normal phantom termination rule in Section 3.7."
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var rs = repostate.RepoState.init(std.testing.allocator, ms.store());
    defer rs.deinit();

    // test_hash_a (the primary parent) stays a pending phantom, exactly as
    // in r3 — the view is computed over the closure available so far.
    _ = try rs.crosslink(.{ .hash = test_hash_b, .mtime = 5, .parents = &.{test_hash_a}, .tags = &.{
        .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" },
    } });
    _ = try rs.currentTrunk("trunk"); // resolves without a sync-layer error

    const obs = RoundObservation{ .operation = .pull, .phantom_table_nonempty = true };
    try std.testing.expect(!continueExchange(obs));
}

const elig_clear = Eligibility{ .peer_lacks = false, .shunned = false, .private = false, .private_sync_enabled = false };

test "sync §9.3: igot for an unheld obtainable artifact creates a phantom" {
    // §3.3 item 1, L511-514 — igot creates a phantom iff the receiver is
    // unheld and in an obtaining operation state; else it is ignored.
    const Case = struct { side: Side, op: Operation, already_holds: bool, want: IgotDisposition };
    const cases = [_]Case{
        .{ .side = .client, .op = .pull, .already_holds = false, .want = .create_phantom },
        .{ .side = .client, .op = .clone, .already_holds = false, .want = .create_phantom },
        .{ .side = .server, .op = .push, .already_holds = false, .want = .create_phantom },
        .{ .side = .client, .op = .pull, .already_holds = true, .want = .ignore }, // held
        .{ .side = .client, .op = .push, .already_holds = false, .want = .ignore }, // not obtaining
        .{ .side = .server, .op = .pull, .already_holds = false, .want = .ignore }, // not obtaining
    };
    for (cases) |c| try std.testing.expectEqual(c.want, igotDisposition(c.side, c.op, c.already_holds));
}

test "sync §9.3: a file/cfile card with an unheld DELTASRC creates a phantom for the source" {
    // §3.3 L513-514 — a file or cfile whose DELTASRC is not held
    // creates a phantom for that source; interning it leaves content absent.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const store = ms.store();

    const fc = try parseFileCard(tokenizeCard("file " ++ test_hash_a ++ " " ++ test_hash_b ++ " 10"));
    try std.testing.expectEqualStrings(test_hash_b, fc.deltasrc.?);
    const frid = try store.intern(fc.deltasrc.?);
    try std.testing.expectEqual(@as(?[]const u8, null), store.content(frid));

    const cc = try parseCfileCard(tokenizeCard("cfile " ++ test_hash_a ++ " " ++ test_hash_b ++ " 100 40"));
    try std.testing.expectEqualStrings(test_hash_b, cc.deltasrc.?);
    const crid = try store.intern(cc.deltasrc.?);
    try std.testing.expectEqual(@as(?[]const u8, null), store.content(crid));
}

test "sync §9.3: an igot-created phantom is gimme-eligible under default filters" {
    // §3.3 L517-518 — "the receiver MUST make that phantom eligible for
    // gimme emission"; with no filter set it is eligible.
    try std.testing.expectEqual(IgotDisposition.create_phantom, igotDisposition(.client, .pull, false));
    try std.testing.expect(gimmeEligible(elig_clear));
}

test "sync §9.3: a phantom is deleted when its content is stored" {
    // §3.3 L537 — the normal-path atomic upgrade: storing content
    // removes the phantom (content(rid) transitions null -> present).
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const store = ms.store();
    const rid = try store.intern(test_hash_a);
    try std.testing.expectEqual(@as(?[]const u8, null), store.content(rid));
    _ = try store.put(test_hash_a, "stored content");
    try std.testing.expect(store.content(rid) != null);
}

test "sync §9.3: dangling-delta HASH is content-received and not re-requested while DELTASRC is pending" {
    // §3.3 L537-541 — the dangling-delta path: "the delta payload for HASH
    // is stored as received, the phantom record for HASH is deleted, and a
    // phantom record is created for DELTASRC ... The content-received flag is
    // set in both paths." OPTION B derives content-received at the sync layer
    // (hasTarget), so HASH drops out of gimme while its DELTASRC stays a
    // genuine phantom and is re-requested.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const store = ms.store();

    const src_bytes = "the DELTASRC artifact's eventual stored content";
    const hash_bytes = "the dangling target, reconstructed from the basis, a bit longer";
    const delta_bytes = try delta.create(std.testing.allocator, src_bytes, hash_bytes);
    defer std.testing.allocator.free(delta_bytes);

    // HASH and SRC both arrive as phantoms; the SRC-basis delta is retained.
    _ = try store.intern(test_hash_a); // HASH phantom, deleted below on receipt
    _ = try store.intern(test_hash_b); // DELTASRC phantom (§3.3 item 2)
    var queue = PendingDeltaQueue{};
    defer queue.deinit(std.testing.allocator);
    try queue.retain(std.testing.allocator, test_hash_a, test_hash_b, .{ .raw = delta_bytes });

    // Pre-basis: HASH is content-received (a retained delta targets it), SRC
    // is a genuine phantom (no retained payload).
    try std.testing.expect(queue.hasTarget(test_hash_a));
    try std.testing.expect(!queue.hasTarget(test_hash_b));
    // The composed gimme gate `!content_received and gimmeEligible`: HASH is
    // excluded, exactly SRC is re-requested.
    try std.testing.expect(!(!queue.hasTarget(test_hash_a) and gimmeEligible(elig_clear)));
    try std.testing.expect(!queue.hasTarget(test_hash_b) and gimmeEligible(elig_clear));

    // The basis arrives: the delta reconstructs, the entry drains.
    _ = try store.put(test_hash_b, src_bytes);
    try queue.completeFor(std.testing.allocator, store, test_hash_b, .unrestricted);

    // Post-basis: the queue entry is gone AND HASH now holds reconstructed
    // content, so both halves of the disjunction still exclude it from gimme.
    try std.testing.expect(!queue.hasTarget(test_hash_a));
    const hash_rid = try store.intern(test_hash_a);
    try std.testing.expect(store.content(hash_rid) != null);
    try std.testing.expectEqualStrings(hash_bytes, store.content(hash_rid).?);
    try std.testing.expect(!(store.content(hash_rid) == null and !queue.hasTarget(test_hash_a) and gimmeEligible(elig_clear)));
}

test "sync §9.3: duplicate retained targets keep HASH content-received until the LAST entry drains" {
    // Two retained deltas may name the same target off different bases (a
    // peer is free to send both). `hasTarget` is counted membership: the
    // target stays content-received while ANY entry survives, and clears
    // only when the last one completes.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    const store = ms.store();

    const test_hash_c = "c" ** 40;
    const src_a = "first basis bytes for the duplicated target artifact";
    const src_b = "second basis bytes, different from the first entirely";
    const target = "the shared target content both deltas reconstruct to!";
    const d_a = try delta.create(std.testing.allocator, src_a, target);
    defer std.testing.allocator.free(d_a);
    const d_b = try delta.create(std.testing.allocator, src_b, target);
    defer std.testing.allocator.free(d_b);

    _ = try store.intern(test_hash_a); // the shared target
    _ = try store.intern(test_hash_b); // basis 1
    _ = try store.intern(test_hash_c); // basis 2
    var queue = PendingDeltaQueue{};
    defer queue.deinit(std.testing.allocator);
    try queue.retain(std.testing.allocator, test_hash_a, test_hash_b, .{ .raw = d_a });
    try queue.retain(std.testing.allocator, test_hash_a, test_hash_c, .{ .raw = d_b });
    try std.testing.expect(queue.hasTarget(test_hash_a));
    try std.testing.expectEqual(@as(usize, 2), queue.itemCount());

    // The first basis arrives: one entry drains, the target STAYS
    // content-received through the survivor.
    _ = try store.put(test_hash_b, src_a);
    try queue.completeFor(std.testing.allocator, store, test_hash_b, .unrestricted);
    try std.testing.expectEqual(@as(usize, 1), queue.itemCount());
    try std.testing.expect(queue.hasTarget(test_hash_a));

    // The second arrives: the queue empties and membership clears.
    _ = try store.put(test_hash_c, src_b);
    try queue.completeFor(std.testing.allocator, store, test_hash_c, .unrestricted);
    try std.testing.expectEqual(@as(usize, 0), queue.itemCount());
    try std.testing.expect(!queue.hasTarget(test_hash_a));
}

test "sync §9.3: a strategy requests missing content only via igot+gimme (single rule surface)" {
    // §8.5 L1633-1635 — the only path to request missing content: igot
    // creates the phantom, gimme requests it under the §3.3 filters. No
    // other operator originates a content request.
    try std.testing.expectEqual(IgotDisposition.create_phantom, igotDisposition(.client, .pull, false));
    try std.testing.expect(gimmeEligible(elig_clear)); // would emit gimme HASH
    // The inbound mirror governs the reply; held+sendable+disclosable sends.
    try std.testing.expectEqual(GimmeResponse.send, gimmeResponse(true, true, true));
}

test "sync §9.4: the per-round peer-lacks, shun, and private filters are applied" {
    // §8.8 L1777-1779 + §3.3 L527-529 — each filter alone
    // excludes; all-clear is eligible.
    const Case = struct { e: Eligibility, want: bool };
    const cases = [_]Case{
        .{ .e = elig_clear, .want = true },
        .{ .e = .{ .peer_lacks = true, .shunned = false, .private = false, .private_sync_enabled = false }, .want = false },
        .{ .e = .{ .peer_lacks = false, .shunned = true, .private = false, .private_sync_enabled = false }, .want = false },
        .{ .e = .{ .peer_lacks = false, .shunned = false, .private = true, .private_sync_enabled = false }, .want = false },
        .{ .e = .{ .peer_lacks = false, .shunned = false, .private = true, .private_sync_enabled = true }, .want = true },
    };
    for (cases) |c| try std.testing.expectEqual(c.want, gimmeEligible(c.e));
}

test "sync §9.4: per-round filters are not persisted across rounds" {
    // §8.8 L1778-1779 + §3.1 r20/r21 — a round boundary resets both tables.
    var rt = RoundTables.init(std.testing.allocator);
    defer rt.deinit();
    try rt.recordPeerLacks(test_hash_a);
    try rt.recordPeerHas(test_hash_b);
    rt.reset();
    try std.testing.expect(!rt.peerLacks(test_hash_a));
    try std.testing.expect(!rt.peerHas(test_hash_b));
}

test "sync §9.4: peer-lacks hashes are excluded from gimme (rejection)" {
    // §3.3 L527 — the ordinary phantom scan excludes peer-lacks hashes.
    try std.testing.expect(!gimmeEligible(.{ .peer_lacks = true, .shunned = false, .private = false, .private_sync_enabled = false }));
}

test "sync §9.4: shunned hashes are excluded from gimme (rejection)" {
    // §3.3 L527-528 — the scan excludes names in the repository shun table.
    try std.testing.expect(!gimmeEligible(.{ .peer_lacks = false, .shunned = true, .private = false, .private_sync_enabled = false }));
}

test "sync §9.4: private hashes excluded unless private sync enabled (rejection)" {
    // §3.3 L527-529 — the scan excludes private hashes unless private sync is enabled.
    try std.testing.expect(!gimmeEligible(.{ .peer_lacks = false, .shunned = false, .private = true, .private_sync_enabled = false }));
    try std.testing.expect(gimmeEligible(.{ .peer_lacks = false, .shunned = false, .private = true, .private_sync_enabled = true }));
}

test "sync §9.4: the eligibility filter is evaluated freshly each round" {
    // §3.3 L516-517 — peer-lacks is per-round state. Record peer-lacks(H)
    // this round -> excluded; reset the round -> eligible again.
    var rt = RoundTables.init(std.testing.allocator);
    defer rt.deinit();
    try rt.recordPeerLacks(test_hash_a);
    try std.testing.expect(!gimmeEligible(.{ .peer_lacks = rt.peerLacks(test_hash_a), .shunned = false, .private = false, .private_sync_enabled = false }));
    rt.reset();
    try std.testing.expect(!rt.peerLacks(test_hash_a));
    try std.testing.expect(gimmeEligible(.{ .peer_lacks = rt.peerLacks(test_hash_a), .shunned = false, .private = false, .private_sync_enabled = false }));
}

test "sync §9.4: receiving gimme records the peer lacks the hash" {
    // §3.3 L532 — a receiver that gets gimme HASH records peer-lacks HASH.
    var rt = RoundTables.init(std.testing.allocator);
    defer rt.deinit();
    try rt.recordPeerLacks(test_hash_a);
    try std.testing.expect(rt.peerLacks(test_hash_a));
    try std.testing.expect(!rt.peerLacks(test_hash_b));
}

test "sync §9.4: gimme for unheld content is silently skipped (rejection)" {
    // §3.3 L535 — unheld gimme is a silent skip, no error card.
    try std.testing.expectEqual(GimmeResponse.silent_skip, gimmeResponse(false, true, true));
    try std.testing.expectEqual(GimmeResponse.silent_skip, gimmeResponse(false, false, false));
}

test "sync §9.4: gimme for held-but-unsendable content emits nothing, no defer (rejection)" {
    // §3.3 L532-535 — held but the direction forbids sending: no content, no
    // error card, no deferred send.
    try std.testing.expectEqual(GimmeResponse.silent_skip, gimmeResponse(true, false, true));
    try std.testing.expectEqual(GimmeResponse.silent_skip, gimmeResponse(true, false, false));
}

test "sync §9.5: loop termination follows §3.7 including clone continuation (e)" {
    // §8.9 L1783-1784 + §3.7 L603-607 — termination is the §3.7 rule, clone (e) included.
    const Case = struct { obs: RoundObservation, want: bool };
    const cases = [_]Case{
        .{ .obs = .{ .operation = .clone, .first_clone_round = true }, .want = true }, // (e)
        .{ .obs = .{ .operation = .clone, .artifact_received_this_round = true }, .want = true }, // (e)
        .{ .obs = .{ .operation = .clone, .clone_seqno_next = 5, .artifact_count_grew = true }, .want = true }, // (e)
        .{ .obs = .{ .operation = .clone }, .want = false }, // clone, none of (e) -> stop
        .{ .obs = .{ .operation = .pull, .versioned_content_delivered = true, .phantom_table_nonempty = true }, .want = true }, // (a)
    };
    for (cases) |c| try std.testing.expectEqual(c.want, continueExchange(c.obs));
}

test "sync §9.5: termination reads the per-round content-received flag, not per-artifact phantom state (rejection)" {
    // §3.7 L621 + §9.5 — the client MUST NOT continue solely because
    // phantoms remain. Continuation is driven by the per-round content-
    // received flag (`versioned_content_delivered`, set by BOTH the normal
    // and the dangling-delta path per §3.3 L537-539), never per-artifact
    // store state. `continueExchange` takes only a `RoundObservation`, so it
    // structurally cannot consult the phantom set.
    const stalled = RoundObservation{ .operation = .pull, .phantom_table_nonempty = true };
    try std.testing.expect(!continueExchange(stalled)); // unresolved phantoms alone -> stop

    const progressed = RoundObservation{ .operation = .pull, .versioned_content_delivered = true, .phantom_table_nonempty = true };
    try std.testing.expect(continueExchange(progressed)); // (a) via the per-round flag
}

test "sync §9.5: the server signals no completion and holds no per-session termination state" {
    // §8.9 L1783-1784 + §3.7 L621-622 — the reply's closing comment carries
    // only a timestamp and an error count; termination is decided solely by
    // the client's `continueExchange` over its own per-round observation.
    const tc = parseTimestampComment("# timestamp 2026-07-10T00:00:00 errors 0").?;
    try std.testing.expectEqualStrings("2026-07-10T00:00:00", tc.datetime);
    try std.testing.expectEqual(@as(u64, 0), tc.errors);
    // TimestampComment carries exactly {datetime, errors} — no completion field.
    try std.testing.expectEqual(@as(usize, 2), @typeInfo(TimestampComment).@"struct".fields.len);
    // With no pending work the client stops on its own, no server input.
    try std.testing.expect(!continueExchange(.{ .operation = .sync }));
}

// ---------------------------------------------------------------------------
// Section 3.2 — Reply consumption (Client.consumeReply); Appendix A worked
// exchanges A.1 (push/pull convergence), A.2 (SHA3 gate error card), A.4
// (comment lines); §7.5 (private-marker state machine, r1-r8).
// ---------------------------------------------------------------------------

// §A.1 L2469-2474 — Round 1 reply, 367-byte body: server-version pragma, two
// gimme cards, two igot cards, closing timestamp. The vector holds raw wire
// bytes; the citation lives here, never inside the .txt.
const a1_round1_reply = @embedFile("rfc_vectors/sync_a1_round1_reply.txt");
// §A.1 L2505-2516 — Round 2 reply, 533-byte body: server-version pragma, two
// full-content file cards (270- and 31-byte payloads), closing timestamp.
const a1_round2_reply = @embedFile("rfc_vectors/sync_a1_round2_reply.txt");
// §A.2 L2542-2545 — SHA3 gate response, 191-byte body: error, clone_seqno 0,
// push, closing timestamp.
const a2_reply = @embedFile("rfc_vectors/sync_a2_reply.txt");

comptime {
    // The RFC states these body sizes directly (L3074, L3126, L3204); a
    // transcription drift trips here before any test runs.
    std.debug.assert(a1_round1_reply.len == 367);
    std.debug.assert(a1_round2_reply.len == 533);
    std.debug.assert(a2_reply.len == 191);
}

// §A.1 hashes: server gimme -> peer-lacks (L2470-2471); the ancestor the
// server already holds (igot L2453, never gimme'd; P parent L2487).
const a1_gimme_1 = "8e0933ed1ef9aa0051d185c8123f0b9ac79865e4b70098b877543fca0092c259";
const a1_gimme_2 = "b1c1b05fea4f52bd90b15fdffd13f5c91dce6505f498eeb79b4952c2ea8d4d15";
const a1_held_ancestor = "c5b2e0ecb779a7616b32553d973bc9c4ae5a07569a2cee18001386da6bd9ae57";

test "sync §A.1: round-1 reply creates two phantoms from the server igot cards" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), a1_round1_reply);
    // L3093-3094 — two new phantoms 4361252d…/f5116517…, keyed by interned rid.
    try std.testing.expectEqual(@as(usize, 2), client.phantoms.count());
    try std.testing.expect(obs.phantom_created);
    const rid1 = try ms.store().intern(a1_phantom_1);
    const rid2 = try ms.store().intern(a1_phantom_2);
    try std.testing.expect(client.phantoms.contains(rid1));
    try std.testing.expect(client.phantoms.contains(rid2));
}

test "sync §A.1: server does not gimme the held ancestor; gimme records peer-lacks" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    // L4099-4100 — the client announced igot for the two gimme'd hashes, so it
    // holds them; pre-store so the round-1 gimme cards queue them for upload.
    const rid1 = try ms.store().put(a1_gimme_1, "manifest payload\n");
    const rid2 = try ms.store().put(a1_gimme_2, "alpha content from client\n");
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), a1_round1_reply);
    // (b) — the two server gimme cards (L4108-4109) queue outbound content.
    try std.testing.expect(obs.content_queued);
    try std.testing.expectEqual(@as(usize, 2), client.send_queue.count());
    try std.testing.expect(client.send_queue.contains(rid1));
    try std.testing.expect(client.send_queue.contains(rid2));
    // The exact peer-lacks set is the two gimme'd hashes; the ancestor
    // c5b2e0ec… is absent because the server already holds it (L4114) and so
    // never gimme's it back — igotDisposition ignores an already-held hash.
    var rt = RoundTables.init(std.testing.allocator);
    defer rt.deinit();
    try rt.recordPeerLacks(a1_gimme_1);
    try rt.recordPeerLacks(a1_gimme_2);
    try std.testing.expect(rt.peerLacks(a1_gimme_1));
    try std.testing.expect(rt.peerLacks(a1_gimme_2));
    try std.testing.expect(!rt.peerLacks(a1_held_ancestor));
    try std.testing.expectEqual(IgotDisposition.ignore, igotDisposition(.client, .sync, true));
}

test "sync §A.1: both §3.7 conditions hold after round 1, exchange continues" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    // Client holds the two gimme'd hashes (L4099-4100), so (b) fires.
    _ = try ms.store().put(a1_gimme_1, "manifest payload\n");
    _ = try ms.store().put(a1_gimme_2, "alpha content from client\n");
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), a1_round1_reply);
    try std.testing.expect(obs.phantom_table_nonempty and obs.phantom_created); // (a)
    try std.testing.expect(obs.content_queued); // (b)
    try std.testing.expect(continueExchange(obs));
    try std.testing.expectEqual(ClientState.exchange, client.state);
}

test "sync §A.1: round-2 delivers two full-content files and empties the phantom set" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    _ = try client.consumeReply(std.testing.allocator, ms.store(), a1_round1_reply);
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), a1_round2_reply);
    // Both file cards are full-content (two-argument) form (L3123-3124); the
    // stated payload sizes are 270 (L3142) and 31 (L3150).
    const rid1 = try ms.store().intern(a1_phantom_1);
    const rid2 = try ms.store().intern(a1_phantom_2);
    try std.testing.expectEqual(@as(usize, 270), ms.store().content(rid1).?.len);
    try std.testing.expectEqual(@as(usize, 31), ms.store().content(rid2).?.len);
    try std.testing.expectEqual(@as(usize, 0), client.phantoms.count());
    try std.testing.expect(obs.versioned_content_delivered and !obs.phantom_created);
}

test "sync §A.1: no round 3 — the client terminates after round 2" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    // Round 1 queues the two held gimmes; round 2's reply carries no gimme.
    _ = try ms.store().put(a1_gimme_1, "manifest payload\n");
    _ = try ms.store().put(a1_gimme_2, "alpha content from client\n");
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    _ = try client.consumeReply(std.testing.allocator, ms.store(), a1_round1_reply);
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), a1_round2_reply);
    // L4179-4182 — round 2 delivered content but its reply had no gimme, so
    // nothing was appended this round: the per-round file-send count is zero.
    try std.testing.expect(!obs.content_queued);
    // The per-session buffer still holds the two rids queued in round 1 (§3.1
    // L605-608), yet §3.7(b) reads the per-round count, not the buffer.
    try std.testing.expectEqual(@as(usize, 2), client.send_queue.count());
    // §3.7 holds nowhere prospectively; the client stops.
    try std.testing.expect(!continueExchange(obs));
    try std.testing.expectEqual(ClientState.terminated, client.state);
}

test "sync §A.2: clone reply discards the error, records seqno and push, advances" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .clone, .state = .clone_round0 };
    defer client.deinit(std.testing.allocator);
    try std.testing.expectEqual(ClientErrorAction.discard, clientOnErrorCard(.clone_round0));
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), a2_reply);
    // L3217 — clone_seqno 0 = exhausted RID range.
    try std.testing.expectEqual(@as(i64, 0), obs.clone_seqno_next);
    // L3208, §8.3/§11.1 — the project code arrives in the push card.
    const push = parsePushPull(tokenizeCard("push 34302acad64fe2397bd907704dec457899e2f9a3 c85e3efddf993e86e8325d08672ea94bdba6c2c4")).?;
    try std.testing.expectEqualStrings("c85e3efddf993e86e8325d08672ea94bdba6c2c4", push.projectcode);
    // r14 — a processed first clone reply advances to EXCHANGE, not aborted
    // (L3221-3224: the round-0 error is discarded, not fatal).
    try std.testing.expectEqual(ClientState.exchange, client.state);
}

test "sync §A.2: cards after the discarded error are still dispatched (non-reset)" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .clone, .state = .clone_round0 };
    defer client.deinit(std.testing.allocator);
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), a2_reply);
    // The closing timestamp line (L3209) follows error/clone_seqno/push; a
    // stop at the error card would leave it unseen (L3212-3216, no reset).
    try std.testing.expect(client.last_timestamp != null);
    try std.testing.expectEqualStrings("2026-07-02T18:26:49", client.last_timestamp.?.datetime);
    try std.testing.expectEqual(@as(i64, 0), obs.clone_seqno_next);
}

test "sync §A.4: both server replies surface the closing timestamp comment" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    _ = try client.consumeReply(std.testing.allocator, ms.store(), a1_round1_reply);
    // L3439 — round-1 reply closes with the timestamp subform.
    try std.testing.expectEqualStrings("2026-07-02T18:31:13", client.last_timestamp.?.datetime);
    try std.testing.expectEqual(@as(u64, 0), client.last_timestamp.?.errors);
    _ = try client.consumeReply(std.testing.allocator, ms.store(), a1_round2_reply);
    // L3443 — round-2 reply closes with the same subform, N = 0.
    try std.testing.expectEqualStrings("2026-07-02T18:31:13", client.last_timestamp.?.datetime);
    try std.testing.expectEqual(@as(u64, 0), client.last_timestamp.?.errors);
}

fn consumeReplyOom(gpa: std.mem.Allocator) !void {
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    // Round 1 fills the phantom set (two igot) and peer-lacks (two gimme); no
    // card in it is fatal or unimplemented, so OOM is the only non-success.
    _ = client.consumeReply(gpa, ms.store(), a1_round1_reply) catch |e| switch (e) {
        error.OutOfMemory => return error.OutOfMemory,
        error.FatalProtocolError => unreachable,
    };
}

test "sync §3.2: consumeReply covers OOM paths (phantom set + peer-lacks fill)" {
    try std.testing.checkAllAllocationFailures(std.testing.allocator, consumeReplyOom, .{});
}

// §7.5 — an arbitrary 64-hex hash for the private-marker tests. No expected
// byte derives from it; the store keys under the literal name and the asserted
// value (private_content_delivered) comes from the r1/r4 rules alone.
const p75_hash = "0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0";

test "sync §7.5 r1: a private card marks the next file card as private" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    // §7.5 L1376-1377 — with the grant present the bound content is stored, so
    // `private_content_delivered` is the observable proof the marker was bound.
    var client = Client{ .operation = .sync, .state = .exchange, .private_granted = true };
    defer client.deinit(std.testing.allocator);
    // L2359-2360 — "A private card … marks the next … file or cfile card as
    // carrying private content."
    const body = "private\nfile " ++ p75_hash ++ " 1\nx";
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), body);
    try std.testing.expect(obs.private_content_delivered);
    try std.testing.expect(!client.private_pending); // r4 — consumed by the file card
}

test "sync §7.5 r2: intervening comment/non-content cards do not consume the marker" {
    // L2361-2362 — "intervening comment lines and other non-content cards do
    // not consume it."
    const intervening = [_][]const u8{ "# a comment\n", "pragma foo bar\n" };
    for (intervening) |line| {
        var ms = repostate.MemStore.init(std.testing.allocator);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange };
        defer client.deinit(std.testing.allocator);
        const body = try std.mem.concat(std.testing.allocator, u8, &.{ "private\n", line });
        defer std.testing.allocator.free(body);
        _ = try client.consumeReply(std.testing.allocator, ms.store(), body);
        try std.testing.expect(client.private_pending); // still set — nothing consumed it
    }
}

test "sync §7.5 r3: a repeated private card leaves the marker set" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    // L2362-2363 — "a repeated private card leaves the flag set."
    _ = try client.consumeReply(std.testing.allocator, ms.store(), "private\nprivate\n");
    try std.testing.expect(client.private_pending);
}

test "sync §7.5 r4: the marker is consumed when the next cfile card handling begins" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    // §7.5 L1376-1377 — grant present so the bound cfile is stored (delivered).
    var client = Client{ .operation = .sync, .state = .exchange, .private_granted = true };
    defer client.deinit(std.testing.allocator);
    // Build the §4.1 container in-test (uncompressed size 1 for the byte "y").
    const container = try zlibContainerEncode(std.testing.allocator, "y", .default);
    defer std.testing.allocator.free(container);
    const header = try std.fmt.allocPrint(std.testing.allocator, "private\ncfile {s} 1 {d}\n", .{ p75_hash, container.len });
    defer std.testing.allocator.free(header);
    const body = try std.mem.concat(std.testing.allocator, u8, &.{ header, container });
    defer std.testing.allocator.free(body);
    // L2363-2364 — "the flag is consumed only when handling of the next file or
    // cfile card begins."
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), body);
    try std.testing.expect(obs.private_content_delivered);
    try std.testing.expect(!client.private_pending);
}

test "sync §7.5 r5: the pending marker persists across reply bodies in a session" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    // §7.5 L1376-1377 — grant present so body 2's file card delivers (stores).
    var client = Client{ .operation = .sync, .state = .exchange, .private_granted = true };
    defer client.deinit(std.testing.allocator);
    // L2364-2366 — "A client receiver carries the pending marker across
    // successive reply bodies within a single sync session." Body 1 keeps the
    // round alive so a 2nd reply is legal: the igot creates a phantom (§3.7(a)
    // → round_continues → state stays .exchange), and being a non-content card
    // it does not consume the marker (r2).
    const body1 = "private\nigot " ++ a1_phantom_1 ++ "\n";
    _ = try client.consumeReply(std.testing.allocator, ms.store(), body1);
    try std.testing.expectEqual(ClientState.exchange, client.state);
    try std.testing.expect(client.private_pending); // carried into the next body
    // Body 2 delivers the announced artifact; that file card consumes the marker.
    const body2 = "file " ++ a1_phantom_1 ++ " 1\nx";
    const obs2 = try client.consumeReply(std.testing.allocator, ms.store(), body2);
    try std.testing.expect(obs2.private_content_delivered);
    try std.testing.expect(!client.private_pending);
}

test "sync §7.5 r6: a pending marker does not carry across the session boundary" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    // L2366-2367 — "only a subsequent file or cfile card, or the end of that
    // session, clears it." A fresh Client (new session) starts clear; a marker
    // left pending in a prior session never binds a content card in a new one.
    var c1 = Client{ .operation = .sync, .state = .exchange };
    _ = try c1.consumeReply(std.testing.allocator, ms.store(), "private\n");
    try std.testing.expect(c1.private_pending);
    try std.testing.expectEqual(ClientState.terminated, c1.state);
    c1.deinit(std.testing.allocator);

    var c2 = Client{ .operation = .sync, .state = .exchange };
    defer c2.deinit(std.testing.allocator);
    try std.testing.expect(!c2.private_pending); // new session starts clear
    const body = "file " ++ p75_hash ++ " 1\nx";
    const obs = try c2.consumeReply(std.testing.allocator, ms.store(), body);
    try std.testing.expect(!obs.private_content_delivered); // no leak from session 1
}

test "sync §7.5 r7: a reply body ending with the marker still pending is not fatal (rejection)" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    // L2369-2371 — "Reaching the end of a client reply body with the marker
    // still pending is not by itself a fatal protocol error." The body returns
    // an observation (no error) and ends in the ordinary .terminated round end,
    // not .aborted.
    _ = try client.consumeReply(std.testing.allocator, ms.store(), "private\n# c\n");
    try std.testing.expect(client.private_pending);
    try std.testing.expectEqual(ClientState.terminated, client.state);
}

test "sync §7.5 r8: a private card carrying an argument is a fatal card-arity abort (rejection)" {
    // r8 — "a private card carrying any argument is a §5.2 card-arity violation
    // and a fatal strict-accept abort (§3.9)." ABNF §B L2863 fixes arity 1; §5.2
    // L1770-1776 (defined operator + bad arg count → its own arity rule) and
    // L1809-1813 (the client aborts the round, no outbound error card).
    const bad = [_][]const u8{ "private x\n", "private a b\n" };
    for (bad) |body| {
        var ms = repostate.MemStore.init(std.testing.allocator);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange };
        defer client.deinit(std.testing.allocator);
        try std.testing.expectError(error.FatalProtocolError, client.consumeReply(std.testing.allocator, ms.store(), body));
        try std.testing.expectEqual(ClientState.aborted, client.state);
        try std.testing.expect(!client.private_pending); // never set on the arity-fail path
    }
}

const a1_phantom_1 = "4361252d0f450879dcd2e0b6a42b95ffe8a41d8e948130253ae8eefa7f4b79ea";

const a1_phantom_2 = "f5116517775075ea483901021282ba97eebe5a2b993a47eb78198adc10efe128";

test "sync §A.4: a client nonce comment is skipped with no store or obs effect" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    // L3425, L3431-3433 — a `#`-prefixed 40-hex nonce is not the timestamp
    // subform, so it is skipped silently.
    const body = "# F2A895EF75B47B7157664A7FEAC978EFFFDB01C4\n";
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), body);
    try std.testing.expect(!obs.versioned_content_delivered and !obs.phantom_created);
    try std.testing.expect(client.last_timestamp == null);
    try std.testing.expectEqual(@as(usize, 0), ms.entries.items.len); // store untouched
}

test "sync §3.3 r24: an igot for an already-held artifact creates no phantom" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    _ = try ms.store().put(a1_phantom_1, "held content");
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    const body = "igot " ++ a1_phantom_1 ++ "\n";
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), body);
    try std.testing.expectEqual(@as(usize, 0), client.phantoms.count());
    try std.testing.expect(!obs.phantom_created);
}

test "sync §3.3: a repeated igot of one hash counts a single phantom" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    const body = "igot " ++ a1_phantom_1 ++ "\nigot " ++ a1_phantom_1 ++ "\n";
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), body);
    try std.testing.expect(obs.phantom_created);
    try std.testing.expectEqual(@as(usize, 1), client.phantoms.count());
}

test "sync §3.3 r27: a malformed gimme hash is fatal at parse, never asserted downstream" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    const body = "gimme deadbeef00\n"; // 10 hex — not a 40/64 hash token
    try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(std.testing.allocator, ms.store(), body));
    try std.testing.expectEqual(ClientState.aborted, client.state);
}

test "sync §3.7(a): content for a never-phantom'd hash does not underflow the set" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    // File for an unseen hash: the remove is a no-op, no trap under ReleaseSafe.
    const body = "file " ++ a1_phantom_1 ++ " 3\nabc";
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), body);
    try std.testing.expect(obs.versioned_content_delivered);
    try std.testing.expect(!obs.phantom_table_nonempty);
    try std.testing.expectEqual(@as(usize, 0), client.phantoms.count());
}

test "sync §3.8 r19: a negative wire clone_seqno is fatal at parse, never stored" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .clone, .state = .clone_round0 };
    defer client.deinit(std.testing.allocator);
    const body = "clone_seqno -1\n";
    try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(std.testing.allocator, ms.store(), body));
    try std.testing.expectEqual(ClientState.aborted, client.state);
}

test "sync §3.9: a clone card in a reply is a fatal protocol violation, not NotImplemented" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    const body = "clone 3 1\n";
    try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(std.testing.allocator, ms.store(), body));
    try std.testing.expectEqual(ClientState.aborted, client.state);
    try std.testing.expectEqual(@as(usize, 0), ms.entries.items.len); // store unchanged
}

test "sync §5: an out-of-scope legal reply card is a strict boundary, not a crash" {
    // The delta-form file/cfile card is no longer here: §7.4 reception is now
    // implemented (a dangling delta is retained, not fenced off). uvfile remains
    // the staged, out-of-scope boundary.
    const bodies = [_][]const u8{
        "uvfile a.txt 1700000000 " ++ test_hash_a ++ " 12 4\n", // §7.3 uvfile, omitted content, still staged
    };
    for (bodies) |body| {
        var ms = repostate.MemStore.init(std.testing.allocator);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange };
        defer client.deinit(std.testing.allocator);
        _ = try client.consumeReply(std.testing.allocator, ms.store(), body);
        try std.testing.expectEqual(@as(u64, 1), client.uv_skipped); // #58: counted, not errored
        try std.testing.expectEqual(@as(usize, 0), ms.entries.items.len); // store unchanged
    }
}

test "sync §3.7(a): an igot then a file for the same hash dephantomizes within the round" {
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    // A reply that announces then delivers one hash: the igot creates the
    // phantom, the file in the same body removes it. phantom_created stays set
    // (it happened) while the table ends empty — the two are independent, so
    // phantom_table_nonempty is the authority for §3.7(a), never phantom_created.
    const body = "igot " ++ a1_phantom_1 ++ "\nfile " ++ a1_phantom_1 ++ " 3\nabc";
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), body);
    try std.testing.expect(obs.phantom_created);
    try std.testing.expect(obs.versioned_content_delivered);
    try std.testing.expectEqual(@as(usize, 0), client.phantoms.count());
    try std.testing.expect(!obs.phantom_table_nonempty);
}

// ---------------------------------------------------------------------------
// §3.6 / §6.1 / §9.3 — reply-dispatch wiring: cookie record, login/reqconfig
// strict-accept aborts, config framing consume.
// ---------------------------------------------------------------------------

test "sync §3.6: consumeReply stores a cookie into the held CookieStore" {
    // §3.6 L567-568 — "The client that receives such a card MUST store the
    // value"; TEXT is opaque with no escapes, so it stores verbatim.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), "cookie s3cr3t\n");
    try std.testing.expectEqualStrings("s3cr3t", client.cookie.value().?);
    try std.testing.expect(obs.cookie_stored);
}

test "sync §3.6: a second cookie overwrites the first" {
    // §3.6 L567-568 — "replacing any previously held cookie for that server".
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    _ = try client.consumeReply(std.testing.allocator, ms.store(), "cookie v1\ncookie v2\n");
    try std.testing.expectEqualStrings("v2", client.cookie.value().?);
}

test "sync §3.6: multiple cookie cards in one reply leave the last, none rejected" {
    // §3.6 L567-570 — "must be prepared to receive more than one cookie card in
    // a single request": each is accepted, the last wins, none aborts.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    _ = try client.consumeReply(std.testing.allocator, ms.store(), "cookie one\ncookie two\ncookie three\n");
    try std.testing.expectEqualStrings("three", client.cookie.value().?);
    try std.testing.expect(client.state != .aborted);
}

test "sync §3.6/§4.3: cookie TEXT is defossilized before storing" {
    // §4.3 — cookie TEXT is fossilize-encoded like error/message TEXT; the wire
    // token `a\sb` decodes to `a b` (the inverse of writeCookieCard("a b")).
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    var buf: [16]u8 = undefined;
    try std.testing.expectEqualStrings("cookie a\\sb", writeCookieCard("a b", &buf));
    _ = try client.consumeReply(std.testing.allocator, ms.store(), "cookie a\\sb\n");
    try std.testing.expectEqualStrings("a b", client.cookie.value().?);
}

test "sync §3.9: a cookie card never aborts, reports a non-fatal observation" {
    // §3.9 L797-801 — "A cookie card from an unexpected server is tolerated": no
    // abort, a non-fatal round observation, the state machine stays live.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), "cookie x\n");
    try std.testing.expect(obs.cookie_stored);
    try std.testing.expect(client.state != .aborted);
}

test "sync §5.2/§B: a cookie card with no TEXT token is a fatal arity abort (rejection)" {
    // §B L2835 `cookie-card = "cookie" SP fossilized LF` — exactly two tokens.
    // A bare `cookie\n` (arity 1) fails the card's argument check; §5.2 L1024-1026
    // makes the client record a local warning and abort the round. §3.9's cookie
    // tolerance (L1312) is scoped to an unexpected *server*, not a missing TEXT.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(std.testing.allocator, ms.store(), "cookie\n"));
    try std.testing.expectEqual(ClientState.aborted, client.state);
    try std.testing.expect(client.cookie.value() == null);
}

test "sync §3.6/§5.1: a cookie card carries no byte payload; dispatch resumes at the next line" {
    // §5.1 — cookie has no byte-counted payload, so the cursor resumes at the
    // next line offset. The file card after it parses and delivers content,
    // proving the resume point was payload_start, not past any bytes.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    const body = "cookie tok\nfile " ++ a1_phantom_1 ++ " 3\nabc";
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), body);
    try std.testing.expectEqualStrings("tok", client.cookie.value().?);
    try std.testing.expect(obs.versioned_content_delivered);
}

test "sync §6.1/§3.9: a login card in a server reply is a fatal protocol violation (rejection)" {
    // §3.2 L399-403 — login is not among the cards a reply may contain (Table 2
    // §3.2.1 L431 places it in requests); the abort is zit-local under §3.9 L797-803.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    const body = "login alice deadbeef cafef00d\n";
    try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(std.testing.allocator, ms.store(), body));
    try std.testing.expectEqual(ClientState.aborted, client.state);
    try std.testing.expectEqual(@as(usize, 0), ms.entries.items.len);
}

test "sync §3.8 r15/r18: login-in-reply aborts from both reply-bearing states" {
    // §3.8 Table 3 — the abort routes through Client.abort: `fatal_error` from
    // exchange, `clone_fatal` from clone_round0; both land in `aborted`.
    const cases = [_]struct { op: Operation, state: ClientState }{
        .{ .op = .sync, .state = .exchange },
        .{ .op = .clone, .state = .clone_round0 },
    };
    for (cases) |c| {
        var ms = repostate.MemStore.init(std.testing.allocator);
        defer ms.deinit();
        var client = Client{ .operation = c.op, .state = c.state };
        defer client.deinit(std.testing.allocator);
        const body = "login alice deadbeef cafef00d\n";
        try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(std.testing.allocator, ms.store(), body));
        try std.testing.expectEqual(ClientState.aborted, client.state);
    }
}

test "sync §9.3/§3.9: a reqconfig card in a server reply is a fatal abort (rejection)" {
    // §9.3 L1883-1884 — "A client MAY issue multiple reqconfig groups in one request": reqconfig is client→server
    // only; the reverse flow is a config push, not reqconfig. A reqconfig in a
    // server reply is a strict-accept abort (§3.9).
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(std.testing.allocator, ms.store(), "reqconfig /ui\n"));
    try std.testing.expectEqual(ClientState.aborted, client.state);
    try std.testing.expectEqual(@as(usize, 0), ms.entries.items.len);
}

test "sync §9.3/§5.1: consumeReply frames a config payload by its SIZE byte count" {
    // §9.3 L1888-1889 — `config GROUPNAME SIZE` then SIZE content bytes. The payload
    // embeds an LF (`A\nBCD` = 5 bytes); byte-count framing (never scanning)
    // means the embedded LF is not a card boundary and the trailing cookie
    // still dispatches.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    _ = try client.consumeReply(std.testing.allocator, ms.store(), "config /config 5\nA\nBCD\ncookie z\n");
    try std.testing.expectEqualStrings("z", client.cookie.value().?);
}

test "sync §9.3: a config card's trailing newline is consumed" {
    // §9.3 L1888-1891 — "A single newline byte follows the content payload ...
    // receivers MUST consume this trailing newline". The cookie dispatches only
    // if the LF after `abc` was consumed.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    _ = try client.consumeReply(std.testing.allocator, ms.store(), "config /config 3\nabc\ncookie z\n");
    try std.testing.expectEqualStrings("z", client.cookie.value().?);
}

test "sync §9.3: dispatch resumes at start + SIZE + 1 after a config card" {
    // §9.3 — after SIZE payload bytes and the mandatory trailing LF, the next
    // card begins at start + SIZE + 1. A following file card delivers content
    // only if the resume offset is exact (off-by-one would misparse it).
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    const body = "config /config 3\nabc\nfile " ++ a1_phantom_1 ++ " 3\nXYZ";
    const obs = try client.consumeReply(std.testing.allocator, ms.store(), body);
    try std.testing.expect(obs.versioned_content_delivered);
}

test "sync §9.3/§7: a config frame past the reply body is a fatal abort (rejection)" {
    // §9.3/§3.9 — SIZE 10 but only 4 bytes follow: ConfigFrameError.Truncated
    // routes through Client.abort to a fatal protocol error.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(std.testing.allocator, ms.store(), "config /config 10\nabc\n"));
    try std.testing.expectEqual(ClientState.aborted, client.state);
    try std.testing.expectEqual(@as(usize, 0), ms.entries.items.len);
}

test "sync §9.3: a config card missing the trailing newline is a fatal abort (rejection)" {
    // §9.3 L1888-1891 — "one mandatory LF" binds senders only; a receiver need not check that byte, so this rejection is zit-local:
    // ConfigFrameError.MissingNewline routes through Client.abort.
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(std.testing.allocator);
    try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(std.testing.allocator, ms.store(), "config /config 3\nabcX\n"));
    try std.testing.expectEqual(ClientState.aborted, client.state);
}

test "sync §9.3: a malformed config header is a fatal abort (rejection)" {
    // §9.3 L1888-1889 `config GROUPNAME SIZE` — each malformed header is a fatal
    // abort: (a) too few tokens, (b) non-numeric SIZE, (c) SIZE > u32,
    // (d) negative SIZE — the RFC defines no receiver fatal for it (§9.3
    // L1923-1927 is server-side; §5.2 L1024-1026 warns), so this abort is zit-local.
    const headers = [_][]const u8{
        "config /config\n", // (a) no SIZE
        "config /config xy\n", // (b) non-numeric SIZE
        "config /config 4294967296\n", // (c) 2^32, past u32
        "config /config -1\n", // (d) negative SIZE
    };
    for (headers) |body| {
        var ms = repostate.MemStore.init(std.testing.allocator);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange };
        defer client.deinit(std.testing.allocator);
        try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(std.testing.allocator, ms.store(), body));
        try std.testing.expectEqual(ClientState.aborted, client.state);
        try std.testing.expectEqual(@as(usize, 0), ms.entries.items.len);
    }
}

test "sync §3.6/§4.3 fuzz: a well-formed cookie card round-trips through consumeReply" {
    // Invariant (§3.6 L567-568, §4.3): for any non-empty opaque value x,
    // consumeReply of `cookie <fossilize(x)>` stores exactly x and never
    // aborts. Fixed seed for exact replay.
    var prng = std.Random.DefaultPrng.init(0xC0_0C_1E_FA);
    const rand = prng.random();
    var raw: [48]u8 = undefined;
    var token: [96]u8 = undefined;
    var card: [128]u8 = undefined;
    var round: usize = 0;
    while (round < 512) : (round += 1) {
        // §B L2835/§5.2 — interleave the arity-reject shape: a `cookie` line
        // whose only trailing content is a whitespace run tokenizes to arity 1,
        // which the client MUST reject as fatal and store nothing, whatever the
        // run length.
        if (rand.boolean()) {
            const bp = "cookie";
            @memcpy(card[0..bp.len], bp);
            const ws = rand.intRangeAtMost(usize, 0, 8);
            for (card[bp.len..][0..ws]) |*b| b.* = if (rand.boolean()) ' ' else '\t';
            card[bp.len + ws] = '\n';
            const bad = card[0 .. bp.len + ws + 1];
            var ms2 = repostate.MemStore.init(std.testing.allocator);
            defer ms2.deinit();
            var bad_client = Client{ .operation = .sync, .state = .exchange };
            defer bad_client.deinit(std.testing.allocator);
            try std.testing.expectError(Client.ConsumeError.FatalProtocolError, bad_client.consumeReply(std.testing.allocator, ms2.store(), bad));
            try std.testing.expectEqual(ClientState.aborted, bad_client.state);
            try std.testing.expect(bad_client.cookie.value() == null);
            continue;
        }
        const n = rand.intRangeAtMost(usize, 1, raw.len); // non-empty: empty has no TEXT token
        for (raw[0..n]) |*b| b.* = rand.int(u8); // any byte: LF/NUL/SP all legal in x
        const enc = artifact.encodeEscapes(raw[0..n], &token); // whitespace-free wire token
        const prefix = "cookie ";
        @memcpy(card[0..prefix.len], prefix);
        @memcpy(card[prefix.len..][0..enc.len], enc);
        card[prefix.len + enc.len] = '\n';
        const body = card[0 .. prefix.len + enc.len + 1];
        var ms = repostate.MemStore.init(std.testing.allocator);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange };
        defer client.deinit(std.testing.allocator);
        _ = client.consumeReply(std.testing.allocator, ms.store(), body) catch |e| {
            try std.testing.expect(e == error.OutOfMemory);
            continue;
        };
        try std.testing.expect(client.state != .aborted);
        try std.testing.expectEqualStrings(raw[0..n], client.cookie.value().?);
    }
}

test "sync §9.3 fuzz: config byte-count framing survives an adversarial payload" {
    // Invariant (§9.3 L1888-1891): a config card frames its payload by
    // SIZE alone, so a trailing cookie always dispatches regardless of payload
    // bytes — embedded LF/NUL never fool the framer into an early boundary.
    var prng = std.Random.DefaultPrng.init(0xC0_F1_6F_1E);
    const rand = prng.random();
    var payload: [64]u8 = undefined;
    var card: [160]u8 = undefined;
    var round: usize = 0;
    while (round < 512) : (round += 1) {
        const size = rand.uintLessThan(usize, payload.len + 1);
        for (payload[0..size]) |*b| b.* = rand.int(u8); // may embed LF/NUL
        const header = std.fmt.bufPrint(&card, "config /config {d}\n", .{size}) catch unreachable;
        var w = header.len;
        @memcpy(card[w..][0..size], payload[0..size]);
        w += size;
        card[w] = '\n'; // mandatory trailing LF
        w += 1;
        const suffix = "cookie mark\n";
        @memcpy(card[w..][0..suffix.len], suffix);
        w += suffix.len;
        const body = card[0..w];
        var ms = repostate.MemStore.init(std.testing.allocator);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange };
        defer client.deinit(std.testing.allocator);
        _ = client.consumeReply(std.testing.allocator, ms.store(), body) catch |e| {
            try std.testing.expect(e == error.OutOfMemory);
            continue;
        };
        try std.testing.expect(client.state != .aborted);
        try std.testing.expectEqualStrings("mark", client.cookie.value().?);
    }
}

// ---------------------------------------------------------------------------
// §7.2 — cfile card (full-content) client reply-dispatch through consumeReply.
// ---------------------------------------------------------------------------

/// Build `cfile HASH USIZE CSIZE\n` followed by `payload` bytes. USIZE and
/// CSIZE are caller-supplied so a rejection test can frame a deliberate lie
/// (a CSIZE past the body, a USIZE that disagrees with the decoded length).
/// Owned by the caller.
fn buildCfileBody(gpa: std.mem.Allocator, hash: []const u8, usize_field: usize, csize_field: usize, payload: []const u8) ![]u8 {
    const header = try std.fmt.allocPrint(gpa, "cfile {s} {d} {d}\n", .{ hash, usize_field, csize_field });
    defer gpa.free(header);
    const body = try gpa.alloc(u8, header.len + payload.len);
    @memcpy(body[0..header.len], header);
    @memcpy(body[header.len..], payload);
    return body;
}

test "sync §7.2-r6: consumeReply stores a full-content cfile's decompressed bytes under HASH" {
    const gpa = std.testing.allocator;
    const plaintext = "cfile artifact payload\n";
    const enc = try zlibContainerEncode(gpa, plaintext, .default);
    defer gpa.free(enc);
    // §7.2-r1/r5 — USIZE is the reconstructed length (syntax only); CSIZE is
    // the wire byte count that actually frames the payload.
    const body = try buildCfileBody(gpa, test_hash_a, plaintext.len, enc.len, enc);
    defer gpa.free(body);
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(obs.versioned_content_delivered); // §3.7(a) versioned progress
    const rid = ms.store().lookup(test_hash_a).?;
    try std.testing.expectEqualStrings(plaintext, ms.store().content(rid).?); // §7.2-r6 decompressed bytes under HASH
}

test "sync §7.2-r7: an igot then a full-content cfile for the same hash dephantomizes" {
    const gpa = std.testing.allocator;
    const plaintext = "dephantomized by cfile";
    const enc = try zlibContainerEncode(gpa, plaintext, .default);
    defer gpa.free(enc);
    const cfile = try buildCfileBody(gpa, test_hash_a, plaintext.len, enc.len, enc);
    defer gpa.free(cfile);
    // §3.7(a)/§3.3 r36 — the igot creates the phantom, the cfile in the same
    // body removes it exactly as a full-content file card would (r7).
    const body = try std.fmt.allocPrint(gpa, "igot {s}\n{s}", .{ test_hash_a, cfile });
    defer gpa.free(body);
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(obs.phantom_created);
    try std.testing.expect(obs.versioned_content_delivered);
    try std.testing.expectEqual(@as(usize, 0), client.phantoms.count());
    try std.testing.expect(!obs.phantom_table_nonempty);
}

test "sync §7.2-r9: a full-content cfile payload is container-decoded exactly once" {
    const gpa = std.testing.allocator;
    // §7.2-r9 — the x-fossil-uncompressed outer body is already decoded, so the
    // cfile payload is container-decoded once. This plaintext is itself NOT a
    // §4.1 container: a spurious second decode pass would abort, so a clean
    // store of these exact bytes proves the single-decode contract.
    const plaintext = "plain artifact bytes, not a nested container";
    if (zlibContainerDecode(gpa, plaintext, std.math.maxInt(usize))) |leaked| {
        gpa.free(leaked);
        try std.testing.expect(false); // guard: the plaintext must not itself be a valid container
    } else |_| {}
    const enc = try zlibContainerEncode(gpa, plaintext, .default);
    defer gpa.free(enc);
    const body = try buildCfileBody(gpa, test_hash_a, plaintext.len, enc.len, enc);
    defer gpa.free(body);
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(obs.versioned_content_delivered);
    try std.testing.expect(client.state != .aborted);
    const rid = ms.store().lookup(test_hash_a).?;
    try std.testing.expectEqualStrings(plaintext, ms.store().content(rid).?);
}

test "sync §5.1: consumeReply consumes the cfile trailing-LF padding when the payload does not end in LF" {
    const gpa = std.testing.allocator;
    // §7.5 L1374-1377 — a private cfile without the grant is discarded after
    // CSIZE framing, before the container is decoded, so an arbitrary 4-byte
    // payload that does NOT end in LF exercises §5.1 padding without needing a
    // valid zlib container. The `private` marks the cfile private (discarded),
    // the payload's last byte 'D' ≠ LF so §5.1 puts one padding LF on the wire,
    // and a trailing `private` whose effect (private_pending) proves the loop
    // parsed the card immediately AFTER the padding.
    const body = "private\ncfile " ++ test_hash_a ++ " 4 4\nABCD\nprivate\n";
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    _ = try client.consumeReply(gpa, ms.store(), body); // §5.1 — old code aborts on the padding empty line
    try std.testing.expect(client.state != .aborted);
    try std.testing.expect(client.private_pending); // the post-padding `private` card was parsed → padding consumed
}

test "sync §5.1: a cfile whose payload ends in LF carries no padding; the following card is parsed" {
    const gpa = std.testing.allocator;
    // payload "AB\n" ends in LF → §5.1 emits NO padding, so the receiver must
    // NOT skip a byte; the immediately following `private` card must parse.
    const body = "private\ncfile " ++ test_hash_a ++ " 3 3\nAB\nprivate\n";
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    _ = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(client.state != .aborted);
    try std.testing.expect(client.private_pending); // no over-skip: the trailing `private` was parsed
}

test "sync §5.1: a cfile payload ending in LF does not consume a following empty line (negative space)" {
    const gpa = std.testing.allocator;
    // payload "AB\n" ends in LF (§5.1 → no padding). A bare empty line then
    // follows: it is a malformed card the receiver MUST abort on, never silently
    // skip. This pins the conditional to the payload's LAST byte — not merely to
    // "is the next wire byte an LF".
    const body = "private\ncfile " ++ test_hash_a ++ " 3 3\nAB\n\nprivate\n";
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    try std.testing.expectError(error.FatalProtocolError, client.consumeReply(gpa, ms.store(), body));
    try std.testing.expectEqual(ClientState.aborted, client.state);
}

test "sync §7.2-r14: a full-content cfile consumed in clone_round0 counts as clone content progress" {
    const gpa = std.testing.allocator;
    const plaintext = "clone content via cfile";
    const enc = try zlibContainerEncode(gpa, plaintext, .default);
    defer gpa.free(enc);
    const body = try buildCfileBody(gpa, test_hash_a, plaintext.len, enc.len, enc);
    defer gpa.free(body);
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .clone, .state = .clone_round0 };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(obs.versioned_content_delivered); // §8.2 r14 — identical to a full file card
    const rid = ms.store().lookup(test_hash_a).?;
    try std.testing.expectEqualStrings(plaintext, ms.store().content(rid).?);
    try std.testing.expectEqual(@as(usize, 0), client.phantoms.count());
    try std.testing.expectEqual(ClientState.exchange, client.state); // §3.8 r14 unconditional advance
}

test "sync §7.2-r15: a Version-3 clone reply carrying a full cfile stores under HASH and clears the phantom" {
    const gpa = std.testing.allocator;
    // §8.1 Version 3 / §B cfile-transfer — igot announces, cfile delivers,
    // clone_seqno closes the round. End-to-end: content stored, phantom gone.
    const plaintext = "version 3 clone artifact";
    const enc = try zlibContainerEncode(gpa, plaintext, .default);
    defer gpa.free(enc);
    const cfile = try buildCfileBody(gpa, test_hash_a, plaintext.len, enc.len, enc);
    defer gpa.free(cfile);
    const body = try std.fmt.allocPrint(gpa, "igot {s}\n{s}clone_seqno 0\n", .{ test_hash_a, cfile });
    defer gpa.free(body);
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .clone, .state = .clone_round0 };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(obs.versioned_content_delivered);
    const rid = ms.store().lookup(test_hash_a).?;
    try std.testing.expectEqualStrings(plaintext, ms.store().content(rid).?);
    try std.testing.expectEqual(@as(usize, 0), client.phantoms.count());
    try std.testing.expect(client.state != .aborted);
}

test "sync §8.2 L1529-1539: a mid-clone cfile batch sets the received-artifact observation that continues the clone" {
    const gpa = std.testing.allocator;
    const plaintext = "batch-boundary artifact";
    const enc = try zlibContainerEncode(gpa, plaintext, .default);
    defer gpa.free(enc);
    const cfile = try buildCfileBody(gpa, test_hash_a, plaintext.len, enc.len, enc);
    defer gpa.free(cfile);
    // A non-final batch: one cfile then a positive `clone_seqno NEXT` — the
    // server still has artifacts to page (§8.2 L1517-1520).
    const body = try std.fmt.allocPrint(gpa, "{s}clone_seqno 7\n", .{cfile});
    defer gpa.free(body);
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    // A follow-up (non-first) clone round is consumed in `exchange`, where §3.7
    // gates continuation on the observation this reply produces (the first
    // round advances unconditionally and never reaches this predicate).
    var client = Client{ .operation = .clone, .state = .exchange };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    // §8.2 L1529-1532 — the raw received-artifact count grew and a delivery was
    // stored; the cursor NEXT was recorded verbatim.
    try std.testing.expect(obs.artifact_count_grew);
    try std.testing.expect(obs.artifact_received_this_round);
    try std.testing.expectEqual(@as(i64, 7), obs.clone_seqno_next);
    // §3.7(e)/§8.2 L1537-1539 — a stored delivery this round with a positive cursor
    // continues the clone; the driver threads NEXT into the next `clone 3 7`.
    try std.testing.expect(continueExchange(obs));
    try std.testing.expectEqual(ClientState.exchange, client.state);
}

test "sync §8.2 L1520/L1541: a clone reply whose cursor is zero and stored nothing terminates the clone" {
    const gpa = std.testing.allocator;
    // A cleanup-shaped clone reply: no cfile, `clone_seqno 0` (exhausted). No
    // stored delivery and a zero cursor — §3.7(e) finds nothing to continue.
    const body = "clone_seqno 0\n";
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .clone, .state = .exchange };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(!obs.artifact_received_this_round);
    try std.testing.expect(!obs.artifact_count_grew);
    try std.testing.expectEqual(@as(i64, 0), obs.clone_seqno_next);
    try std.testing.expect(!continueExchange(obs)); // §8.2 L1541-1542 — NEXT zero bars further clone
    try std.testing.expectEqual(ClientState.terminated, client.state);
}

test "sync §7.2-r8: a full-content cfile is stored without a pre-storage hash recompute (transfer layer)" {
    const gpa = std.testing.allocator;
    // §7.7 — pre-storage integrity does not apply to cfile in the transfer
    // layer: the plaintext deliberately does NOT hash to HASH, yet it stores.
    const plaintext = "content whose sha does not match the card name";
    const enc = try zlibContainerEncode(gpa, plaintext, .default);
    defer gpa.free(enc);
    const body = try buildCfileBody(gpa, test_hash_a, plaintext.len, enc.len, enc);
    defer gpa.free(body);
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(obs.versioned_content_delivered);
    try std.testing.expect(client.state != .aborted);
    const rid = ms.store().lookup(test_hash_a).?;
    try std.testing.expectEqualStrings(plaintext, ms.store().content(rid).?);
}

test "sync §7.2-r10: a malformed cfile header is a fatal client protocol error (rejection)" {
    const gpa = std.testing.allocator;
    const cases = [_][]const u8{
        "cfile " ++ test_hash_a ++ " 10\n", // arity 3 — too few tokens
        "cfile " ++ test_hash_a ++ " x 5\nHELLO", // non-numeric USIZE
        "cfile " ++ test_hash_a ++ " 5 x\nHELLO", // non-numeric CSIZE
    };
    for (cases) |body| {
        var ms = repostate.MemStore.init(gpa);
        defer ms.deinit();
        var client = Client{ .operation = .sync, .state = .exchange };
        defer client.deinit(gpa);
        try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(gpa, ms.store(), body));
        try std.testing.expectEqual(ClientState.aborted, client.state); // §3.9 strict-accept, §5.2 client fatal
    }
}

test "sync §7.2-r11: a cfile whose CSIZE span runs past the reply body is a fatal content error (rejection)" {
    const gpa = std.testing.allocator;
    // CSIZE 100 but only 5 payload bytes follow → ContentFrameError.Truncated.
    const body = "cfile " ++ test_hash_a ++ " 5 100\nshort";
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(gpa, ms.store(), body));
    try std.testing.expectEqual(ClientState.aborted, client.state);
}

test "sync §7.2/§7.7/§5.2: a full-form cfile whose container fails to inflate is retained at receive, not a receive-time abort" {
    const gpa = std.testing.allocator;
    // §7.2 L1214-1218 — USIZE "is parsed syntactically but not transfer-layer
    // validated"; the payload "is the repository's stored compressed full
    // artifact... retransmitted verbatim". §7.7 L1465-1471 — integrity is a
    // persist-commit-hook obligation that hashes the RECONSTRUCTED bytes, and a
    // clone-context cfile is EXCLUDED from any pre-storage transfer-layer check.
    // §5.2 L968-970 — a locally-malformed reply card is a local warning that stops
    // only the current reply-card loop, NOT a fatal abort. So a full-form (non-
    // delta) container whose declared length exceeds the read cap (OutputTooLarge)
    // or whose RFC1950 stream is corrupt (InvalidStream) is ACCEPTED and retained
    // verbatim at receive; the corruption surfaces only later at read/derivation
    // (content() cannot inflate → null), never as a receive-time clone abort. These
    // are the decompression-class payloads the pre-optimization receiver aborted on
    // by inflating every full payload to discard — that over-strict inflate is gone.
    const payloads = [_][]const u8{
        "\xff\xff\xff\xff\x00", // §4.1 frame present; declared length » read cap → OutputTooLarge at read
        "\x00\x00\x00\x04\x78\x9c\xff\xff", // §4.1 frame present; corrupt RFC1950 stream → InvalidStream at read
    };
    for (payloads) |p| {
        const body = try buildCfileBody(gpa, test_hash_a, 4, p.len, p);
        defer gpa.free(body);
        var ms = repostate.MemStore.init(gpa);
        defer ms.deinit();
        var client = Client{ .operation = .clone, .state = .clone_round0 };
        defer client.deinit(gpa);
        const obs = try client.consumeReply(gpa, ms.store(), body); // §5.2 — no receive-time abort
        try std.testing.expect(client.state != .aborted);
        try std.testing.expect(obs.versioned_content_delivered); // §3.7(a)/§8.2 r14 — counted like a full file card
        const rid = ms.store().lookup(test_hash_a).?; // HASH interned (phantom upgraded)
        // §7.7 — content-addressed corruption surfaces at read/derivation (the
        // capped inflate fails → null), not as a receive-time clone abort.
        try std.testing.expectEqual(@as(?[]const u8, null), ms.store().content(rid));
    }
}

test "sync §7.4/§5.2: a pending pull .raw delta whose basis arrives as a CORRUPT full-form cfile aborts the round, never traps" {
    // §7.4 basis precondition (W4 regression). Pre-W4 a corrupt full container
    // aborted at receive, so it could never become a delta basis — the "basis
    // content is materialisable" precondition always held. W4 (§7.2/§7.7) now
    // retains a stored full cfile container VERBATIM and defers §7.7 inflate/
    // verify to read, so a corrupt full container is interned+retained with a null
    // content(). A malicious server can exploit that: send a pull `.raw` file-card
    // delta naming basis H (retained dangling, §7.4-r1), THEN send H as a full-form
    // cfile whose container fails to inflate. When completeFor drives the dependent
    // delta, its basis cannot be materialised. §5.2 — that surfaces as a graceful
    // round-abort at the dependent delta, NOT a `content() orelse unreachable` trap
    // (the remote-triggerable DoS the pre-fix line 5140 would panic on).
    const gpa = std.testing.allocator;
    // A well-framed raw [FossilDelta] for the file card; its bytes are never
    // applied (the round aborts when the basis proves un-materialisable), so the
    // basis it was cut against is immaterial — only its wire framing matters.
    const d = try delta.create(gpa, "some basis plaintext", "some basis plaintext, extended");
    defer gpa.free(d);
    // §7.2/§7.7 corrupt full container: §4.1 frame present (>=4 bytes) but the
    // declared length dwarfs the read cap → OutputTooLarge at read → content()==null.
    // The same decompression-class fixture the retain-verbatim receive test uses.
    const corrupt = "\xff\xff\xff\xff\x00";
    // Card order IS the attack: the dangling `.raw` delta (basis H = test_hash_b)
    // is retained FIRST, then H arrives as the corrupt full-form cfile which drives
    // completeFor over the pending entry (sync.zig:778 → completeEntry `.raw`).
    const body = try std.fmt.allocPrint(gpa, "file {s} {s} {d}\n{s}cfile {s} 4 {d}\n{s}", .{
        test_hash_a, test_hash_b, d.len, d, test_hash_b, corrupt.len, corrupt,
    });
    defer gpa.free(body);
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .pull, .state = .exchange };
    defer client.deinit(gpa);
    try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(gpa, ms.store(), body));
    try std.testing.expectEqual(ClientState.aborted, client.state); // §5.2 graceful round-abort, not a trap
}

test "sync §7.4/§5.2: a held-but-uninflatable full-form cfile that is gimme'd is skipped on the upload path, never trapped" {
    // BLOCKER (W4 upload-path regression). W4 (§7.2/§7.7) retains a stored full
    // cfile container VERBATIM and defers §7.7 inflate/verify to read, so a
    // corrupt full container is interned+retained with `held()==true` but
    // `content()==null`. A malicious server can weaponise that on the UPLOAD
    // path in a single session: send H as a full-form cfile whose container
    // fails to inflate (retained wire-backed, content()==null), THEN `gimme H`.
    // The request composer keys its send buffer on `held()` (§3.3 L532-533) and
    // then does `content(rid).?` (§3.2 L412-413) — a null-deref/assert trap on
    // the corrupt container. §3.3 — a gimme this side cannot MATERIALISE for
    // upload must fold to the same byte-neutral silent-skip as a gimme for an
    // unheld hash (gimmeResponse `.silent_skip`): not queued, no corrupt bytes
    // sent, the request built cleanly.
    const gpa = std.testing.allocator;
    // §7.2/§7.7 corrupt full container: §4.1 frame present (>=4 bytes) but the
    // declared length dwarfs the read cap → OutputTooLarge at read → content()==null.
    // The same decompression-class fixture the retain-verbatim receive test uses.
    const corrupt = "\xff\xff\xff\xff\x00";
    // Card order IS the attack: retain H wire-backed FIRST (corrupt full cfile),
    // then `gimme H` drives the upload-queue decision over the held-but-null rid.
    const body = try std.fmt.allocPrint(gpa, "cfile {s} 4 {d}\n{s}gimme {s}\n", .{
        test_hash_a, corrupt.len, corrupt, test_hash_a,
    });
    defer gpa.free(body);
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body); // §5.2 — no receive-time abort
    try std.testing.expect(client.state != .aborted);
    const rid = ms.store().lookup(test_hash_a).?; // H interned, retained wire-backed
    try std.testing.expect(ms.store().held(rid)); // §7.2 — held via its container
    try std.testing.expectEqual(@as(?[]const u8, null), ms.store().content(rid)); // §7.7 — but not reconstructable
    // §3.3 silent-skip precedent — a gimme this side cannot materialise is not
    // queued for upload (byte-neutral, exactly as an unheld-hash gimme).
    try std.testing.expect(!obs.content_queued);
    try std.testing.expectEqual(@as(usize, 0), client.send_queue.count());
    // §3.2 — composing the next request drives the upload path (drainSendQueue →
    // file-card composer). It must complete GRACEFULLY (the corrupt container is
    // skipped), never trap on `content(rid).?`.
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);
    _ = try client.buildRequest(gpa, ms.store(), &out);
    try std.testing.expectEqual(@as(usize, 0), client.content_sent); // nothing uploaded
}

test "sync §4.1/§3.9: a full-form cfile payload too short to carry the §4.1 length prefix is a fatal framing error" {
    const gpa = std.testing.allocator;
    // Distinct from the decompression-class corruption above (§7.2/§7.7 — retained
    // then read-verified): a payload under 4 bytes cannot carry the §4.1 4-byte
    // length prefix, so it is not a container frame at all. That is a framing
    // violation (§3.9 fatal), the one full-form receive-time reject the retain-
    // verbatim path keeps — retaining it would break `retain`'s container.len >= 4
    // §4.1 precondition.
    const body = try buildCfileBody(gpa, test_hash_a, 4, 2, "\x00\x01");
    defer gpa.free(body);
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(gpa, ms.store(), body));
    try std.testing.expectEqual(ClientState.aborted, client.state);
}

test "sync §7.2-r13/§7.4: a delta-form cfile against a held basis reconstructs and stores under HASH" {
    // §7.2-r13 — the cfile delta form carries a stored COMPRESSED delta; after
    // §4.1 container decode the bytes are the [FossilDelta] against DELTASRC,
    // reconstructed on a held basis (§7.4) and stored under HASH.
    const gpa = std.testing.allocator;
    const basis = "the basis artifact bytes for a cfile-delta reconstruction";
    const target = "the basis artifact bytes for a cfile-delta reconstruction, extended";
    const d = try delta.create(gpa, basis, target);
    defer gpa.free(d);
    const enc = try zlibContainerEncode(gpa, d, .default); // §4.1 stored compressed delta
    defer gpa.free(enc);

    var target_buf: [64]u8 = undefined;
    const target_hash = artifact.artifactName(.sha3_256, target, &target_buf);
    var basis_buf: [64]u8 = undefined;
    const basis_hash = artifact.artifactName(.sha3_256, basis, &basis_buf);

    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    _ = try ms.store().put(basis_hash, basis); // basis already held
    var client = Client{ .operation = .pull, .state = .exchange };
    defer client.deinit(gpa);

    const body = try std.fmt.allocPrint(gpa, "cfile {s} {s} {d} {d}\n{s}", .{ target_hash, basis_hash, target.len, enc.len, enc });
    defer gpa.free(body);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(obs.versioned_content_delivered);
    try std.testing.expect(client.state != .aborted);
    const rid = ms.store().lookup(target_hash).?;
    try std.testing.expectEqualStrings(target, ms.store().content(rid).?);
}

// §7.4/§3.3 — delta-form `file` reception through consumeReply: the handler
// wiring (parse → receiveDelta → PendingDeltaQueue → gimme exclusion), with the
// RFC-worked deltas as the oracle. Expected values are the reconstructed
// artifact bytes and their SHA3-256 names, never fossil-captured.

test "sync §7.4: a delta-form file against a held basis reconstructs and stores under HASH" {
    // §7.1-r5/§7.4 — the file delta form carries the [FossilDelta] uncompressed;
    // a held basis reconstructs the target immediately, stored under HASH whose
    // §7.7 integrity holds over the reconstructed bytes.
    const gpa = std.testing.allocator;
    const basis = "delta-file basis content, the source the delta is taken against";
    const target = "delta-file basis content, the source the delta is taken against, plus a tail";
    const d = try delta.create(gpa, basis, target);
    defer gpa.free(d);
    var tbuf: [64]u8 = undefined;
    const target_hash = artifact.artifactName(.sha3_256, target, &tbuf);
    var bbuf: [64]u8 = undefined;
    const basis_hash = artifact.artifactName(.sha3_256, basis, &bbuf);

    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    _ = try ms.store().put(basis_hash, basis);
    var client = Client{ .operation = .pull, .state = .exchange };
    defer client.deinit(gpa);

    const body = try std.fmt.allocPrint(gpa, "file {s} {s} {d}\n{s}", .{ target_hash, basis_hash, d.len, d });
    defer gpa.free(body);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(obs.versioned_content_delivered);
    try std.testing.expect(client.state != .aborted);
    try std.testing.expectEqualStrings(target, ms.store().content(ms.store().lookup(target_hash).?).?);
    try std.testing.expectEqual(@as(usize, 0), client.pending.itemCount());
}

// §7.1/§7.4/§7.7 — delta-form `file` reception on the SERVER push-ingest path
// (`Server.respond`), the mirror of the client tests above. A real `fossil push`
// sends the pushed manifest as `file HASH DELTASRC SIZE` delta-encoded against a
// basis the server advertised holding (§3.2 igot); these prove the server
// reconstructs it against the held basis, §7.7-verifies, and stores it — with the
// RFC-built delta as the oracle, never fossil-captured bytes.

test "sync §7.1/§7.4/§7.7: server respond reconstructs a delta-form pushed file against a held basis and stores it under HASH" {
    const gpa = std.testing.allocator;
    const basis = "server-held basis manifest the pushed delta is encoded against";
    const target = "server-held basis manifest the pushed delta is encoded against, plus the pushed check-in tail";
    const d = try delta.create(gpa, basis, target);
    defer gpa.free(d);
    var tbuf: [64]u8 = undefined;
    const target_hash = artifact.artifactName(.sha3_256, target, &tbuf);
    var bbuf: [64]u8 = undefined;
    const basis_hash = artifact.artifactName(.sha3_256, basis, &bbuf);

    var ss = repostate.MemStore.init(gpa);
    defer ss.deinit();
    _ = try ss.store().put(basis_hash, basis); // §3.2 — the server already holds the basis

    const server = Server{ .version = h2_server_version };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);

    // §7.1 — a push carrying the delta-form file card, exactly fossil's shape.
    const request = try std.fmt.allocPrint(gpa, "push {s} {s}\nfile {s} {s} {d}\n{s}", .{ h2_servercode, h2_projectcode, target_hash, basis_hash, d.len, d });
    defer gpa.free(request);
    const reply = try server.respond(gpa, ss.store(), request, &out);

    try std.testing.expect(!std.mem.startsWith(u8, reply, "error ")); // accepted, not a §5.2 error card
    const rid = ss.store().lookup(target_hash) orelse return error.TestUnexpectedResult;
    try std.testing.expectEqualStrings(target, ss.store().content(rid).?); // §7.4 reconstructed + stored
    try std.testing.expect(artifact.verify(target_hash, ss.store().content(rid).?)); // §7.7 integrity holds

    // §3.8 — stateless: a second endpoint with the same held basis and the same
    // request reaches the identical stored result (same request + same store).
    var ss2 = repostate.MemStore.init(gpa);
    defer ss2.deinit();
    _ = try ss2.store().put(basis_hash, basis);
    var out2: std.ArrayListUnmanaged(u8) = .empty;
    defer out2.deinit(gpa);
    _ = try server.respond(gpa, ss2.store(), request, &out2);
    const rid2 = ss2.store().lookup(target_hash) orelse return error.TestUnexpectedResult;
    try std.testing.expectEqualStrings(target, ss2.store().content(rid2).?);
}

test "sync §7.7: server respond rejects a delta-form pushed file whose reconstruction hashes to the wrong HASH, store untouched" {
    // §7.7 — pre-storage integrity hashes the reconstructed bytes: a delta that
    // reconstructs to bytes not matching the announced HASH is rejected, and the
    // fault stops the scan before pass-2 ingest, so nothing lands in the store.
    const gpa = std.testing.allocator;
    const basis = "server-held basis for the wrong-hash integrity rejection case";
    const target = "server-held basis for the wrong-hash integrity rejection case, extended payload";
    const d = try delta.create(gpa, basis, target);
    defer gpa.free(d);
    var bbuf: [64]u8 = undefined;
    const basis_hash = artifact.artifactName(.sha3_256, basis, &bbuf);
    // A HASH that is NOT the name of the reconstructed `target` (the basis's own
    // hash stands in as a valid-shape-but-wrong name).
    const wrong_hash = basis_hash;

    var ss = repostate.MemStore.init(gpa);
    defer ss.deinit();
    _ = try ss.store().put(basis_hash, basis);
    const held_before = ss.store().count();

    const server = Server{ .version = h2_server_version };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);

    const request = try std.fmt.allocPrint(gpa, "push {s} {s}\nfile {s} {s} {d}\n{s}", .{ h2_servercode, h2_projectcode, wrong_hash, basis_hash, d.len, d });
    defer gpa.free(request);
    const reply = try server.respond(gpa, ss.store(), request, &out);

    try std.testing.expect(std.mem.startsWith(u8, reply, "error malformed file")); // §5.2 framing
    try std.testing.expectEqual(held_before, ss.store().count()); // §7.7 — no new artifact interned/stored
}

test "sync §7.4/§3.8: server respond rejects a delta-form pushed file whose basis it does not hold and is NOT shunned" {
    // §3.8 — the stateless server keeps no cross-request PendingDeltaQueue, so it
    // cannot retain a dangling delta until a later request delivers the basis
    // (the client's §7.4 dangling path). A basis absent from the store is rejected
    // cleanly (never silently dropped); this branch is unreachable by a conformant
    // fossil push, which deltas only against advertised (igot) holdings.
    //
    // §7.7 L1444-1447 opens "For an admitted target whose DELTASRC name is in the
    // repository SHUN TABLE" — the non-error carve-out is scoped to that cause of
    // absence and to no other. Here the basis is simply unknown, so the fault
    // stands. The shunned-source case is the separate §7.7 L1444-1450 test below.
    const gpa = std.testing.allocator;
    const basis = "a basis the server was never given, so it cannot be resolved";
    const target = "a basis the server was never given, so it cannot be resolved, with a tail";
    const d = try delta.create(gpa, basis, target);
    defer gpa.free(d);
    var tbuf: [64]u8 = undefined;
    const target_hash = artifact.artifactName(.sha3_256, target, &tbuf);
    var bbuf: [64]u8 = undefined;
    const basis_hash = artifact.artifactName(.sha3_256, basis, &bbuf); // NOT stored

    var ss = repostate.MemStore.init(gpa);
    defer ss.deinit();
    const held_before = ss.store().count();

    const server = Server{ .version = h2_server_version };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);

    const request = try std.fmt.allocPrint(gpa, "push {s} {s}\nfile {s} {s} {d}\n{s}", .{ h2_servercode, h2_projectcode, target_hash, basis_hash, d.len, d });
    defer gpa.free(request);
    const reply = try server.respond(gpa, ss.store(), request, &out);

    try std.testing.expect(std.mem.startsWith(u8, reply, "error malformed file")); // clean rejection
    try std.testing.expect(ss.store().lookup(target_hash) == null); // target never landed
    try std.testing.expectEqual(held_before, ss.store().count()); // store untouched
}

test "sync §7.4/§3.3: a dangling delta is retained, not rejected, and its target is not re-gimme'd; the basis completes it" {
    // §3.3 L528-532 — "For a dangling delta, retain the received delta under
    // HASH, delete HASH's phantom, create DELTASRC's phantom, and set
    // content-received; HASH is not requested again." OPTION B derives
    // content-received via pending.hasTarget, so a target that was an igot
    // phantom drops out of gimme while the basis phantom stays eligible.
    const gpa = std.testing.allocator;
    const basis = "dangling-delta basis, arrives in a later round to complete the target";
    const target = "dangling-delta basis, arrives in a later round to complete the target, extended";
    const d = try delta.create(gpa, basis, target);
    defer gpa.free(d);
    var tbuf: [64]u8 = undefined;
    const target_hash = artifact.artifactName(.sha3_256, target, &tbuf);
    var bbuf: [64]u8 = undefined;
    const basis_hash = artifact.artifactName(.sha3_256, basis, &bbuf);

    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .pull, .state = .exchange };
    defer client.deinit(gpa);
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);

    // An igot first makes the target a genuine store phantom (gimme-eligible).
    const igot_body = try std.fmt.allocPrint(gpa, "igot {s}\n", .{target_hash});
    defer gpa.free(igot_body);
    _ = try client.consumeReply(gpa, ms.store(), igot_body);
    try std.testing.expect(ms.store().content(ms.store().lookup(target_hash).?) == null);

    // The dangling delta arrives (basis absent): retained, not rejected.
    const d_body = try std.fmt.allocPrint(gpa, "file {s} {s} {d}\n{s}", .{ target_hash, basis_hash, d.len, d });
    defer gpa.free(d_body);
    const obs = try client.consumeReply(gpa, ms.store(), d_body);
    try std.testing.expect(client.state != .aborted); // §7.4 MUST NOT reject
    try std.testing.expect(obs.phantom_created); // §3.7(a) DELTASRC phantom
    try std.testing.expect(client.pending.hasTarget(target_hash)); // content-received
    try std.testing.expect(ms.store().content(ms.store().lookup(basis_hash).?) == null); // basis phantom

    // §3.3 — the next request gimmes the basis but NOT the content-received
    // target. The basis is the only phantom (content null), so it is gimme'd; the
    // target's hash never appears (neither gimme nor igot).
    const req = try client.buildRequest(gpa, ms.store(), &out);
    const gimme_basis = try std.fmt.allocPrint(gpa, "gimme {s}", .{basis_hash});
    defer gpa.free(gimme_basis);
    try expectHasLine(req, gimme_basis); // gimme DELTASRC
    try std.testing.expect(std.mem.indexOf(u8, req, target_hash) == null); // not re-requested

    // The basis arrives as full content → completion reconstructs the target.
    const basis_body = try std.fmt.allocPrint(gpa, "file {s} {d}\n{s}", .{ basis_hash, basis.len, basis });
    defer gpa.free(basis_body);
    _ = try client.consumeReply(gpa, ms.store(), basis_body);
    try std.testing.expectEqualStrings(target, ms.store().content(ms.store().lookup(target_hash).?).?);
    try std.testing.expectEqual(@as(usize, 0), client.pending.itemCount());
}

test "artifact-format §3.2 L347-350: a DELTASRC of another width is malformed — the peer never chooses the phantom set's key domain" {
    // THE KEY-DOMAIN BOUNDARY, from the direction a peer controls. `ShunSet` and
    // `PendingDeltaQueue` are keyed by COMPLETE artifact name, and a dangling
    // delta is the third way a name enters the phantom set — after `igot` and
    // `gimme`, both of which reject any other width as malformed. But
    // `parseFileCard`/`parseCfileCard` validate arity and the byte counts only, so
    // before the gate in `receiveDelta` a `file HASH zz SIZE` card interned "zz"
    // as a phantom and every subsequent §3.3 scan asked the shun table and the
    // pending queue about it — one card, one crashed `zit pull`, in EVERY
    // repository (the shun table need not even exist). The card is rejected
    // instead, and the store is left with no such name.
    const gpa = std.testing.allocator;
    const basis = "the basis this peer names with an abbreviated, out-of-domain token";
    const target = "the basis this peer names with an abbreviated, out-of-domain token, extended";
    const d = try delta.create(gpa, basis, target);
    defer gpa.free(d);
    var tbuf: [64]u8 = undefined;
    const target_hash = artifact.artifactName(.sha3_256, target, &tbuf);
    const short_src = "zz"; // neither 40 nor 64 hex

    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    // `.none` on purpose: nothing about this depends on a table existing.
    var client = Client{ .operation = .pull, .state = .exchange, .shun = ShunSet.none };
    defer client.deinit(gpa);

    const body = try std.fmt.allocPrint(gpa, "file {s} {s} {d}\n{s}", .{ target_hash, short_src, d.len, d });
    defer gpa.free(body);
    try std.testing.expectError(error.FatalProtocolError, client.consumeReply(gpa, ms.store(), body));
    try std.testing.expectEqual(ClientState.aborted, client.state);
    try std.testing.expect(ms.store().lookup(short_src) == null);
    try std.testing.expectEqual(@as(usize, 0), client.pending.itemCount());

    // And the filter itself ANSWERS an out-of-domain width rather than trapping —
    // the interface's own published contract, kept independent of which boundaries
    // happen to guard it today.
    try std.testing.expect(!ShunSet.none.contains(short_src));
    try std.testing.expect(!ShunSet.none.contains(""));

    // Negative space: the same card with a COMPLETE DELTASRC is the ordinary
    // dangling delta, retained rather than rejected.
    var bbuf: [64]u8 = undefined;
    const basis_hash = artifact.artifactName(.sha3_256, basis, &bbuf);
    var ok_ms = repostate.MemStore.init(gpa);
    defer ok_ms.deinit();
    var ok = Client{ .operation = .pull, .state = .exchange, .shun = ShunSet.none };
    defer ok.deinit(gpa);
    const ok_body = try std.fmt.allocPrint(gpa, "file {s} {s} {d}\n{s}", .{ target_hash, basis_hash, d.len, d });
    defer gpa.free(ok_body);
    _ = try ok.consumeReply(gpa, ok_ms.store(), ok_body);
    try std.testing.expect(ok.state != .aborted);
    try std.testing.expect(ok_ms.store().lookup(basis_hash) != null);
}

test "sync §7.4: a chained delta reception completes to a fixpoint when the chain root arrives (C<-A<-B)" {
    // The filed §7.4 erratum: completion is recursive. C (basis A) and A (basis
    // B) both arrive before B; storing B completes A, and storing A completes C.
    // A direct-only receiver would leave C a permanent phantom.
    const gpa = std.testing.allocator;
    const b_bytes = "chain root B, the on-wire full-content basis of the whole chain";
    const a_bytes = "chain root B, the on-wire full-content basis of the whole chain, then A";
    const c_bytes = "chain root B, the on-wire full-content basis of the whole chain, then A, then C";
    const d_a = try delta.create(gpa, b_bytes, a_bytes);
    defer gpa.free(d_a);
    const d_c = try delta.create(gpa, a_bytes, c_bytes);
    defer gpa.free(d_c);
    var ab: [64]u8 = undefined;
    var bb: [64]u8 = undefined;
    var cb: [64]u8 = undefined;
    const a_hash = artifact.artifactName(.sha3_256, a_bytes, &ab);
    const b_hash = artifact.artifactName(.sha3_256, b_bytes, &bb);
    const c_hash = artifact.artifactName(.sha3_256, c_bytes, &cb);

    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .pull, .state = .exchange };
    defer client.deinit(gpa);

    // C then A arrive dangling (bases A then B still absent).
    const c_body = try std.fmt.allocPrint(gpa, "file {s} {s} {d}\n{s}", .{ c_hash, a_hash, d_c.len, d_c });
    defer gpa.free(c_body);
    _ = try client.consumeReply(gpa, ms.store(), c_body);
    const a_body = try std.fmt.allocPrint(gpa, "file {s} {s} {d}\n{s}", .{ a_hash, b_hash, d_a.len, d_a });
    defer gpa.free(a_body);
    _ = try client.consumeReply(gpa, ms.store(), a_body);
    try std.testing.expectEqual(@as(usize, 2), client.pending.itemCount());

    // B arrives full-content → cascade A then C to fixpoint.
    const b_body = try std.fmt.allocPrint(gpa, "file {s} {d}\n{s}", .{ b_hash, b_bytes.len, b_bytes });
    defer gpa.free(b_body);
    _ = try client.consumeReply(gpa, ms.store(), b_body);
    try std.testing.expectEqualStrings(a_bytes, ms.store().content(ms.store().lookup(a_hash).?).?);
    try std.testing.expectEqualStrings(c_bytes, ms.store().content(ms.store().lookup(c_hash).?).?);
    try std.testing.expectEqual(@as(usize, 0), client.pending.itemCount()); // no permanent phantom
}

test "sync §7.4/§7.7: a delta that reconstructs to the wrong hash is rejected (fatal)" {
    // §7.7 — integrity hashes the reconstructed bytes; a non-clone file whose
    // reconstruction does not hash to HASH is rejected before storage, never
    // landing bytes under a name they do not match.
    const gpa = std.testing.allocator;
    const basis = "wrong-hash basis content for the delta reconstruction check";
    const target = "wrong-hash basis content for the delta reconstruction check, appended";
    const d = try delta.create(gpa, basis, target);
    defer gpa.free(d);
    var bbuf: [64]u8 = undefined;
    const basis_hash = artifact.artifactName(.sha3_256, basis, &bbuf);
    const wrong_hash = "a" ** 64; // a valid 64-hex name that is NOT the target's

    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    _ = try ms.store().put(basis_hash, basis);
    var client = Client{ .operation = .pull, .state = .exchange };
    defer client.deinit(gpa);

    const body = try std.fmt.allocPrint(gpa, "file {s} {s} {d}\n{s}", .{ wrong_hash, basis_hash, d.len, d });
    defer gpa.free(body);
    try std.testing.expectError(Client.ConsumeError.FatalProtocolError, client.consumeReply(gpa, ms.store(), body));
    try std.testing.expectEqual(ClientState.aborted, client.state);
    try std.testing.expect(ms.store().lookup(wrong_hash) == null); // nothing stored under the wrong name
}

fn consumeCfileStoresNoLeak(gpa: std.mem.Allocator) !void {
    // The store lives on the base allocator so only consumeReply's decode
    // buffer and deinit see the failing allocator (mirrors consumeGimme...).
    const plaintext = "cfile allocation-path payload";
    const enc = try zlibContainerEncode(std.testing.allocator, plaintext, .default);
    defer std.testing.allocator.free(enc);
    const body = try buildCfileBody(std.testing.allocator, test_hash_a, plaintext.len, enc.len, enc);
    defer std.testing.allocator.free(body);
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(obs.versioned_content_delivered);
}

test "sync §7.2-r6: consumeReply stores a full-content cfile under allocation failure" {
    try std.testing.checkAllAllocationFailures(std.testing.allocator, consumeCfileStoresNoLeak, .{});
}

test "sync §7.2 fuzz: cfile framing+container decode round-trips and never crashes" {
    // Invariant (§7.2-r2/r3/r5/r6): a full-content cfile framed by CSIZE and
    // carrying a §4.1 container of x stores exactly x and never aborts;
    // adversarial CSIZE/payload bytes only ever yield a declared error, never
    // a crash (memory-safe under ReleaseSafe). Fixed seed for exact replay.
    const gpa = std.testing.allocator;
    var prng = std.Random.DefaultPrng.init(0xCF_11_E7_2C);
    const rand = prng.random();
    var raw: [96]u8 = undefined;
    var round: usize = 0;
    while (round < 512) : (round += 1) {
        if (rand.boolean()) {
            // Well-formed branch: a real container round-trips exactly.
            const n = rand.intRangeAtMost(usize, 0, raw.len);
            for (raw[0..n]) |*b| b.* = rand.int(u8);
            const enc = try zlibContainerEncode(gpa, raw[0..n], .default);
            defer gpa.free(enc);
            const body = try buildCfileBody(gpa, test_hash_a, n, enc.len, enc);
            defer gpa.free(body);
            var ms = repostate.MemStore.init(gpa);
            defer ms.deinit();
            var client = Client{ .operation = .sync, .state = .exchange };
            defer client.deinit(gpa);
            _ = client.consumeReply(gpa, ms.store(), body) catch |e| {
                try std.testing.expect(e == error.OutOfMemory);
                continue;
            };
            try std.testing.expect(client.state != .aborted);
            const rid = ms.store().lookup(test_hash_a).?;
            try std.testing.expectEqualStrings(raw[0..n], ms.store().content(rid).?);
        } else {
            // Adversarial branch: random CSIZE and random payload. The only
            // outcomes are a declared error (fatal/OOM) or a
            // clean parse — never a crash.
            const plen = rand.intRangeAtMost(usize, 0, raw.len);
            for (raw[0..plen]) |*b| b.* = rand.int(u8);
            const csize = rand.intRangeAtMost(usize, 0, raw.len + 8); // may over/under-run the payload
            const body = try buildCfileBody(gpa, test_hash_a, rand.int(u16), csize, raw[0..plen]);
            defer gpa.free(body);
            var ms = repostate.MemStore.init(gpa);
            defer ms.deinit();
            var client = Client{ .operation = .sync, .state = .exchange };
            defer client.deinit(gpa);
            _ = client.consumeReply(gpa, ms.store(), body) catch |e| switch (e) {
                error.OutOfMemory, error.FatalProtocolError => continue,
            };
        }
    }
}

// ---------------------------------------------------------------------------
// §7.5 — Private-content receive-side gates through consumeReply.
// ---------------------------------------------------------------------------

test "sync §7.5 L1376-1377: a private file without the grant is discarded without error" {
    const gpa = std.testing.allocator;
    // `private` then a full-content file card, with no send-private grant.
    const body = "private\nfile " ++ test_hash_a ++ " 5\nHELLO";
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange, .private_granted = false };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(client.state != .aborted); // discarded without error
    try std.testing.expect(ms.store().lookup(test_hash_a) == null); // store untouched
    try std.testing.expect(obs.private_content_discarded);
    try std.testing.expect(!obs.versioned_content_delivered); // no §3.7(a) progress
    try std.testing.expect(!obs.private_content_delivered);
    try std.testing.expectEqual(@as(usize, 0), client.phantoms.count()); // no phantom created
    // consumeReply asserts cursor == body.len, so the offset resumed past the
    // 5 payload bytes: HELLO was never parsed as a card.
}

test "sync §7.5 L1376-1377: a private cfile without the grant is discarded without decoding" {
    const gpa = std.testing.allocator;
    // A CSIZE-framed payload that is NOT a valid §4.1 container: if the store
    // path ran, zlibContainerDecode would abort — so a non-aborting round
    // proves the discard gate fires before any decode.
    const garbage = "not a fossil zlib container at all";
    const cfile = try buildCfileBody(gpa, test_hash_a, 99, garbage.len, garbage);
    defer gpa.free(cfile);
    const body = try std.fmt.allocPrint(gpa, "private\n{s}", .{cfile});
    defer gpa.free(body);
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange, .private_granted = false };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(client.state != .aborted); // no decode required to advance
    try std.testing.expect(ms.store().lookup(test_hash_a) == null); // store untouched
    try std.testing.expect(obs.private_content_discarded);
    try std.testing.expect(!obs.versioned_content_delivered);
}

test "sync §7.5 L1376-1377: a private file with the grant is stored and dephantomized" {
    const gpa = std.testing.allocator;
    // A public igot seeds the phantom; `private` then the file card, with the
    // grant present, stores the content and removes the phantom.
    const body = "igot " ++ test_hash_a ++ "\nprivate\nfile " ++ test_hash_a ++ " 5\nHELLO";
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange, .private_granted = true };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    const rid = ms.store().lookup(test_hash_a).?;
    try std.testing.expectEqualStrings("HELLO", ms.store().content(rid).?); // store.put occurred
    try std.testing.expect(obs.private_content_delivered);
    try std.testing.expect(!obs.private_content_discarded);
    try std.testing.expect(obs.versioned_content_delivered);
    try std.testing.expectEqual(@as(usize, 0), client.phantoms.count()); // phantom for HASH removed
}

test "sync §7.5 L1376-1377: the store gate is general — a private cfile with the grant is decoded and stored" {
    const gpa = std.testing.allocator;
    const plaintext = "private cfile stored";
    const enc = try zlibContainerEncode(gpa, plaintext, .default);
    defer gpa.free(enc);
    const cfile = try buildCfileBody(gpa, test_hash_a, plaintext.len, enc.len, enc);
    defer gpa.free(cfile);
    const body = try std.fmt.allocPrint(gpa, "private\n{s}", .{cfile});
    defer gpa.free(body);
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange, .private_granted = true };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    const rid = ms.store().lookup(test_hash_a).?;
    try std.testing.expectEqualStrings(plaintext, ms.store().content(rid).?); // decoded + stored
    try std.testing.expect(obs.private_content_delivered);
    try std.testing.expect(obs.versioned_content_delivered);
}

test "sync §7.5 receive: a private marker survives uvfile and binds a later file" {
    const gpa = std.testing.allocator;
    const empty_hash = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange, .private_granted = true };
    defer client.deinit(gpa);

    // One round: marker, declined uv card, then the content the marker binds.
    const obs = try client.consumeReply(gpa, ms.store(), "private\nuvfile a\\sb 1 " ++ empty_hash ++ " 0 0\n" ++ "file " ++ test_hash_a ++ " 5\nHELLO");
    try std.testing.expectEqual(@as(u64, 1), client.uv_skipped); // #58: counted, not errored
    const rid = ms.store().lookup(test_hash_a).?;
    try std.testing.expectEqualStrings("HELLO", ms.store().content(rid).?);
    try std.testing.expect(obs.private_content_delivered);
    try std.testing.expect(!obs.private_content_discarded);
    try std.testing.expect(!client.private_pending);
}

test "sync §7.5 receive: a private marker survives uvfile and binds a later cfile" {
    const gpa = std.testing.allocator;
    const empty_hash = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
    const plaintext = "private cfile after uvfile";
    const encoded = try zlibContainerEncode(gpa, plaintext, .default);
    defer gpa.free(encoded);
    const cfile = try buildCfileBody(gpa, test_hash_a, plaintext.len, encoded.len, encoded);
    defer gpa.free(cfile);
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange, .private_granted = true };
    defer client.deinit(gpa);

    const combined = try std.mem.concat(gpa, u8, &.{ "private\nuvfile a\\sb 1 " ++ empty_hash ++ " 0 0\n", cfile });
    defer gpa.free(combined);
    const obs = try client.consumeReply(gpa, ms.store(), combined);
    try std.testing.expectEqual(@as(u64, 1), client.uv_skipped); // #58: counted, not errored
    const rid = ms.store().lookup(test_hash_a).?;
    try std.testing.expectEqualStrings(plaintext, ms.store().content(rid).?);
    try std.testing.expect(obs.private_content_delivered);
    try std.testing.expect(!obs.private_content_discarded);
    try std.testing.expect(!client.private_pending);
}

test "sync §7.5 receive: an end-of-reply private marker is nonfatal and remains pending" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);

    _ = try client.consumeReply(gpa, ms.store(), "private\n");
    try std.testing.expectEqual(ClientState.terminated, client.state);
    try std.testing.expect(client.private_pending);
}

test "sync §7.5 L1374-1376: a private igot is not an error and processing continues" {
    const gpa = std.testing.allocator;
    // A private igot followed by a second card in the same body: no abort, and
    // the trailing cookie card is dispatched (its store effect is observable).
    const body = "igot " ++ test_hash_a ++ " 1\ncookie tok\n";
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(client.state != .aborted);
    try std.testing.expect(obs.cookie_stored); // the following card was processed
}

test "sync §7.5 L1374-1376: an unauthorized private igot for an unknown hash creates no phantom" {
    const gpa = std.testing.allocator;
    // §7.5 L1374-1376 scopes the no-phantom disposition to a private igot from
    // a peer that LACKS private-content authorization: at the client receiver
    // that is private_granted == false (no accepted send-private pragma). Such
    // an igot is not an error and does not intern or phantom HASH (lookup, not
    // intern) — the content will never be sent to this receiver.
    const body = "igot " ++ test_hash_a ++ " 1\n";
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange, .private_granted = false };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expectEqual(@as(usize, 0), client.phantoms.count());
    try std.testing.expect(!obs.phantom_created);
    try std.testing.expect(ms.store().lookup(test_hash_a) == null); // lookup, not intern
}

test "sync §7.5 L1381-1382: an authorized private igot for an unknown hash creates a phantom to pull it" {
    const gpa = std.testing.allocator;
    // §7.5 L1381-1382 — when the receiver holds private-content authorization
    // (private_granted, the send-private pragma accepted and gated on the
    // client holding Private) it MAY pull private content, so a private igot is
    // handled like a public one (§3.3 r24): it interns HASH and creates a
    // phantom so §3.3(c) requests the content, rather than suppressing it.
    const body = "igot " ++ test_hash_a ++ " 1\n";
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange, .private_granted = true };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(ms.store().lookup(test_hash_a) != null); // interned, unlike the unauthorized case
    const rid = ms.store().lookup(test_hash_a).?;
    try std.testing.expect(client.phantoms.getPtr(rid) != null);
    try std.testing.expect(obs.phantom_created);
    try std.testing.expectEqual(@as(usize, 1), client.phantoms.count());
}

test "sync §7.5 L1379-1380: a private igot marks an existing phantom private" {
    const gpa = std.testing.allocator;
    // A public igot creates the phantom; the private igot for the same hash
    // marks it private without adding a second entry.
    const body = "igot " ++ test_hash_a ++ "\nigot " ++ test_hash_a ++ " 1\n";
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    const rid = ms.store().lookup(test_hash_a).?;
    try std.testing.expect(client.phantoms.getPtr(rid).?.private);
    try std.testing.expect(obs.phantom_marked_private);
    try std.testing.expectEqual(@as(usize, 1), client.phantoms.count());
}

test "sync §7.5 L1379-1380 / §3.3 L527-529: a marked-private phantom is gimme-ineligible" {
    const gpa = std.testing.allocator;
    const body = "igot " ++ test_hash_a ++ "\nigot " ++ test_hash_a ++ " 1\n";
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    _ = try client.consumeReply(gpa, ms.store(), body);
    const rid = ms.store().lookup(test_hash_a).?;
    const marked = client.phantoms.getPtr(rid).?.private;
    try std.testing.expect(marked);
    // §3.3(c): with private-content sync not enabled, the marked-private phantom
    // is not gimme-eligible — the receiver stops re-requesting it (the single-
    // round half of deferred DST invariant (a)).
    try std.testing.expect(!gimmeEligible(.{ .peer_lacks = false, .shunned = false, .private = marked, .private_sync_enabled = false }));
}

test "sync §7.5 L1375-1376: a private igot records peer-has and processing continues" {
    const gpa = std.testing.allocator;
    const body = "igot " ++ test_hash_a ++ " 1\ncookie tok\n";
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(obs.private_igot_peer_has);
    try std.testing.expect(obs.cookie_stored); // a trailing card is still processed
}

test "sync §7.5 L1376-1377: send-private is emitted only when the client holds Private" {
    // §7.5 L1376-1377 — pulling private content MUST be preceded by a send-
    // private the client is entitled to send; §9.5 L1983-1984 gates that
    // entitlement on the client holding the Private (x) capability.
    const Case = struct { wants: bool, has_x: bool, want: bool };
    const cases = [_]Case{
        .{ .wants = true, .has_x = true, .want = true },
        .{ .wants = true, .has_x = false, .want = false },
        .{ .wants = false, .has_x = true, .want = false },
        .{ .wants = false, .has_x = false, .want = false },
    };
    for (cases) |c| try std.testing.expectEqual(c.want, clientSendsSendPrivate(c.wants, c.has_x));
    // The caller derives has_private_x from the established capability set (B1).
    const held = capabilitySetFromLetters("x");
    try std.testing.expect(clientSendsSendPrivate(true, held.contains(.private)));
    const unheld = capabilitySetFromLetters("oi");
    try std.testing.expect(!clientSendsSendPrivate(true, unheld.contains(.private)));
}

// ---------------------------------------------------------------------------
// A0 transport half 2 — the stateless Server.respond. Every expected value is
// transcribed from the sync RFC and its Appendix A worked exchange, never from
// running fossil.
// ---------------------------------------------------------------------------

// §A.1 L2443-2473 — the two repositories share this project code and the five
// artifact hashes of the worked push/pull convergence.
const h2_servercode = "da3e69ab0655a7dd2aba2520b2d3302a97c85787";
const h2_projectcode = "aa87ea41b0bb05ba528f7a3bdba700d7e23d6318";
const h2_manifest_x = "8e0933ed1ef9aa0051d185c8123f0b9ac79865e4b70098b877543fca0092c259";
const h2_alpha = "b1c1b05fea4f52bd90b15fdffd13f5c91dce6505f498eeb79b4952c2ea8d4d15";
const h2_ancestor = "c5b2e0ecb779a7616b32553d973bc9c4ae5a07569a2cee18001386da6bd9ae57";
const h2_manifest_y = "4361252d0f450879dcd2e0b6a42b95ffe8a41d8e948130253ae8eefa7f4b79ea";
const h2_beta = "f5116517775075ea483901021282ba97eebe5a2b993a47eb78198adc10efe128";
// §A.1 L2448 / §10.1 — a dated client/server version pragma pair.
const h2_server_version = VersionInfo{ .version = 22800, .datetime = .{ .date = 20260311, .time = 113146 } };

// §A.1 L2448-2454 — the client's Round 1 request body, transcribed verbatim.
const h2_round1_request =
    "pragma client-version 22800 20260311 113146\n" ++
    "pull " ++ h2_servercode ++ " " ++ h2_projectcode ++ "\n" ++
    "push " ++ h2_servercode ++ " " ++ h2_projectcode ++ "\n" ++
    "igot " ++ h2_manifest_x ++ "\n" ++
    "igot " ++ h2_alpha ++ "\n" ++
    "igot " ++ h2_ancestor ++ "\n" ++
    "# F2A895EF75B47B7157664A7FEAC978EFFFDB01C4\n";

// §A.1 L2469-2473 — a store seeded with the server's Round 1 holdings: the
// shared ancestor plus the two artifacts only it holds.
fn h2ServerHoldings(ms: *repostate.MemStore) !void {
    _ = try ms.store().put(h2_ancestor, "shared ancestor manifest");
    _ = try ms.store().put(h2_manifest_y, "server checkin manifest");
    _ = try ms.store().put(h2_beta, "beta content added server-side\n");
}

test "sync §A.1: respond gimmes what the server lacks, igots its extras, and never gimmes content it already holds" {
    const gpa = std.testing.allocator;
    var ss = repostate.MemStore.init(gpa);
    defer ss.deinit();
    try h2ServerHoldings(&ss);
    const server = Server{ .version = h2_server_version };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);

    const reply = try server.respond(gpa, ss.store(), h2_round1_request, &out);

    // §A.1 L2470-2471 / §3.3 L511-518 — the server interned the client's two
    // unheld igots as phantoms and requests their content with gimme.
    try std.testing.expect(std.mem.indexOf(u8, reply, "gimme " ++ h2_manifest_x) != null);
    try std.testing.expect(std.mem.indexOf(u8, reply, "gimme " ++ h2_alpha) != null);
    // §A.1 L2472-2473 / §8.6 L1753-1754 — the two holdings the client did not
    // announce are advertised with igot.
    try std.testing.expect(std.mem.indexOf(u8, reply, "igot " ++ h2_manifest_y) != null);
    try std.testing.expect(std.mem.indexOf(u8, reply, "igot " ++ h2_beta) != null);
    // §A.1 L2469-2474 — the ancestor the server already holds is never gimme'd.
    try std.testing.expect(std.mem.indexOf(u8, reply, "gimme " ++ h2_ancestor) == null);
}

test "sync §3.8: respond is stateless — a repeat over the same store and request yields identical bytes" {
    const gpa = std.testing.allocator;
    var ss = repostate.MemStore.init(gpa);
    defer ss.deinit();
    try h2ServerHoldings(&ss);
    const server = Server{ .version = h2_server_version };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);

    // §3.1 L262-264 — only the client runs a state machine. The first call
    // interns the client's igots as phantoms; the second must re-derive the same
    // bytes, proving the phantom interning is idempotent and no Server field
    // carries state across the two calls.
    const first = try server.respond(gpa, ss.store(), h2_round1_request, &out);
    const first_copy = try gpa.dupe(u8, first);
    defer gpa.free(first_copy);
    const second = try server.respond(gpa, ss.store(), h2_round1_request, &out);
    try std.testing.expectEqualStrings(first_copy, second);
}

test "sync §10.1: respond emits server-version for a dated client-version and suppresses it for a bare one" {
    const gpa = std.testing.allocator;
    var ss = repostate.MemStore.init(gpa);
    defer ss.deinit();
    const server = Server{ .version = h2_server_version };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);

    // §9.1 L1854-1856 (r11/r12) — DATE and TIME present ⇒ server-version replies.
    const dated = try server.respond(gpa, ss.store(), "pragma client-version 22800 20260311 113146\n", &out);
    try std.testing.expect(std.mem.indexOf(u8, dated, "pragma server-version") != null);

    // §9.1 L1857-1859 — a VERSION-only client-version suppresses server-version.
    const bare = try server.respond(gpa, ss.store(), "pragma client-version 22800\n", &out);
    try std.testing.expect(std.mem.indexOf(u8, bare, "pragma server-version") == null);
}

test "sync §6.2/§6.4: a login built by buildRequest verifies against the same stored secret with no error card" {
    const gpa = std.testing.allocator;
    // §6.3 L1091-1095 — both endpoints hold the same derived shared secret.
    const secret: [40]u8 = deriveSharedSecret(h2_projectcode, "alice", "correct-horse");

    var cs = repostate.MemStore.init(gpa);
    defer cs.deinit();
    _ = try cs.store().put(h2_alpha, "alpha content from client\n");
    var client = Client{
        .operation = .sync,
        .self_servercode = h2_servercode,
        .project_code = h2_projectcode,
        .version = h2_server_version,
        .credential = .{ .user = "alice", .secret_hex = secret },
    };
    defer client.deinit(gpa);
    var req_buf: std.ArrayListUnmanaged(u8) = .empty;
    defer req_buf.deinit(gpa);
    const request = try client.buildRequest(gpa, cs.store(), &req_buf);

    var ss = repostate.MemStore.init(gpa);
    defer ss.deinit();
    const server = Server{ .version = h2_server_version, .credential = .{ .user = "alice", .secret_hex = secret } };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);

    // §6.4 L1137-1146 — the nonce covers exactly the post-login body, so the
    // server's independent SIGNATURE reconstruction matches: no error card.
    const reply = try server.respond(gpa, ss.store(), request, &out);
    try std.testing.expect(!std.mem.startsWith(u8, reply, "error "));
}

test "sync §6.4: a login whose SIGNATURE verifies against a different stored secret is rejected with an error card" {
    const gpa = std.testing.allocator;
    const client_secret: [40]u8 = deriveSharedSecret(h2_projectcode, "alice", "correct-horse");
    const server_secret: [40]u8 = deriveSharedSecret(h2_projectcode, "alice", "wrong-password");

    var cs = repostate.MemStore.init(gpa);
    defer cs.deinit();
    _ = try cs.store().put(h2_alpha, "alpha content from client\n");
    var client = Client{
        .operation = .sync,
        .self_servercode = h2_servercode,
        .project_code = h2_projectcode,
        .version = h2_server_version,
        .credential = .{ .user = "alice", .secret_hex = client_secret },
    };
    defer client.deinit(gpa);
    var req_buf: std.ArrayListUnmanaged(u8) = .empty;
    defer req_buf.deinit(gpa);
    const request = try client.buildRequest(gpa, cs.store(), &req_buf);

    var ss = repostate.MemStore.init(gpa);
    defer ss.deinit();
    const server = Server{ .version = h2_server_version, .credential = .{ .user = "alice", .secret_hex = server_secret } };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);

    // §6.4 L1152-1153 / §5.2 L1019-1022 — the reconstructed SIGNATURE differs,
    // so the server resets its output and replies with an error card.
    const reply = try server.respond(gpa, ss.store(), request, &out);
    try std.testing.expect(std.mem.startsWith(u8, reply, "error "));
}

test "sync §6.1: a request carrying two login cards is an authentication-fatal error card" {
    const gpa = std.testing.allocator;
    const hex40 = "0123456789abcdef0123456789abcdef01234567";
    // §6.1 L1078-1079 — more than one login card in one request is fatal.
    const request =
        "pragma client-version 22800\n" ++
        "login alice " ++ hex40 ++ " " ++ hex40 ++ "\n" ++
        "login bob " ++ hex40 ++ " " ++ hex40 ++ "\n";
    var ss = repostate.MemStore.init(gpa);
    defer ss.deinit();
    const server = Server{ .version = h2_server_version };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);

    const reply = try server.respond(gpa, ss.store(), request, &out);
    // §6.1 L1078-1079 — the server MUST reset its partial output and emit an
    // error card: the reply is the error card alone, no announcement cards.
    try std.testing.expect(std.mem.startsWith(u8, reply, "error "));
    try std.testing.expect(std.mem.indexOf(u8, reply, "gimme ") == null);
    try std.testing.expect(std.mem.indexOf(u8, reply, "igot ") == null);
}

test "sync §5.2: an unknown request operator is answered with an error bad command card" {
    const gpa = std.testing.allocator;
    // §5.2 L969-971 — an unknown card operator is fatal; the server resets its
    // output and emits `error bad command: LINE` (L1815-1817, Appendix C).
    const request = "pragma client-version 22800\nbogus\n";
    var ss = repostate.MemStore.init(gpa);
    defer ss.deinit();
    const server = Server{ .version = h2_server_version };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);

    const reply = try server.respond(gpa, ss.store(), request, &out);
    try std.testing.expectEqualStrings("error bad command: bogus\n", reply);
}

test "sync §5.2/§7.1: a malformed file card is answered with an error malformed file line card" {
    const gpa = std.testing.allocator;
    // §7.1 — a full-content file card is `file HASH SIZE`; a two-token card has
    // an arity failure the server reports as `error malformed file line`
    // (§5.2 L1013-1016) after resetting its output.
    const request = "pragma client-version 22800\nfile deadbeef\n";
    var ss = repostate.MemStore.init(gpa);
    defer ss.deinit();
    const server = Server{ .version = h2_server_version };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);

    const reply = try server.respond(gpa, ss.store(), request, &out);
    try std.testing.expectEqualStrings("error malformed file line\n", reply);
}

test "sync §3.7/§A.1: buildRequest/respond/consumeReply converge two endpoints to the union of their holdings" {
    const gpa = std.testing.allocator;
    // §A.1 L2451-2453/L2472-2473 — client holds X = {manifest_x, alpha}, server holds
    // Y = {manifest_y, beta}, both share the ancestor cloned earlier.
    var cs = repostate.MemStore.init(gpa);
    defer cs.deinit();
    _ = try cs.store().put(h2_ancestor, "shared ancestor manifest");
    _ = try cs.store().put(h2_manifest_x, "client checkin manifest");
    _ = try cs.store().put(h2_alpha, "alpha content from client\n");

    var ss = repostate.MemStore.init(gpa);
    defer ss.deinit();
    _ = try ss.store().put(h2_ancestor, "shared ancestor manifest");
    _ = try ss.store().put(h2_manifest_y, "server checkin manifest");
    _ = try ss.store().put(h2_beta, "beta content added server-side\n");

    var client = Client{
        .operation = .sync,
        .state = initialState(.sync),
        .self_servercode = h2_servercode,
        .project_code = h2_projectcode,
        .version = h2_server_version,
    };
    defer client.deinit(gpa);
    const server = Server{ .version = h2_server_version };

    var req_buf: std.ArrayListUnmanaged(u8) = .empty;
    defer req_buf.deinit(gpa);
    var reply_buf: std.ArrayListUnmanaged(u8) = .empty;
    defer reply_buf.deinit(gpa);

    // §3.7 L574-579 — drive rounds until the client's state machine terminates;
    // the worked exchange converges in two rounds, so a small bound suffices.
    var round: usize = 0;
    while (client.state == .exchange and round < 8) : (round += 1) {
        const request = try client.buildRequest(gpa, cs.store(), &req_buf);
        const reply = try server.respond(gpa, ss.store(), request, &reply_buf);
        _ = try client.consumeReply(gpa, cs.store(), reply);
    }
    try std.testing.expectEqual(ClientState.terminated, client.state);

    // §3.7 L574-579 — both endpoints now hold X ∪ Y, and neither retains a
    // content-null rid whose hash the peer holds (no unresolved phantom).
    const union_hashes = [_][]const u8{ h2_ancestor, h2_manifest_x, h2_alpha, h2_manifest_y, h2_beta };
    for (union_hashes) |h| {
        const crid = cs.store().lookup(h) orelse return error.TestUnexpectedResult;
        try std.testing.expect(cs.store().content(crid) != null);
        const srid = ss.store().lookup(h) orelse return error.TestUnexpectedResult;
        try std.testing.expect(ss.store().content(srid) != null);
    }
    try expectNoPhantom(cs.store());
    try expectNoPhantom(ss.store());
}

// §3.3 r24 — a converged endpoint retains no phantom (content-null) rid.
fn expectNoPhantom(store: repostate.ArtifactStore) !void {
    const n = store.count();
    var rid: repostate.Rid = 1;
    while (rid <= n) : (rid += 1) {
        try std.testing.expect(store.content(rid) != null);
    }
}

// ---------------------------------------------------------------------------
// §8 server-side clone — Server.respond behaviour (public surface). Expected
// values come from the sync RFC §8 text only, never from observing Fossil.
// ---------------------------------------------------------------------------

/// Walk a reply's cards, framing past a file/cfile payload (binary, LF-legal) so
/// no payload byte is miscounted as a card boundary. `payload` holds the last
/// content card's CSIZE/SIZE bytes. Built on `sync`'s public parsing API only.
const CloneReplyWalk = struct {
    body: []const u8,
    pos: usize = 0,
    payload: []const u8 = "",
    fn next(self: *CloneReplyWalk) ?sync.CardClass {
        while (self.pos < self.body.len) {
            const line = sync.readLine(self.body, self.pos) catch return null;
            self.pos = line.next;
            if (sync.isCommentLine(line.bytes)) continue;
            const cc = sync.classifyCard(line.bytes);
            if (cc.operator) |op| switch (op) {
                .cfile => {
                    const c = sync.parseCfileCard(cc.tokens) catch return cc;
                    const p = sync.consumeContentPayload(self.body, self.pos, c.compressed_size) catch return cc;
                    self.payload = p.bytes;
                    self.pos = sync.cfilePaddingEnd(self.body, p); // §5.1 — skip the padding LF like the receiver
                },
                .file => {
                    const c = sync.parseFileCard(cc.tokens) catch return cc;
                    const p = sync.consumeContentPayload(self.body, self.pos, c.size) catch return cc;
                    self.payload = p.bytes;
                    self.pos = p.next;
                },
                else => {},
            };
            return cc;
        }
        return null;
    }

    /// draft-fossil-submanifests-00 §7.2 — consume the leading
    /// `pragma avoid-submanifests` card every reply from a server whose §8.1
    /// setting is false carries, asserting it is there.
    ///
    /// A consuming ASSERTION rather than a silent skip: the card is now part of
    /// the sequence these tests walk, so each one should say so once and then
    /// go on describing the §8.2 clone sequence it exists to pin. Hiding the
    /// card inside `next` would make a regression that stopped emitting it
    /// invisible to every test here.
    fn expectAvoidSubmanifests(self: *CloneReplyWalk) !void {
        const cc = self.next() orelse return error.TestUnexpectedResult;
        try std.testing.expectEqual(sync.Operator.pragma, cc.operator.?);
        try std.testing.expectEqual(sync.PragmaName.@"avoid-submanifests", sync.parsePragmaName(cc.tokens.items[1]).?);
    }
};

fn cloneTestServer() sync.Server {
    return .{
        .version = .{ .version = 22800, .datetime = .{ .date = 20260715, .time = 0 } },
        .self_servercode = a1_servercode,
        .project_code = a1_projectcode,
    };
}

test "sync §8.2: respond answers clone 3 1 with RID-ordered cfiles, then clone_seqno 0 and push" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    // Dense RIDs 1,2 in insertion order (repostate contract).
    _ = try ms.store().put(a1_manifest_hash, "manifest content\n");
    _ = try ms.store().put(a1_send_hash, "alpha content from client\n");
    const server = cloneTestServer();
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);
    const reply = try server.respond(gpa, ms.store(), "pragma client-version 22700\nclone 3 1\n", &out);

    var walk = CloneReplyWalk{ .body = reply };
    try walk.expectAvoidSubmanifests(); // submanifests §7.2 — leads the reply
    // §8.2 L1517-1518 — cfile cards in RID order; each decodes to stored content.
    const c1 = walk.next().?;
    try std.testing.expectEqual(sync.Operator.cfile, c1.operator.?);
    try std.testing.expectEqualStrings(a1_manifest_hash, (try sync.parseCfileCard(c1.tokens)).hash);
    const d1 = try codec.decode(gpa, walk.payload, sync.reply_body_cap);
    defer gpa.free(d1);
    try std.testing.expectEqualStrings("manifest content\n", d1);
    const c2 = walk.next().?;
    try std.testing.expectEqual(sync.Operator.cfile, c2.operator.?);
    try std.testing.expectEqualStrings(a1_send_hash, (try sync.parseCfileCard(c2.tokens)).hash);
    const d2 = try codec.decode(gpa, walk.payload, sync.reply_body_cap);
    defer gpa.free(d2);
    try std.testing.expectEqualStrings("alpha content from client\n", d2);
    // §8.2 L1519-1520 — clone_seqno 0 (exhausted), then push SERVERCODE PROJECTCODE.
    const c3 = walk.next().?;
    try std.testing.expectEqual(sync.Operator.clone_seqno, c3.operator.?);
    try std.testing.expectEqual(@as(i64, 0), try sync.parseCloneSeqno(c3.tokens));
    const c4 = walk.next().?;
    try std.testing.expectEqual(sync.Operator.push, c4.operator.?);
    const pc = sync.parsePushPull(c4.tokens).?;
    try std.testing.expectEqualStrings(a1_servercode, pc.servercode);
    try std.testing.expectEqualStrings(a1_projectcode, pc.projectcode);
    // §8 — clone is receive-only: no pull/push igot/gimme walk follows (the card
    // sequence is exactly cfile,cfile,clone_seqno,push and then exhausted).
    try std.testing.expect(walk.next() == null);
}

test "sync §8.2 L1520: server-version precedes the clone sequence iff the client-version is dated" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    _ = try ms.store().put(a1_manifest_hash, "m\n");
    const server = cloneTestServer();
    // Dated client-version solicits server-version (§10.1 r11); §8.2 puts it first.
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);
    const dated = try server.respond(gpa, ms.store(), "pragma client-version 22700 20260101 120000\nclone 3 1\n", &out);
    try std.testing.expect(std.mem.startsWith(u8, dated, "pragma server-version " ++
        std.fmt.comptimePrint("{d}", .{sync.submanifest_min_peer_version}) ++ " "));
    // Undated client-version suppresses it (§10.1 r12): the reply then opens on
    // the submanifests §7.2 pragma, which sits in the same leading pragma run,
    // and the clone sequence follows it.
    var out2: std.ArrayListUnmanaged(u8) = .empty;
    defer out2.deinit(gpa);
    const undated = try server.respond(gpa, ms.store(), "pragma client-version 22700\nclone 3 1\n", &out2);
    try std.testing.expect(std.mem.indexOf(u8, undated, "pragma server-version") == null);
    try std.testing.expect(std.mem.startsWith(u8, undated, "pragma avoid-submanifests\ncfile "));
}

test "sync §8.2 L1504-1505: clone skips a content-absent RID (phantom), sending only held artifacts" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    _ = try ms.store().put(a1_manifest_hash, "held-1\n"); // rid 1: held
    _ = try ms.store().intern(a1_send_hash); // rid 2: phantom (content absent)
    _ = try ms.store().put(a1_held_hash, "held-3\n"); // rid 3: held
    const server = cloneTestServer();
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);
    const reply = try server.respond(gpa, ms.store(), "pragma client-version 22700\nclone 3 1\n", &out);
    var walk = CloneReplyWalk{ .body = reply };
    try walk.expectAvoidSubmanifests(); // submanifests §7.2 — leads the reply
    // Two cfiles for the held rids 1 and 3 — the phantom rid 2 is silently
    // skipped (§8.2 L1505 "skipping gaps") — then clone_seqno 0.
    try std.testing.expectEqualStrings(a1_manifest_hash, (try sync.parseCfileCard(walk.next().?.tokens)).hash);
    try std.testing.expectEqualStrings(a1_held_hash, (try sync.parseCfileCard(walk.next().?.tokens)).hash);
    const cs = walk.next().?;
    try std.testing.expectEqual(sync.Operator.clone_seqno, cs.operator.?);
    try std.testing.expectEqual(@as(i64, 0), try sync.parseCloneSeqno(cs.tokens));
    try std.testing.expectEqual(sync.Operator.push, walk.next().?.operator.?);
    try std.testing.expect(walk.next() == null);
}

test "sync §8.1: clone 3 0 is the fatal invalid-sequence error, resetting output (no cfile, no clone_seqno)" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    _ = try ms.store().put(a1_manifest_hash, "m\n");
    const server = cloneTestServer();
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);
    // §8.1 L1493-1498 — VERSION>=2, digit-only SEQNO 0: the accumulated output is
    // cleared and the response is exactly the fossilized error card.
    const reply = try server.respond(gpa, ms.store(), "pragma client-version 22700\nclone 3 0\n", &out);
    try std.testing.expectEqualStrings("error invalid\\sclone\\ssequence\\snumber\n", reply);
}

test "sync §3.8/§8.9: a clone response is a pure function of (store, request) — identical bytes on replay" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    _ = try ms.store().put(a1_manifest_hash, "manifest\n");
    _ = try ms.store().put(a1_send_hash, "alpha\n");
    const server = cloneTestServer();
    const req = "pragma client-version 22700\nclone 3 1\n";
    var out_a: std.ArrayListUnmanaged(u8) = .empty;
    defer out_a.deinit(gpa);
    var out_b: std.ArrayListUnmanaged(u8) = .empty;
    defer out_b.deinit(gpa);
    const a = try server.respond(gpa, ms.store(), req, &out_a);
    const b = try server.respond(gpa, ms.store(), req, &out_b);
    // §3.8 stateless server: the server wrote nothing across requests, so a byte-
    // identical (store, request) yields a byte-identical reply.
    try std.testing.expectEqualSlices(u8, a, b);
}

// §3.3 r24 / §8.5 — a cluster is the bulk form of igot: its M cards announce
// artifacts the peer holds. A fossil server folds everything older than its
// ~100-entry unclustered window into cluster artifacts and stops announcing
// those members individually, so a client that does not phantomize a received
// cluster's members has nothing left to gimme and §3.7(a) terminates the
// exchange after one round — a pull that silently fetches nothing.
const cluster_member_a = "0d0c8b4c67c02b90a6f0e0e6b4d4d0b7f96e9de0a0da1a2c2a5f1cba55a9f3f4";
const cluster_member_b = "9a3b0f1e5f6d7c8b9a0b1c2d3e4f50617283940a1b2c3d4e5f60718293a4b5c6";
const cluster_hash = "20376927fe72a07a8cd5e0f02530d85493742fba659ae26571825dad7f90217f";
const cluster_body = "M " ++ cluster_member_a ++ "\nM " ++ cluster_member_b ++ "\nZ 8a09b1c8ae2c5f4a2d5e9f0b7c1d3e4f\n";

test "sync §3.3 r24/§8.5: a pulled cluster phantomizes its members and the next round gimmes them" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .pull, .state = .exchange };
    defer client.deinit(gpa);

    const reply = try std.fmt.allocPrint(gpa, "file {s} {d}\n{s}", .{ cluster_hash, cluster_body.len, cluster_body });
    defer gpa.free(reply);

    const obs = try client.consumeReply(gpa, ms.store(), reply);

    // The members are unheld, so each M card must mint a phantom — the same
    // disposition an `igot` for that hash would produce (§3.3 r25).
    try std.testing.expect(ms.store().lookup(cluster_member_a) != null);
    try std.testing.expect(ms.store().lookup(cluster_member_b) != null);
    try std.testing.expectEqual(@as(usize, 2), client.phantoms.count());
    try std.testing.expect(obs.phantom_created);
    try std.testing.expect(obs.phantom_table_nonempty);

    // §3.7(a) — with a non-empty phantom table after versioned progress the
    // exchange must continue; terminating here is the silent no-op pull.
    try std.testing.expect(continueExchange(obs));
    try std.testing.expectEqual(ClientState.exchange, client.state);

    // §3.2 L397-399 — and the follow-up request must actually ask for them.
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);
    const req = try client.buildRequest(gpa, ms.store(), &out);
    try expectHasLine(req, "gimme " ++ cluster_member_a);
    try expectHasLine(req, "gimme " ++ cluster_member_b);
}

test "sync §3.3 r24: a terminated pull that still holds phantoms reports them unresolved" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .pull, .state = .exchange };
    defer client.deinit(gpa);

    // A peer announcement this side never resolved: the phantom outlives the
    // round, and the count is what lets a caller refuse to report success.
    _ = try client.consumeReply(gpa, ms.store(), "igot " ++ a1_send_hash ++ "\n");
    try std.testing.expectEqual(@as(usize, 1), client.unresolvedPhantoms(ms.store()));

    // Once the content lands the same phantom is resolved, so the count is a
    // live "what the peer announced and this side still lacks", not a tally of
    // every phantom ever minted.
    _ = try ms.store().put(a1_send_hash, "alpha\n");
    try std.testing.expectEqual(@as(usize, 0), client.unresolvedPhantoms(ms.store()));
}

// ---------------------------------------------------------------------------
// Section 3.3 L527-530 — the phantom-scan shun filter. "The ordinary phantom
// scan excludes peer-lacks hashes, names in the repository shun table, and
// private hashes unless private sync is enabled; shun-sha1 does not itself
// suppress an otherwise eligible gimme." The filter sits at the SCAN, not at
// phantom CREATION (§3.3 r24(1) L511-514 — "An igot HASH creates a phantom only
// when HASH is absent and the receiver can obtain it" — is unconditional on the
// shun table), so a shunned announcement still mints an inert phantom that is
// never requested. §8.8 L1777-1779 restates the same rule imperatively, and
// L516-521 makes the filter inputs a per-round question.
//
// ERRATUM CANDIDATE (sync §3.3 L527-530): the draft keys the table by "complete
// artifact name" (§3.3 L518-519; artifact-format §3.2 L347-350 fixes the two
// complete forms) but is silent on what an ABBREVIATED wire name means at the
// scan. The reading implemented here is exact-key-miss: a non-complete name
// cannot be a key, so `false` is the exact answer, never a prefix match. The
// case is unreachable in practice — `parseIgotCard`/`parseGimmeCard` reject any
// other width as malformed, so no abbreviated name ever becomes a phantom — and
// the erratum only fixes what the predicate answers if one ever did.
// ---------------------------------------------------------------------------

/// A test shun table over a fixed list of complete wire names, plus a round
/// counter so a test can make the answer change between rounds. Exact string
/// equality only — never a prefix compare.
const FixtureShunSet = struct {
    names: []const []const u8,
    /// When true, `contains` answers false until `armed` is set — the instrument
    /// for "the scan re-probes the set every round".
    gated: bool = false,
    armed: bool = false,
    probes: usize = 0,

    fn shunSet(self: *FixtureShunSet) sync.ShunSet {
        return .{ .context = self, .vtable = &vtable };
    }

    const vtable: sync.ShunSet.VTable = .{ .contains = containsName };

    fn containsName(context: *anyopaque, name: []const u8) bool {
        const self: *FixtureShunSet = @ptrCast(@alignCast(context));
        self.probes += 1;
        if (self.gated and !self.armed) return false;
        for (self.names) |n| {
            if (std.mem.eql(u8, n, name)) return true;
        }
        return false;
    }
};

/// Does `request` carry `gimme HASH` as a whole card line?
fn hasGimmeCard(request: []const u8, hash: []const u8) bool {
    var it = std.mem.splitScalar(u8, request, '\n');
    while (it.next()) |line| {
        if (line.len == 6 + hash.len and
            std.mem.startsWith(u8, line, "gimme ") and
            std.mem.eql(u8, line[6..], hash)) return true;
    }
    return false;
}

test "sync §3.3 L527-530: ShunSet.none is the identity — every eligible phantom is still gimme'd" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .sync, .state = .exchange };
    defer client.deinit(gpa);
    // A default-constructed Client carries `.none`: the regression lock that
    // every pre-existing sync test keeps its behavior verbatim.
    try std.testing.expect(!client.shun.contains(a1_phantom_1));

    _ = try client.consumeReply(gpa, ms.store(), a1_round1_reply);
    var buf: std.ArrayListUnmanaged(u8) = .empty;
    defer buf.deinit(gpa);
    const request = try client.buildRequest(gpa, ms.store(), &buf);
    try std.testing.expect(hasGimmeCard(request, a1_phantom_1));
    try std.testing.expect(hasGimmeCard(request, a1_phantom_2));
}

test "sync §3.3 L527-530: a phantom whose name is in the shun table is never gimme'd, in any round" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var shun = FixtureShunSet{ .names = &.{a1_phantom_1} };
    var client = Client{ .operation = .sync, .state = .exchange, .shun = shun.shunSet() };
    defer client.deinit(gpa);

    _ = try client.consumeReply(gpa, ms.store(), a1_round1_reply);
    var buf: std.ArrayListUnmanaged(u8) = .empty;
    defer buf.deinit(gpa);
    // Across every round the phantom survives: the shunned name is suppressed
    // and the unshunned sibling is not. Nothing about the filter is one-shot.
    var round: usize = 0;
    while (round < 3) : (round += 1) {
        const request = try client.buildRequest(gpa, ms.store(), &buf);
        try std.testing.expect(!hasGimmeCard(request, a1_phantom_1));
        try std.testing.expect(hasGimmeCard(request, a1_phantom_2));
    }
}

test "sync §3.3 r24(1) L511-514 vs L527-530: an igot for a shunned hash still CREATES the phantom" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var shun = FixtureShunSet{ .names = &.{ a1_phantom_1, a1_phantom_2 } };
    var client = Client{ .operation = .sync, .state = .exchange, .shun = shun.shunSet() };
    defer client.deinit(gpa);

    const obs = try client.consumeReply(gpa, ms.store(), a1_round1_reply);
    // Creation is untouched: the round-1 igot cards mint both phantoms even
    // though both names are shunned. The phantom is inert and permanent — the
    // draft filters the SCAN, and inventing a creation-side rule it does not
    // have would also lose the §3.7(a) continuation signal.
    try std.testing.expect(obs.phantom_created);
    try std.testing.expectEqual(@as(usize, 2), client.phantoms.count());
    const rid1 = try ms.store().intern(a1_phantom_1);
    const rid2 = try ms.store().intern(a1_phantom_2);
    try std.testing.expect(client.phantoms.contains(rid1));
    try std.testing.expect(client.phantoms.contains(rid2));
    // …and it is never requested.
    var buf: std.ArrayListUnmanaged(u8) = .empty;
    defer buf.deinit(gpa);
    const request = try client.buildRequest(gpa, ms.store(), &buf);
    try std.testing.expect(!hasGimmeCard(request, a1_phantom_1));
    try std.testing.expect(!hasGimmeCard(request, a1_phantom_2));
}

test "sync §3.3 L527-530: the scan re-probes the shun set every round, so a shun applied between rounds is honoured by the next" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var shun = FixtureShunSet{ .names = &.{a1_phantom_1}, .gated = true };
    var client = Client{ .operation = .sync, .state = .exchange, .shun = shun.shunSet() };
    defer client.deinit(gpa);

    _ = try client.consumeReply(gpa, ms.store(), a1_round1_reply);
    var buf: std.ArrayListUnmanaged(u8) = .empty;
    defer buf.deinit(gpa);

    // Round 1: the set is not yet armed, so the phantom is requested.
    const round1 = try client.buildRequest(gpa, ms.store(), &buf);
    try std.testing.expect(hasGimmeCard(round1, a1_phantom_1));
    const probes_after_round1 = shun.probes;
    try std.testing.expect(probes_after_round1 > 0);

    // The operator shuns it BETWEEN rounds.
    shun.armed = true;

    // Round 2 honours it: `buildRequest` asks the set again rather than reusing
    // a cached answer from construction time.
    const round2 = try client.buildRequest(gpa, ms.store(), &buf);
    try std.testing.expect(!hasGimmeCard(round2, a1_phantom_1));
    try std.testing.expect(shun.probes > probes_after_round1);
}

test "sync §3.3 L527-530: the scan asks with the complete wire name — a prefix is a different question" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    // The table holds a 12-hex PREFIX of the phantom's name. Exact-key lookup
    // means that is not this phantom's key, so the gimme still goes out: the
    // protocol layer never truncates, pads, or prefix-matches the name it asks
    // about (see the erratum candidate above).
    var shun = FixtureShunSet{ .names = &.{a1_phantom_1[0..12]} };
    var client = Client{ .operation = .sync, .state = .exchange, .shun = shun.shunSet() };
    defer client.deinit(gpa);

    _ = try client.consumeReply(gpa, ms.store(), a1_round1_reply);
    var buf: std.ArrayListUnmanaged(u8) = .empty;
    defer buf.deinit(gpa);
    const request = try client.buildRequest(gpa, ms.store(), &buf);
    try std.testing.expect(hasGimmeCard(request, a1_phantom_1));
}

test "sync §3.3 L527-530: an unresolved-phantom tally excludes a phantom the shun table suppressed" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var shun = FixtureShunSet{ .names = &.{a1_phantom_1} };
    var client = Client{ .operation = .pull, .state = .exchange, .shun = shun.shunSet() };
    defer client.deinit(gpa);

    _ = try client.consumeReply(gpa, ms.store(), a1_round1_reply);
    // Both phantoms exist and neither is held, but only the UNSHUNNED one is an
    // unmet expectation: the shunned name was never requested, by the operator's
    // own instruction. Counting it would make `zit pull` report "the peer holds N
    // artifacts this pull did not obtain" on every pull forever — the peer
    // re-announces the hash each round — for obeying the shun row.
    try std.testing.expectEqual(@as(usize, 2), client.phantoms.count());
    try std.testing.expectEqual(@as(usize, 1), client.unresolvedPhantoms(ms.store()));

    // With no shun table both count: the exclusion is the shun filter's, not a
    // weakening of the tally.
    var bare = Client{ .operation = .pull, .state = .exchange };
    defer bare.deinit(gpa);
    var bare_ms = repostate.MemStore.init(gpa);
    defer bare_ms.deinit();
    _ = try bare.consumeReply(gpa, bare_ms.store(), a1_round1_reply);
    try std.testing.expectEqual(@as(usize, 2), bare.unresolvedPhantoms(bare_ms.store()));
}

test "sync §3.3 L527-530 / §8.8 L1777-1779: a server reply never gimme's a shunned phantom it interned from a push igot" {
    const gpa = std.testing.allocator;
    // §3.3 r24(1) L511-514 — a server under push-receive is a receiver, so the
    // client's igot for content it lacks interns a phantom the reply would
    // otherwise beg for. Left unfiltered, the shunning side ACQUIRES the shunned
    // artifact through its own serve direction: the peer answers the gimme with a
    // file card and `respond` stores it.
    const request = "push " ++ h2_servercode ++ " " ++ h2_projectcode ++ "\nigot " ++ a1_phantom_1 ++ "\n";

    // Unshunned: the reply asks for it (the behavior the filter must change).
    {
        var ss = repostate.MemStore.init(gpa);
        defer ss.deinit();
        const server = Server{ .version = h2_server_version };
        var out: std.ArrayListUnmanaged(u8) = .empty;
        defer out.deinit(gpa);
        const reply = try server.respond(gpa, ss.store(), request, &out);
        try std.testing.expect(hasGimmeCard(reply, a1_phantom_1));
    }

    // Shunned: the phantom is still interned (§3.3 r24(1) is unconditional on the
    // shun table) and the reply is silent about it.
    {
        var ss = repostate.MemStore.init(gpa);
        defer ss.deinit();
        var shun = FixtureShunSet{ .names = &.{a1_phantom_1} };
        const server = Server{ .version = h2_server_version, .shun = shun.shunSet() };
        var out: std.ArrayListUnmanaged(u8) = .empty;
        defer out.deinit(gpa);
        const reply = try server.respond(gpa, ss.store(), request, &out);
        try std.testing.expect(!hasGimmeCard(reply, a1_phantom_1));
        const rid = ss.store().lookup(a1_phantom_1) orelse return error.TestUnexpectedResult;
        try std.testing.expect(!ss.store().held(rid)); // the phantom exists, unheld
    }
}

test "sync §9.2 / artifact-format §3.2 L347-350: an igot naming a non-complete hash is malformed, so no abbreviated name can become a phantom" {
    // The fail-CLOSED half of the shun filter's key domain. §3.3 L518-519 keys
    // the shun table by complete artifact name; if an abbreviated igot could mint
    // a phantom, that phantom's name could not be a key, the filter would answer
    // "not shunned", and the scan would request it — the fail-open case
    // `logstore.LogStore.isShunned`'s caller contract forbids by name. The repair
    // is at the parse layer, where `gimme` already lives: "A candidate name of
    // any other length MUST be rejected as invalid" (artifact-format §3.2 L349-350).
    const short = a1_phantom_1[0..60]; // even length, decodes, still not a form
    inline for (.{ a1_phantom_1[0..12], a1_phantom_1[0..39], a1_phantom_1[0..41], a1_phantom_1[0..63] }) |bad| {
        try std.testing.expectError(CardParseError.Malformed, parseIgotCard(tokenizeCard("igot " ++ bad)));
    }
    try std.testing.expectError(CardParseError.Malformed, parseIgotCard(tokenizeCard("igot " ++ short ++ " 1")));
    // Both complete forms still parse.
    _ = try parseIgotCard(tokenizeCard("igot " ++ a1_phantom_1)); // 64-hex
    _ = try parseIgotCard(tokenizeCard("igot " ++ a1_phantom_1[0..40])); // 40-hex

    // …and the client honours it end to end: a reply whose igot carries an
    // abbreviated name mints no phantom, so the scan has nothing out of domain to
    // ask the shun set about.
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .pull, .state = .exchange };
    defer client.deinit(gpa);
    const reply = "igot " ++ a1_phantom_1[0..60] ++ "\n";
    try std.testing.expectError(error.FatalProtocolError, client.consumeReply(gpa, ms.store(), reply));
    try std.testing.expectEqual(@as(usize, 0), client.phantoms.count());
}

// ---------------------------------------------------------------------------
// Section 7.7 L1434-1463 — receive-side target admission. "Admission applies to
// the received target HASH, not independently to DELTASRC. A target is shunned
// for admission when its complete name is in the repository shun table or when
// shun-sha1 applies to its SHA1 name. A non-clone file whose target is shunned
// is consumed and discarded after its payload and pending private marker; it
// emits no error, does not set content-received, and makes no repository
// mutation. A clone file bypasses this combined target check. For cfile, the
// private-admission behavior of Section 7.5 runs first; if processing
// continues, a shunned target is consumed and discarded after its payload even
// in clone, and its raw parser artifact-card count nevertheless increments
// after handler invocation."
//
// The asymmetry between `file` in clone and `cfile` in clone is DELIBERATE and
// is what L1440-1442 says; it is the pair of tests below that pins it.
// ---------------------------------------------------------------------------

/// A 64-hex (SHA3) target name the fixture shun table names, and two siblings.
const s77_target = "d" ** 64;
const s77_other = "e" ** 64;

/// 22 bytes — the `file` SIZE the tests below frame with.
const s77_payload = "shunned artifact bytes";

test "sync §7.7 L1437-1440: a non-clone file card whose target is shunned is consumed and discarded, mutating nothing" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var shun = FixtureShunSet{ .names = &.{s77_target} };
    var client = Client{ .operation = .pull, .state = .exchange, .shun = shun.shunSet() };
    defer client.deinit(gpa);

    // The trailing igot is the instrument for "consumed EXACTLY SIZE bytes":
    // it is only reached if the cursor resumed at the byte after the payload.
    const body = "file " ++ s77_target ++ " 22\n" ++ s77_payload ++ "igot " ++ s77_other ++ "\n";
    const obs = try client.consumeReply(gpa, ms.store(), body);

    // "makes no repository mutation" — not even the intern a `put` would do.
    try std.testing.expect(ms.store().lookup(s77_target) == null);
    // "does not set content-received".
    try std.testing.expect(!obs.versioned_content_delivered);
    try std.testing.expect(!obs.artifact_received_this_round);
    // "it emits no error" — at this receiver an error is `abort()`, i.e. the
    // FatalProtocolError the `try` above would have surfaced. Processing
    // continued instead, and the next card landed.
    try std.testing.expect(ms.store().lookup(s77_other) != null);
}

test "sync §7.7 L1440: a CLONE file card bypasses the combined target check — the shunned target is admitted and stored" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var shun = FixtureShunSet{ .names = &.{s77_target} };
    var client = Client{ .operation = .clone, .state = .clone_round0, .shun = shun.shunSet() };
    defer client.deinit(gpa);

    const body = "clone_seqno 0\nfile " ++ s77_target ++ " 22\n" ++ s77_payload;
    const obs = try client.consumeReply(gpa, ms.store(), body);

    const rid = ms.store().lookup(s77_target) orelse return error.TestUnexpectedResult;
    try std.testing.expect(ms.store().held(rid));
    try std.testing.expectEqualStrings(s77_payload, ms.store().content(rid).?);
    try std.testing.expect(obs.versioned_content_delivered);
}

test "sync §7.7 L1440-1444: a cfile whose target is shunned is discarded EVEN IN CLONE, and the artifact-card count still increments" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    const plain = "the compressed-form artifact this repository has shunned";
    const container = try zlibContainerEncode(gpa, plain, .default);
    defer gpa.free(container);

    var body: std.ArrayList(u8) = .empty;
    defer body.deinit(gpa);
    try body.appendSlice(gpa, "clone_seqno 0\n");
    const head = try std.fmt.allocPrint(gpa, "cfile {s} {d} {d}\n", .{ s77_target, plain.len, container.len });
    defer gpa.free(head);
    try body.appendSlice(gpa, head);
    try body.appendSlice(gpa, container);
    try body.append(gpa, '\n'); // §5.1 optional trailing-LF padding

    var shun = FixtureShunSet{ .names = &.{s77_target} };
    var client = Client{ .operation = .clone, .state = .clone_round0, .shun = shun.shunSet() };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body.items);

    try std.testing.expect(ms.store().lookup(s77_target) == null);
    try std.testing.expect(!obs.versioned_content_delivered);
    try std.testing.expect(!obs.artifact_received_this_round);
    // L1443-1444 — "its raw parser artifact-card count nevertheless increments
    // after handler invocation": the §3.7(e) clone-continuation counter is a RAW
    // handler count, so a discarded cfile still advances the clone.
    try std.testing.expect(obs.artifact_count_grew);
}

test "sync §7.7 L1440-1442: for cfile the §7.5 private-admission behavior runs FIRST" {
    // The two arms would both discard, so the discriminator is WHICH ONE did:
    // the §7.5 arm records `private_content_discarded`, the §7.7 arm records
    // nothing. Run the shun check first and that flag goes false.
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    const plain = "a private artifact whose name is also in the shun table";
    const container = try zlibContainerEncode(gpa, plain, .default);
    defer gpa.free(container);

    var body: std.ArrayList(u8) = .empty;
    defer body.deinit(gpa);
    try body.appendSlice(gpa, "private\n");
    const head = try std.fmt.allocPrint(gpa, "cfile {s} {d} {d}\n", .{ s77_target, plain.len, container.len });
    defer gpa.free(head);
    try body.appendSlice(gpa, head);
    try body.appendSlice(gpa, container);
    try body.append(gpa, '\n');

    var shun = FixtureShunSet{ .names = &.{s77_target} };
    // private_granted false — §7.5 L1362-1367 discards for lack of the grant.
    // (The `L2383-2385` spelling on the sibling lines in this file indexes an
    // older draft rendering and is part of the deferred citation sweep, which
    // covers `sync_test.zig` as well as `sync.zig`.)
    var client = Client{ .operation = .sync, .state = .exchange, .shun = shun.shunSet() };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body.items);

    try std.testing.expect(obs.private_content_discarded);
    try std.testing.expect(ms.store().lookup(s77_target) == null);
}

test "sync §7.7 L1444-1450: an admitted target whose DELTASRC is shunned reconstructs from a held basis" {
    // "the receiver does not apply admission independently to that source:
    // content still held locally MAY serve as the delta basis". zit lands this
    // with NO carve-out code: admission is simply never asked about DELTASRC.
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();

    const basis_plain = "the basis artifact bytes, long enough that a forward delta over them is compact";
    const target_plain = basis_plain ++ " plus a small appended mutation";
    var nb: [64]u8 = undefined;
    const basis_name = artifact.artifactName(.sha3_256, basis_plain, &nb);
    var nt: [64]u8 = undefined;
    const target_name = artifact.artifactName(.sha3_256, target_plain, &nt);

    _ = try ms.store().put(basis_name, basis_plain);
    const raw = try delta.create(gpa, basis_plain, target_plain);
    defer gpa.free(raw);

    var body: std.ArrayList(u8) = .empty;
    defer body.deinit(gpa);
    const head = try std.fmt.allocPrint(gpa, "file {s} {s} {d}\n", .{ target_name, basis_name, raw.len });
    defer gpa.free(head);
    try body.appendSlice(gpa, head);
    try body.appendSlice(gpa, raw);

    var shun = FixtureShunSet{ .names = &.{basis_name} };
    var client = Client{ .operation = .pull, .state = .exchange, .shun = shun.shunSet() };
    defer client.deinit(gpa);
    _ = try client.consumeReply(gpa, ms.store(), body.items);

    const rid = ms.store().lookup(target_name) orelse return error.TestUnexpectedResult;
    try std.testing.expectEqualStrings(target_plain, ms.store().content(rid).?);
}

test "sync §7.7 L1447-1450: with the shunned DELTASRC unavailable, ordinary dangling-delta handling retains the source phantom and §3.3 never gimme's it" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();

    const basis_plain = "the basis artifact bytes, long enough that a forward delta over them is compact";
    const target_plain = basis_plain ++ " plus a small appended mutation";
    var nb: [64]u8 = undefined;
    const basis_name = artifact.artifactName(.sha3_256, basis_plain, &nb);
    var nt: [64]u8 = undefined;
    const target_name = artifact.artifactName(.sha3_256, target_plain, &nt);

    const raw = try delta.create(gpa, basis_plain, target_plain);
    defer gpa.free(raw);

    var body: std.ArrayList(u8) = .empty;
    defer body.deinit(gpa);
    const head = try std.fmt.allocPrint(gpa, "file {s} {s} {d}\n", .{ target_name, basis_name, raw.len });
    defer gpa.free(head);
    try body.appendSlice(gpa, head);
    try body.appendSlice(gpa, raw);

    var shun = FixtureShunSet{ .names = &.{basis_name} };
    var client = Client{ .operation = .pull, .state = .exchange, .shun = shun.shunSet() };
    defer client.deinit(gpa);
    const obs = try client.consumeReply(gpa, ms.store(), body.items);

    // "ordinary dangling-delta handling creates or retains the source phantom"
    try std.testing.expect(obs.phantom_created);
    const src_rid = ms.store().lookup(basis_name) orelse return error.TestUnexpectedResult;
    try std.testing.expect(!ms.store().held(src_rid));
    // "but the Section 3.3 scan excludes it, so it is not requested."
    var buf: std.ArrayListUnmanaged(u8) = .empty;
    defer buf.deinit(gpa);
    const request = try client.buildRequest(gpa, ms.store(), &buf);
    try std.testing.expect(!hasGimmeCard(request, basis_name));
}

test "sync §7.7 L1434-1437: shun-sha1 is the OTHER half of one combined predicate, not a second check" {
    // "A target is shunned for admission when its complete name is in the
    // repository shun table OR when shun-sha1 applies to its SHA1 name." An
    // empty shun table plus shun-sha1 still discards a 40-hex target.
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var client = Client{ .operation = .pull, .state = .exchange, .hash_policy = .shun_sha1 };
    defer client.deinit(gpa);

    const body = "file " ++ test_hash_a ++ " 22\n" ++ s77_payload ++ "igot " ++ s77_other ++ "\n";
    const obs = try client.consumeReply(gpa, ms.store(), body);
    try std.testing.expect(ms.store().lookup(test_hash_a) == null);
    try std.testing.expect(!obs.versioned_content_delivered);
    try std.testing.expect(ms.store().lookup(s77_other) != null);

    // A 64-hex target under the same policy is admitted: the SHA1 half of the
    // predicate is keyed on the NAME's algorithm, never on the card.
    var ms2 = repostate.MemStore.init(gpa);
    defer ms2.deinit();
    var c2 = Client{ .operation = .pull, .state = .exchange, .hash_policy = .shun_sha1 };
    defer c2.deinit(gpa);
    _ = try c2.consumeReply(gpa, ms2.store(), "file " ++ s77_target ++ " 22\n" ++ s77_payload);
    try std.testing.expect(ms2.store().lookup(s77_target) != null);
}

test "sync §7.7 L1437-1440: a server push-receive discards a shunned pushed file — no store mutation and no error card" {
    const gpa = std.testing.allocator;
    const request = "push " ++ h2_servercode ++ " " ++ h2_projectcode ++ "\n" ++
        "file " ++ s77_target ++ " 22\n" ++ s77_payload;

    // Unshunned: the server stores it (the behavior the admission check changes).
    {
        var ss = repostate.MemStore.init(gpa);
        defer ss.deinit();
        const server = Server{ .version = h2_server_version };
        var out: std.ArrayListUnmanaged(u8) = .empty;
        defer out.deinit(gpa);
        _ = try server.respond(gpa, ss.store(), request, &out);
        const rid = ss.store().lookup(s77_target) orelse return error.TestUnexpectedResult;
        try std.testing.expect(ss.store().held(rid));
    }

    // Shunned: server push-receive is always non-clone, so the discard arm runs.
    {
        var ss = repostate.MemStore.init(gpa);
        defer ss.deinit();
        var shun = FixtureShunSet{ .names = &.{s77_target} };
        const server = Server{ .version = h2_server_version, .shun = shun.shunSet() };
        var out: std.ArrayListUnmanaged(u8) = .empty;
        defer out.deinit(gpa);
        const reply = try server.respond(gpa, ss.store(), request, &out);
        try std.testing.expect(ss.store().lookup(s77_target) == null);
        // "it emits no error" — literally, over the wire this time.
        try std.testing.expect(std.mem.indexOf(u8, reply, "error ") == null);
    }
}

test "sync §7.7 L1437-1440: a server push-receive of a shunned target in DELTA form emits no error either — the gate is above the reconstruction, not on the store" {
    // The full form never reaches a fault, so gating only the pass-2 `put` looks
    // correct until the target arrives as `file HASH DELTASRC SIZE`. Then the
    // scan reconstructs FIRST, and a basis this repository does not hold turns a
    // discard §7.7 requires to be silent into `error malformed file line`. The
    // absent basis is the COMMON case for a shunned target. L1437-1440 —
    // "consumed and discarded after its payload ...; it emits no error" — draws
    // no distinction between the two forms of the card.
    const gpa = std.testing.allocator;
    // Deliberately NOT a well-formed delta and against a basis nothing holds:
    // both would fault today, and neither may be observable through the reply.
    const request = "push " ++ h2_servercode ++ " " ++ h2_projectcode ++ "\n" ++
        "file " ++ s77_target ++ " " ++ s77_other ++ " 4\n" ++ "junk";

    var ss = repostate.MemStore.init(gpa);
    defer ss.deinit();
    var shun = FixtureShunSet{ .names = &.{s77_target} };
    const server = Server{ .version = h2_server_version, .shun = shun.shunSet() };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);
    const reply = try server.respond(gpa, ss.store(), request, &out);

    try std.testing.expect(std.mem.indexOf(u8, reply, "error ") == null);
    // "makes no repository mutation" — not even the DELTASRC phantom: admission
    // is decided before DELTASRC is looked at (L1434-1435 is a TARGET question).
    try std.testing.expect(ss.store().lookup(s77_target) == null);
    try std.testing.expect(ss.store().lookup(s77_other) == null);
    try std.testing.expectEqual(@as(repostate.Rid, 0), ss.store().count());
}

test "sync §7.7 L1444-1450: on the SERVER, an admitted target whose shunned DELTASRC is unresolvable is dangling-handled — source phantom kept, never gimme'd, no error" {
    // "For an admitted target whose DELTASRC name is in the repository shun
    // table, the receiver does not apply admission independently to that source:
    // content still held locally MAY serve as the delta basis; if the content is
    // unavailable, ordinary dangling-delta handling creates or retains the source
    // phantom, but the Section 3.3 scan excludes it, so it is not requested."
    //
    // The basis is unavailable, so the second branch is the one that runs. (In
    // the CLI's served store a shunned name is additionally masked out of the
    // by-name index, which is how this arrives in production; the fixture reaches
    // the same branch by simply not holding it.) Two things must hold together:
    // no error card (the whole round would otherwise be replaced by one), and the
    // phantom exists yet is excluded from the reply's §3.3 L527-530 gimme scan.
    const gpa = std.testing.allocator;
    const basis = "a shunned basis the served store will not resolve by name";
    const target = "a shunned basis the served store will not resolve by name, plus a tail";
    const d = try delta.create(gpa, basis, target);
    defer gpa.free(d);
    var tbuf: [64]u8 = undefined;
    const target_hash = artifact.artifactName(.sha3_256, target, &tbuf);
    var bbuf: [64]u8 = undefined;
    const basis_hash = artifact.artifactName(.sha3_256, basis, &bbuf);

    var ss = repostate.MemStore.init(gpa);
    defer ss.deinit();

    var shun = FixtureShunSet{ .names = &.{basis_hash} };
    const server = Server{ .version = h2_server_version, .shun = shun.shunSet() };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);

    const request = try std.fmt.allocPrint(gpa, "push {s} {s}\nfile {s} {s} {d}\n{s}", .{ h2_servercode, h2_projectcode, target_hash, basis_hash, d.len, d });
    defer gpa.free(request);
    const reply = try server.respond(gpa, ss.store(), request, &out);

    // Not an error round.
    try std.testing.expect(std.mem.indexOf(u8, reply, "error ") == null);
    // The target is ADMITTED but unresolvable, so it does not land.
    try std.testing.expect(ss.store().lookup(target_hash) == null);
    // The SOURCE PHANTOM was created even though the source is shunned —
    // admission is not applied independently to DELTASRC.
    const src_rid = ss.store().lookup(basis_hash) orelse return error.TestUnexpectedResult;
    try std.testing.expect(!ss.store().held(src_rid));
    // §3.3 L527-530 — but the reply never begs for it.
    try std.testing.expect(std.mem.indexOf(u8, reply, basis_hash) == null);
}

test "sync §7.7 L1444-1447 scope: the non-error dangling path is for a SHUNNED source only — an ordinary absent basis keeps the §7.4/§3.8 fault" {
    // L1444-1447 opens "For an admitted target whose DELTASRC name is in the
    // repository shun table". The two runs below differ in EXACTLY that: same
    // request bytes, same (empty) store, only the shun table changes. A receiver
    // that widened the carve-out to every absent basis would answer both the same
    // way, and the §7.4/§3.8 fault for an unknown, truncated, or attacker-chosen
    // DELTASRC would be gone.
    const gpa = std.testing.allocator;
    const basis = "the basis neither run holds, so both take the BasisAbsent arm";
    const target = "the basis neither run holds, so both take the BasisAbsent arm, plus tail";
    const d = try delta.create(gpa, basis, target);
    defer gpa.free(d);
    var tbuf: [64]u8 = undefined;
    const target_hash = artifact.artifactName(.sha3_256, target, &tbuf);
    var bbuf: [64]u8 = undefined;
    const basis_hash = artifact.artifactName(.sha3_256, basis, &bbuf);

    const request = try std.fmt.allocPrint(gpa, "push {s} {s}\nfile {s} {s} {d}\n{s}", .{ h2_servercode, h2_projectcode, target_hash, basis_hash, d.len, d });
    defer gpa.free(request);

    // (1) DELTASRC not in the shun table: the fault stands.
    {
        var ss = repostate.MemStore.init(gpa);
        defer ss.deinit();
        const server = Server{ .version = h2_server_version }; // ShunSet.none
        var out: std.ArrayListUnmanaged(u8) = .empty;
        defer out.deinit(gpa);
        const reply = try server.respond(gpa, ss.store(), request, &out);
        try std.testing.expect(std.mem.startsWith(u8, reply, "error malformed file"));
        try std.testing.expectEqual(@as(usize, 0), ss.store().count()); // not even a phantom
    }

    // (2) The SAME bytes with DELTASRC in the shun table: L1447-1450's ordinary
    // dangling-delta handling, no error card, source phantom retained.
    {
        var ss = repostate.MemStore.init(gpa);
        defer ss.deinit();
        var shun = FixtureShunSet{ .names = &.{basis_hash} };
        const server = Server{ .version = h2_server_version, .shun = shun.shunSet() };
        var out: std.ArrayListUnmanaged(u8) = .empty;
        defer out.deinit(gpa);
        const reply = try server.respond(gpa, ss.store(), request, &out);
        try std.testing.expect(std.mem.indexOf(u8, reply, "error ") == null);
        const src_rid = ss.store().lookup(basis_hash) orelse return error.TestUnexpectedResult;
        try std.testing.expect(!ss.store().held(src_rid));
    }
}

test "sync §7.7 L1437-1440 / §3.3 L518-525: a shun applied BETWEEN rounds stops a deferred delta's target from landing on completion" {
    // "A non-clone file whose target is shunned ... makes no repository
    // mutation." The card arms alone cannot carry that clause for a DELTA: a
    // dangling delta is retained on one round and its target is STORED on a later
    // one, out of the completion path, where no card arm runs. §3.3 L518-525
    // makes the shun table durable and locally mutable, and the scan re-probes it
    // every round, so the completion must re-ask too.
    const gpa = std.testing.allocator;
    const basis = "the deferred delta's basis, delivered on the round after the shun";
    const target = "the deferred delta's basis, delivered on the round after the shun, plus tail";
    const d = try delta.create(gpa, basis, target);
    defer gpa.free(d);
    var tbuf: [64]u8 = undefined;
    const target_hash = artifact.artifactName(.sha3_256, target, &tbuf);
    var bbuf: [64]u8 = undefined;
    const basis_hash = artifact.artifactName(.sha3_256, basis, &bbuf);

    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    // Gated: the target is not yet shunned when the delta is retained.
    var shun = FixtureShunSet{ .names = &.{target_hash}, .gated = true };
    var client = Client{ .operation = .pull, .state = .exchange, .shun = shun.shunSet() };
    defer client.deinit(gpa);

    // Round 1 — the delta arrives dangling and is retained (§7.4/§3.3).
    const d_body = try std.fmt.allocPrint(gpa, "file {s} {s} {d}\n{s}", .{ target_hash, basis_hash, d.len, d });
    defer gpa.free(d_body);
    _ = try client.consumeReply(gpa, ms.store(), d_body);
    try std.testing.expect(client.pending.hasTarget(target_hash)); // admitted at receipt

    // The operator shuns the TARGET between rounds.
    shun.armed = true;

    // Round 2 — the basis arrives. It lands (it is not shunned), but the target
    // it would complete does not: §7.7 makes no repository mutation for it.
    const basis_body = try std.fmt.allocPrint(gpa, "file {s} {d}\n{s}", .{ basis_hash, basis.len, basis });
    defer gpa.free(basis_body);
    _ = try client.consumeReply(gpa, ms.store(), basis_body);
    try std.testing.expect(ms.store().content(ms.store().lookup(basis_hash).?) != null); // basis stored
    // The target never becomes held, and the queue entry is gone (freed, not leaked).
    if (ms.store().lookup(target_hash)) |rid| try std.testing.expect(!ms.store().held(rid));
    try std.testing.expectEqual(@as(usize, 0), client.pending.itemCount());
    try std.testing.expect(client.state != .aborted); // §7.7 emits no error
}

test "sync §7.7 L1437-1444: TargetAdmission.admits is the predicate PLUS the two L1440 card rules — a clone `file` bypasses, a clone `cfile` does not" {
    // `admitInbound` deliberately carries no card type and no clone context
    // (pinned above). `TargetAdmission` is where the draft's two rules attach, so
    // a landing path far from any card arm — the deferred queue's completion —
    // can re-ask the same question the arms asked. Both rules in one table:
    //   L1437-1440  non-clone `file`  shunned -> discard
    //   L1440       clone     `file`  shunned -> BYPASS, admitted
    //   L1440-1444  either    `cfile` shunned -> discard, even in clone
    const shunned = "d" ** 64;
    const clean = "e" ** 64;
    var shun = FixtureShunSet{ .names = &.{shunned} };

    const outside_clone = sync.TargetAdmission{ .shun = shun.shunSet(), .clone = false };
    const in_clone = sync.TargetAdmission{ .shun = shun.shunSet(), .clone = true };

    try std.testing.expect(!outside_clone.admits(shunned, false)); // non-clone file: discard
    try std.testing.expect(in_clone.admits(shunned, false)); // clone file: BYPASS
    try std.testing.expect(!outside_clone.admits(shunned, true)); // cfile: discard
    try std.testing.expect(!in_clone.admits(shunned, true)); // cfile in clone: still discard

    // An unshunned target is admitted in all four combinations — the rules gate
    // only on a shunned name.
    try std.testing.expect(outside_clone.admits(clean, false));
    try std.testing.expect(in_clone.admits(clean, false));
    try std.testing.expect(outside_clone.admits(clean, true));
    try std.testing.expect(in_clone.admits(clean, true));

    // §3.3 L523-525 — the identity: an empty table under `auto` admits everything,
    // so every call site with no policy to express behaves as before admission
    // existed.
    try std.testing.expect(sync.TargetAdmission.unrestricted.admits(shunned, true));
}

test "sparse-cone §5.3: a progress-free error round-0 reply aborts the clone" {
    // Goal: the §11.1 discard tolerance does not extend to a refusal -- a
    // round-0 reply carrying an error card and NO clone progress (no
    // clone-seqno card, no artifact) is fatal, and the peer's words are
    // kept for the diagnostic. Method: feed the one-card refusal reply and
    // pin the abort, the state, and the recorded text; contrast with §A.2's
    // advancing reply (pinned by its own tests above).
    var ms = repostate.MemStore.init(std.testing.allocator);
    defer ms.deinit();
    var client = Client{ .operation = .clone, .state = .clone_round0 };
    defer client.deinit(std.testing.allocator);
    const refusal = "error sparse\\sorigin\\sdoes\\snot\\scover\\sthe\\srequested\\sclone\\scone\n";
    try std.testing.expectError(
        error.FatalProtocolError,
        client.consumeReply(std.testing.allocator, ms.store(), refusal),
    );
    try std.testing.expectEqual(ClientState.aborted, client.state);
    try std.testing.expectEqualStrings(
        "sparse origin does not cover the requested clone cone",
        client.peerErrorText().?,
    );
}

test "sync §6.5 (#74): the server verifies an x-f-l-c cookie — accept, wrong signature refused, cookie+body login is duplicate" {
    const gpa = std.testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    const secret = sync.deriveSharedSecret("pc", "dan", "pw");
    const server = sync.Server{
        .version = .{ .version = 22900, .datetime = .{ .date = 20260715, .time = 120000 } },
        .credential = .{ .user = "dan", .secret_hex = secret },
    };
    var out: std.ArrayListUnmanaged(u8) = .empty;
    defer out.deinit(gpa);

    const request = "pragma client-version 22700 20260101 000000\npull 0123456789abcdef 0123456789abcdef\n";
    const nonce = sync.computeNonce(request);
    const signature = sync.computeSignature(&nonce, &secret);
    var cookie_buf: [256]u8 = undefined;
    const cookie = sync.writeCookieLogin(.{ .user = "dan", .nonce = &nonce, .signature = &signature }, &cookie_buf).?;

    // A valid per-request cookie authenticates: no error card in the reply.
    {
        const reply = try server.respondCookie(gpa, ms.store(), request, cookie, &out);
        try std.testing.expect(std.mem.indexOf(u8, reply, "error ") == null);
    }
    // A tampered signature is refused, not ignored.
    {
        var bad_sig = signature;
        bad_sig[0] = if (bad_sig[0] == 'a') 'b' else 'a';
        var bad_buf: [256]u8 = undefined;
        const bad = sync.writeCookieLogin(.{ .user = "dan", .nonce = &nonce, .signature = &bad_sig }, &bad_buf).?;
        const reply = try server.respondCookie(gpa, ms.store(), request, bad, &out);
        try std.testing.expect(std.mem.indexOf(u8, reply, "error ") != null);
    }
    // A nonce over different bytes (a replayed cookie) is refused: the
    // credential binds to THIS request's body, which is the whole point.
    {
        const other = "pragma client-version 22700 20260101 000000\npush 0123456789abcdef 0123456789abcdef\n";
        const reply = try server.respondCookie(gpa, ms.store(), other, cookie, &out);
        try std.testing.expect(std.mem.indexOf(u8, reply, "error ") != null);
    }
    // One login per request across BOTH channels (§6.1/§6.5): cookie plus a
    // body login card is the duplicate_login protocol error.
    {
        const tail = "pull 0123456789abcdef 0123456789abcdef\n";
        const body_nonce = sync.computeNonce(tail);
        const body_sig = sync.computeSignature(&body_nonce, &secret);
        var line: [256]u8 = undefined;
        const login = try sync.writeLogin(.{ .user = "dan", .nonce = &body_nonce, .signature = &body_sig }, &line);
        var both: std.ArrayListUnmanaged(u8) = .empty;
        defer both.deinit(gpa);
        try both.appendSlice(gpa, "pragma client-version 22700 20260101 000000\n");
        try both.appendSlice(gpa, login);
        try both.appendSlice(gpa, "\n");
        try both.appendSlice(gpa, tail);
        const dup_nonce = sync.computeNonce(both.items);
        const dup_sig = sync.computeSignature(&dup_nonce, &secret);
        var dup_buf: [256]u8 = undefined;
        const dup_cookie = sync.writeCookieLogin(.{ .user = "dan", .nonce = &dup_nonce, .signature = &dup_sig }, &dup_buf).?;
        const reply = try server.respondCookie(gpa, ms.store(), both.items, dup_cookie, &out);
        try std.testing.expect(std.mem.indexOf(u8, reply, "error ") != null);
    }
}