Skip to content
zit Artifact [ffbc289147]
Login

Artifact [ffbc289147]

Artifact ffbc289147b51160dcf10535b5cf9e4e704965ddaf1476404728281704a09fb4:


//! 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 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 = sync.CompressLevel;
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 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 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 PrivatePairing = sync.PrivatePairing;
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 ServerUvRequest = sync.ServerUvRequest;
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 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 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 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 privatePairing = sync.privatePairing;
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 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 = sync.zlibContainerDecode;
const zlibContainerEncode = sync.zlibContainerEncode;

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 §5.2/§12.3-r2: card and comment lines bounded at 65535 bytes including LF" {
    // §12.3-r2 L2755-2760: the 65535-byte line bound is a fatal-error bound —
    // an over-length line (no LF within the window) is MissingTerminator, the
    // resource-exhaustion guard restated here.
    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 §12.3-r1: the line bound is measured through LF, excluding the following byte-counted payload" {
    // §12.3 L2757-2758: the bound is measured through the terminating LF and
    // excludes 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 §12.3-r3: an over-length line aborts before card dispatch (replyBodyStep .truncated, never .card_ready)" {
    // §12.3 L2760-2762: the receiver MUST abort before interpreting an
    // over-length line as a card or consuming any subsequent payload.
    // 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 §12.3-r4: a declared 70000-byte count advances past the 65535 line bound" {
    // §12.3 L2763-2766: the line bound does not cap the SIZE/CSIZE payloads of
    // file/cfile/uvfile/config cards, which remain governed by their declared
    // byte counts. 70000 is the smallest round value above the 65535 bound.
    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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.3 r32: pushed config card with negative SIZE is fatal (rejection)" {
    try std.testing.expectError(ConfigError.NegativeSize, parseConfigCard(tokenizeCard("config /config -1")));
}

test "sync §10.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 §10.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 §10.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")));
}

test "sync §10.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 §10.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 §10.4 r37: recorded ci-lock-fault blocks only a fork-creating commit without an override" {
    // §10.4 L2548-2556: blocks 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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.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 §10.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 r3: a message card never terminates the session" {
    // L2658-2660. The client's dispositions are {pull_only_silent,
    // display_optional} -- neither terminates.
    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("hello"));
}

test "sync §11.2 r4/r5: 'pull only *' clears push intent silently; other text may display" {
    const Case = struct { text: []const u8, want: MessageAction };
    const cases = [_]Case{
        // r4 L2662-2665 -- the glob "pull only *" carries its separating space;
        // a match silently clears push intent, no display.
        .{ .text = "pull only *", .want = .pull_only_silent },
        .{ .text = "pull only clone", .want = .pull_only_silent },
        // r5 L2665-2666 -- everything else (including a bare "pull only" with no
        // trailing content) MAY be displayed.
        .{ .text = "pull only", .want = .display_optional },
        .{ .text = "hello world", .want = .display_optional },
        .{ .text = "pull all now", .want = .display_optional },
    };
    for (cases) |c| {
        try std.testing.expectEqual(c.want, clientOnMessageCard(c.text));
        try std.testing.expectEqual(c.want == .pull_only_silent, matchesPullOnly(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.send_queue_nonempty = 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.send_queue_nonempty = 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 L983-985 — 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 L983-985 "and a malformed content card (Section 7)" — a
    // content-bearing operator carrying surplus tokens (§5 L234-235).
    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 L990-999 — liberal ∪ strict partitions the posture; the four
    // named points (L992-998) each tolerate, every other malformed input
    // 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" {
    // §3.9 L994-995 — 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 L995-996 — 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 L234-235), a simple card is
    // .bad_command (§5 L235 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" {
    // §3.9 L992-994 — 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 L996-997 (§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 L997-998 (§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 L998-999 — 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.10 r1: the request-order restatement adds no constraint checkRequestOrder lacks" {
    // §3.10 L1016-1018 — §3.10 restates §3.2 and defers to it: a Table-4
    // clone request is well-ordered; a §3.2 violation still yields the §3.2
    // outcome, so §3.10 introduces no new order rule.
    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.10 r2: the canonical Table-4 write order projects to a well-ordered request" {
    // §3.10 L1020-1054 — 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.10 r3: every request body ends with a random-bytes comment line, not the timestamp subform" {
    // §3.10 L1052-1053 (Table 4) + §5.3 L1418-1422 — the request's closing
    // comment carries implementation random bytes; the timestamp/errors
    // subform (§5.3 L1389) 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.10 r7: the login-card row defers to §6 presence and §8.3 round-0 suppression" {
    // §3.10 L1032-1034 — §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.10 adds
    // no new login 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.10 r4: the reply-card membership set resolves to known operators" {
    // §3.10 L1058-1103 (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.10 r4: the reply body is an unordered applicable combination, not a positional sequence" {
    // §3.10 L1058 "any applicable combination" — 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 §3.10 r5: every reply body ends with a timestamp/error-count comment line" {
    // §3.10 L1101-1103 + §5.3 L1389 — 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 L1391).
    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.10 r6: a reply push/pull advertisement is recorded but does not by itself select the operation" {
    // §3.10 L1076-1081 — 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("pull only *"));
    try std.testing.expectEqual(MessageAction.display_optional, clientOnMessageCard("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, .{});
}

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.1 r18: the container is BE32 length then a zlib stream (worked example)" {
    const encoded = try zlibContainerEncode(std.testing.allocator, "abc", .default);
    defer std.testing.allocator.free(encoded);

    // BE32 uncompressed length (r18).
    try std.testing.expectEqualSlices(u8, &[_]u8{ 0, 0, 0, 3 }, encoded[0..4]);
    // RFC1950 §2.2 MUST: CM (low nibble of CMF) is 8 (deflate).
    try std.testing.expectEqual(@as(u8, 8), encoded[4] & 0x0f);
    // RFC1950 §2.2 MUST: (CMF*256 + FLG) % 31 == 0 (FCHECK).
    const cmf_flg = @as(u16, encoded[4]) * 256 + @as(u16, encoded[5]);
    try std.testing.expectEqual(@as(u16, 0), cmf_flg % 31);
    // FDICT must be 0 — no preset dictionary.
    try std.testing.expectEqual(@as(u8, 0), encoded[5] & 0x20);
    // RFC1950 §9 Adler-32 of "abc": s1=1+97+98+99=295=0x0127,
    // s2=0+98+196+295=589=0x024D, stored MSB-first as the trailer.
    try std.testing.expectEqualSlices(u8, &[_]u8{ 0x02, 0x4D, 0x01, 0x27 }, encoded[encoded.len - 4 ..]);
}

test "sync §4.1 r19: the ordinary single-input path uses the zlib default level" {
    const encoded = try zlibContainerEncode(std.testing.allocator, "hello fossil sync", .default);
    defer std.testing.allocator.free(encoded);
    const decoded = try zlibContainerDecode(std.testing.allocator, encoded, reply_body_cap);
    defer std.testing.allocator.free(decoded);
    try std.testing.expectEqualStrings("hello fossil sync", decoded);
}

test "sync §4.1 r20: an inline-login prepend compresses the concatenation at level 9" {
    const login = "login foo bar baz\n";
    const body = "gimme 0123456789012345678901234567890123456789\n";
    const combined = login ++ body;

    const encoded = try zlibContainerEncode(std.testing.allocator, combined, .best);
    defer std.testing.allocator.free(encoded);
    try std.testing.expectEqual(@as(u32, combined.len), std.mem.readInt(u32, encoded[0..4], .big));

    const decoded = try zlibContainerDecode(std.testing.allocator, encoded, reply_body_cap);
    defer std.testing.allocator.free(decoded);
    try std.testing.expectEqualStrings(combined, decoded);
}

test "sync §4.1 r21: decoding does not depend on the compression level" {
    const input = "the quick brown fox jumps over the lazy dog";

    const enc_default = try zlibContainerEncode(std.testing.allocator, input, .default);
    defer std.testing.allocator.free(enc_default);
    const enc_best = try zlibContainerEncode(std.testing.allocator, input, .best);
    defer std.testing.allocator.free(enc_best);

    const dec_default = try zlibContainerDecode(std.testing.allocator, enc_default, reply_body_cap);
    defer std.testing.allocator.free(dec_default);
    const dec_best = try zlibContainerDecode(std.testing.allocator, enc_best, reply_body_cap);
    defer std.testing.allocator.free(dec_best);

    try std.testing.expectEqualStrings(input, dec_default);
    try std.testing.expectEqualStrings(input, dec_best);
}

test "sync §4.1: a truncated container header is rejected" {
    try std.testing.expectError(error.TruncatedHeader, zlibContainerDecode(std.testing.allocator, "\x00\x00", reply_body_cap));
}

test "sync §4.1: a decode that fits within declared capacity is accepted" {
    // §4.1 L1210-1211: "MUST NOT require the actual decoded length to equal
    // the field value when the decoder succeeds within capacity" — declared
    // 99, decoded 11 <= 99, so the shorter stream is accepted as-is.
    const encoded = try zlibContainerEncode(std.testing.allocator, "mismatch me", .default);
    defer std.testing.allocator.free(encoded);
    var lied: [64]u8 = undefined;
    @memcpy(lied[0..encoded.len], encoded);
    std.mem.writeInt(u32, lied[0..4], 99, .big); // declares 99 (capacity), stream carries 11
    const decoded = try zlibContainerDecode(std.testing.allocator, lied[0..encoded.len], reply_body_cap);
    defer std.testing.allocator.free(decoded);
    try std.testing.expectEqualStrings("mismatch me", decoded);
}

test "sync §4.1: output exceeding declared capacity is rejected" {
    // §4.1 L1209-1210: reject when the decoder cannot fit its output within
    // the declared capacity — declared 5, actual 11 > 5.
    const encoded = try zlibContainerEncode(std.testing.allocator, "mismatch me", .default);
    defer std.testing.allocator.free(encoded);
    var lied: [64]u8 = undefined;
    @memcpy(lied[0..encoded.len], encoded);
    std.mem.writeInt(u32, lied[0..4], 5, .big); // declares capacity 5, stream carries 11
    try std.testing.expectError(error.OutputTooLarge, zlibContainerDecode(std.testing.allocator, lied[0..encoded.len], reply_body_cap));
}

test "sync §4.1: a declared length over max_output is rejected" {
    const encoded = try zlibContainerEncode(std.testing.allocator, "capped", .default);
    defer std.testing.allocator.free(encoded);
    try std.testing.expectError(error.OutputTooLarge, zlibContainerDecode(std.testing.allocator, encoded, 2));
}

test "sync §4.1: garbage after the length prefix is rejected" {
    const garbage = [_]u8{ 0, 0, 0, 5, 0xff, 0xff, 0xff, 0xff, 0xff };
    try std.testing.expectError(error.InvalidStream, zlibContainerDecode(std.testing.allocator, &garbage, reply_body_cap));
}

fn encodeNoLeak(gpa: std.mem.Allocator) !void {
    const out = try zlibContainerEncode(gpa, "allocation failure probe", .default);
    gpa.free(out);
}

test "sync §4.1: encode survives allocation failure" {
    try std.testing.checkAllAllocationFailures(std.testing.allocator, encodeNoLeak, .{});
}

fn decodeNoLeak(gpa: std.mem.Allocator) !void {
    const encoded = try zlibContainerEncode(gpa, "allocation failure probe", .default);
    defer gpa.free(encoded);
    const out = try zlibContainerDecode(gpa, encoded, reply_body_cap);
    gpa.free(out);
}

test "sync §4.1: decode survives allocation failure" {
    try std.testing.checkAllAllocationFailures(std.testing.allocator, decodeNoLeak, .{});
}

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/§12.1-r2: computeNonce('example-request-body-bytes')" {
    // §12.1-r2 L2709-2711 restates this: 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 §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/§12.1-r3: distinct body tails yield distinct nonces (replay note)" {
    // §12.1-r3 L2711-2713: independently generated rounds produce distinct
    // nonces. Its causal mechanism — the per-request random comment line of
    // §5.3 (L2711-2712) — 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 §12.1-r1: neither password nor shared secret appears in the login body card" {
    // §12.1 L2704-2707: 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 L3236)" {
    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, L3268)" {
    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 L3268 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 §12.2-r5: writeCookieLogin never places the shared secret in the x-f-l-c cookie" {
    // §12.2 L2733-2734 (+ §12 preamble L2679-2681): a shared secret MUST NOT be
    // 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 §12.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 §12.1-r4: a byte-identical login verifies .ok twice (no nonce cache, no freshness check)" {
    // §12.1 L2713-2717: 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 peer: Peer = .{ .version = 22700, .datetime = .{ .date = cookie_gate_min_date, .time = cookie_gate_min_time } };
    try std.testing.expectEqual(LoginTransport.cookie, loginTransport(peer));
    try std.testing.expect(loginTransport(peer).emitsCookieHeader());
}

test "sync §6.5 r4: either condition unmet -> body-embedded login fallback" {
    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(peer));
        try std.testing.expect(loginTransport(peer).emitsBodyLogin());
    }
}

test "sync §6.5 r5: unobserved server-version (null timestamp) falls back to body" {
    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(peer));
}

test "sync §6.5 r6: ungated peer never emits the x-f-l-c cookie header" {
    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(peer).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 peer: Peer = .{ .version = 22700, .datetime = .{ .date = cookie_gate_min_date, .time = cookie_gate_min_time } };
    const transport = loginTransport(peer);
    try std.testing.expectEqual(LoginTransport.cookie, transport);
    try std.testing.expect(!transport.emitsBodyLogin());
}

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/§10.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 §10.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 §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 L3003-3031)" {
    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 L3011-3034)" {
    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{};
    const delta_bytes = "opaque delta payload, not yet decoded";
    try queue.retain(card.hash, card.deltasrc.?, delta_bytes);

    try std.testing.expectEqual(@as(usize, 1), queue.len);
    try std.testing.expectEqualStrings(card.hash, queue.items[0].hash);
    try std.testing.expectEqualStrings(card.deltasrc.?, queue.items[0].deltasrc);
    try std.testing.expectEqualStrings(delta_bytes, queue.items[0].delta_bytes);
    // 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-r1: retention queue rejects beyond its fixed capacity (bound)" {
    var queue = PendingDeltaQueue{};
    var round: usize = 0;
    while (round < PendingDeltaQueue.max_pending) : (round += 1) {
        try queue.retain(test_hash_a, test_hash_b, "d");
    }
    try std.testing.expectEqual(PendingDeltaQueue.max_pending, queue.len);
    try std.testing.expectError(PendingDeltaQueue.RetainError.QueueFull, queue.retain(test_hash_a, test_hash_b, "d"));
}

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{};
    try queue.retain(card.hash, card.deltasrc.?, 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.?);

    try std.testing.expectEqual(@as(usize, 0), queue.len);
    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{};
    try queue.retain(test_hash_c, test_hash_a, d_c);
    try queue.retain(test_hash_a, test_hash_b, 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);

    try std.testing.expectEqual(@as(usize, 0), queue.len);
    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{};
    try queue.retain(test_hash_a, test_hash_b, delta_ab);
    try queue.retain(test_hash_a, other_source, delta_ac);

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

    try std.testing.expectEqual(@as(usize, 1), queue.len);
    try std.testing.expectEqualStrings(other_source, queue.items[0].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{};
    try queue.retain(test_hash_a, test_hash_b, delta_bytes);
    _ = try store.put(test_hash_b, source);
    try queue.completeFor(allocator, store, test_hash_b);
}

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

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 r1: a private card is immediately followed by a file or cfile card" {
    try std.testing.expectEqual(PrivatePairing.content, privatePairing(.file));
    try std.testing.expectEqual(PrivatePairing.content, privatePairing(.cfile));
}

test "sync §7.5 r2: a private card not followed by a content-bearing file/cfile is a protocol error (rejection)" {
    const followers = [_]?Operator{ .igot, .gimme, .private, .@"error", .pragma, null };
    for (followers) |f| try std.testing.expectEqual(PrivatePairing.protocol_error, privatePairing(f));
}

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 r6: a private card before a uvfile is an undefined pairing routed to the r2 protocol error (rejection)" {
    try std.testing.expectEqual(PrivatePairing.protocol_error, privatePairing(.uvfile));
}

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, L3254 (public) and round-2 reply,
    // L3257 (private).
    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 L1814-1816 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, &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, &name3_copy));
    try std.testing.expect(artifact.verify(&name3_copy, good_content));
}

test "sync §7.7 L1820-1822 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, sha1_name));
    try std.testing.expectEqual(Admission.admit, admitInbound(.shun_sha1, sha3_name));
}

test "sync §7.7 L1822-1824 r3: the admission gate applies identically at file and cfile sites" {
    // One decision function, no card-type parameter: file and cfile see the
    // same rejection for the same (policy, name) pair.
    const sha1_name = "a" ** 40;
    const sites = [_]Operator{ .file, .cfile };
    for (sites) |site| {
        _ = site; // the gate takes no card-type input — this loop documents that both sites route through it identically
        try std.testing.expectEqual(Admission.reject, admitInbound(.shun_sha1, sha1_name));
    }
}

test "sync §7.7 L1823-1824 r3b: a cfile on the clone 3 SEQNO path is admission-gated too (rejection)" {
    const sha1_name = "a" ** 40;
    // clone_seqno is the Operator naming the clone 3 SEQNO path; the cfile
    // card it carries is still subject to the same admission gate.
    _ = Operator.clone_seqno;
    try std.testing.expectEqual(Admission.reject, admitInbound(.shun_sha1, sha1_name));
}

test "sync §7.7 L1818-1820 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, sha1_name));
    }
    try std.testing.expectEqual(Admission.reject, admitInbound(.shun_sha1, sha1_name));
}

test "sync §7.7 L1826-1829 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 L1829-1830 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 L1830-1832 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 L1758-1759: 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 L1748: 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 L1758+L1765-1767: a real-HASH card with the 0x0001 bit set frames its SIZE payload" {
    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" ++ ("x" ** 42) ++ "TAIL";
    const line = try readLine(body, 0);
    const payload = try consumeUvfilePayload(body, line.next, card);
    try std.testing.expectEqual(@as(usize, 42), payload.bytes.len);
    try std.testing.expectEqualStrings("TAIL", body[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" {
    // §9.1.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: over-arity uvfile card is malformed" {
    // §5.2 L1324, §7.3-r2 — the blocking case: arity (6) equals
    // max_tokens (6), so a 7th token drops silently unless `surplus`
    // is checked ahead of the length check.
    const tokens = tokenizeCard("uvfile n 1 - 0 0 x");
    try std.testing.expectEqual(@as(usize, max_tokens), tokens.len);
    try std.testing.expect(tokens.surplus);
    try std.testing.expectError(CardParseError.Malformed, parseUvfileCard(tokens));
}

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 §9.1.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 §9.1.1: catalog hash of an empty catalog is SHA-1(\"\")" {
    const hash = uvCatalogHash(&.{});
    try std.testing.expectEqualStrings("da39a3ee5e6b4b0d3255bfef95601890afd80709", &hash);
}

test "sync §9.1.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 §9.1.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 §9.1.1: status 0 when no local row exists" {
    try std.testing.expectEqual(UvStatus.absent, uvStatus(null, 100, "abc"));
}

test "sync §9.1.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 §9.1.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 §9.1.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 §9.1.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 §9.1.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 §9.1.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 §9.1.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 §9.1.1: send table init and apply survive allocation failure" {
    try std.testing.checkAllAllocationFailures(std.testing.allocator, uvSendTableNoLeak, .{});
}

test "sync §9.1.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 §9.1.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, L621-625 — 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 item 2, L626-628 — 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 L641-643 — "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 L664-666 — 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 L664-680 — 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{};
    try queue.retain(test_hash_a, test_hash_b, 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);

    // 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: a strategy requests missing content only via igot+gimme (single rule surface)" {
    // §9.3 L2328-2331 — 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" {
    // §9.4 L2334-2337 + §3.3 (a)/(b)/(c), L646-652 — 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" {
    // §9.4 L2337-2338 + §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 filter (a), L647.
    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 filter (b), L649.
    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 filter (c), L651-652.
    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 L644-645 — "evaluated freshly each round." 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 L654-655 — 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 L661-662 — 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 L658-660 — 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)" {
    // §9.5 L2341-2344 — termination is the §3.7 rule, clone 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 L775-778 + §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 L677-680), 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" {
    // §9.5 L2344-2347 + §3.7 L789-791 — 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 }));
}

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" {
    const bodies = [_][]const u8{
        "cfile " ++ a1_phantom_1 ++ " 100 40\n",
        "message hello\n",
        "file " ++ a1_phantom_1 ++ " " ++ a1_phantom_2 ++ " 5\nHELLO", // delta-form (arity 4)
    };
    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 std.testing.expectError(Client.ConsumeError.NotImplemented, client.consumeReply(std.testing.allocator, ms.store(), body));
        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);
}