Artifact d1d1319d9c388001fa09de0fab206548141464bf2464331c1ab0fb354e91890b:
- File
conformance/sync_server_pragma_test.go
— part of check-in
[600861484f]
at
2026-10-13 15:55:30
on branch main
— test(conformance): re-pin the -01 drafts with the 2026-10-13 errata (#430)
* test(conformance): re-pin the -01 drafts with the 2026-10-13 errata
Re-pin the repo-state and sync-protocol drafts to zit check-in 5a1b741b25d8, which applies five more -01 errata: repo-state §5.3, §7.3 and §8.3, sync-protocol §9.4/§9.5 and §10.1. zit now keeps each -01 text under the -00 file name; the copies keep their -01 names and conformance/rfcs/README.md records the zit path and check-in. The new errata are pinned, and the requirement tables are re-read against every changed hunk.
The §7.3 and §8.3 cases that were deviations now pass as written, and new cases cover a date tag's arrival order (§5.3), untagged leaves and a merge parent's recheck (§7.3).
The sync server answers each guard-passing ci-lock with pragma avoid-delta-manifests while forbid-delta-manifests is on, defaulting to on as the draft and fossil 2.29 do (fossil 2.28 defaults it off). The client leaves its transfer unchanged on that pragma. A file or cfile whose content does not match its hash now fails the client's round, in a pull and a clone: the reply rolls back, prefix included, and no further round starts.
The leaf table is still recomputed per crosslink batch, so a parent whose untagged child arrives in a later batch loses the verdict §7.3 keeps; that case is marked as a gap, tracked in #426.
* fix(sync): continue after a uvfile content error; renumber the avoid-delta-manifests row
A uvfile whose content does not match its HASH is a handler-reported content error: warn, commit the prefix and follow ordinary continuation (sync-protocol-01 10.1). The avoid-delta-manifests client row is SP-9.5-7 now that share-links took SP-9.5-5 and SP-9.5-6. Older unreleased changelog entries drop the pending-erratum wording. (user: [email protected], size: 12516) [annotate] [blame] [check-ins using]
package conformance import "strings" // serverDirectionCases are Section 8.5 and 8.5.1: project codes and the // unversioned catalog. func serverDirectionCases() []syncServerCase { return []syncServerCase{{ Case: Case{ID: "sync.8.5.project-mismatch-pull", Req: "SP-8.5-1"}, body: []string{"{CV}", "pull {SC} {Z1}", "{R}"}, want: all(completed, onlyError("wrong project"), noCard("igot ")), }, { Case: Case{ID: "sync.8.5.project-mismatch-push", Req: "SP-8.5-1"}, body: []string{"{CV}", "push {SC} {Z1}", "file {N1} {N1.size}", "{N1.data}", "{R}"}, want: all(completed, onlyError("wrong project"), held("N1", 0)), }, { Case: Case{ID: "sync.8.5.1.hash-match", Req: "SP-8.5.1-4"}, body: []string{"{CV}", pullCard, "pragma uv-hash {UVCAT}", "{R}"}, want: all(completed, noCard("uvigot "), noCard("pragma uv-")), }, { Case: Case{ID: "sync.8.5.1.catalog-with-tombstones", Req: "SP-8.5.1-4"}, setup: tombstone, body: []string{"{CV}", pullCard, "pragma uv-hash {Z1}", "{R}"}, want: all(completed, withCard("uvigot u.txt {UVT} {UVH} 11"), withCard("uvigot t.txt {UVT} - 0"), count("uvigot ", 2)), }, { Case: Case{ID: "sync.9.2.push-ok-needs-write", Req: "SP-9.2-1"}, body: []string{"{CV}", pullCard, "pragma uv-hash {Z1}", "{R}"}, want: all(completed, count("pragma uv-pull-only", 1), noCard("pragma uv-push-ok")), }, { Case: Case{ID: "sync.9.2.push-ok-with-write", Req: "SP-9.2-1"}, body: []string{"{LOGIN uvw secret}", "{CV}", pullCard, "pragma uv-hash {Z1}", "{R}"}, want: all(completed, count("pragma uv-push-ok", 1), noCard("pragma uv-pull-only")), }} } // serverPragmaCases are Section 9: unknown and wrong-direction pragmas, // a bare pragma, and the commit lock. func serverPragmaCases() []syncServerCase { return []syncServerCase{{ Case: Case{ID: "sync.9.unknown-pragma-server", Req: "SP-9-1"}, body: []string{"{CV}", "pragma zzz 1", pullCard, "{R}"}, want: all(completed, noErrors, withCard("igot {A1}")), }, { Case: Case{ID: "sync.9.wrong-direction-server", Req: "SP-9-2"}, body: []string{"{CV}", "pragma uv-push-ok", "pragma server-version 1", pullCard, "{R}"}, want: all(completed, noErrors, withCard("igot {A1}")), }, { Case: Case{ID: "sync.9.bare-pragma", Req: "SP-9-3"}, body: []string{"{CV}", pullCard, "pragma", "gimme {A1}", "{R}"}, want: all(completed, onlyError("bad command: pragma\n"), withCard("file {A1} {A1.size}")), }, { // Logged in, so the lock's LOGIN is a name: fossil(1) leaves it // empty for nobody, an empty token no canonical card can carry. Case: Case{ID: "sync.9.4.ci-lock-conflict", Req: "SP-9.4-1"}, body: []string{"{LOGIN alice secret}", "{CV}", pushCard, "pragma ci-lock {M2} aaaa", "pragma ci-lock {M2} bbbb", "{R}"}, want: all(completed, count("pragma ci-lock-fail ", 1), matches("pragma ci-lock-fail ", `^pragma ci-lock-fail alice \d+$`)), }, { Case: Case{ID: "sync.9.4.ci-lock-same-client", Req: "SP-9.4-1"}, body: []string{"{LOGIN alice secret}", "{CV}", pushCard, "pragma ci-lock {M2} aaaa", "pragma ci-lock {M2} aaaa", "{R}"}, want: all(completed, noErrors, noCard("pragma ci-lock-fail")), }, { // No lock is held: one is written, for alice and client aaaa, and // nothing acknowledges it. Case: Case{ID: "sync.9.4.ci-lock-written", Req: "SP-9.4-1"}, body: []string{"{LOGIN alice secret}", "{CV}", pushCard, "pragma ci-lock {M2} aaaa", "{R}"}, want: all(completed, noErrors, noCard("pragma ci-lock"), repoCount("SELECT count(*) FROM config WHERE name='ci-lock-{M2}'"+ " AND json_extract(value, '$.login')='alice'"+ " AND json_extract(value, '$.clientid')='aaaa'", 1)), }, { // bob's lock for client aaaa is live: alice's ci-lock from the same // client leaves it as it was, holder and time alike. Case: Case{ID: "sync.9.4.ci-lock-kept", Req: "SP-9.4-1"}, setup: []string{"INSERT INTO config(name, value, mtime) VALUES('ci-lock-{M2}'," + " json_object('login', 'bob', 'clientid', 'aaaa'), strftime('%s', 'now') - 30)"}, body: []string{"{LOGIN alice secret}", "{CV}", pushCard, "pragma ci-lock {M2} aaaa", "{R}"}, want: all(completed, noErrors, noCard("pragma ci-lock"), repoCount("SELECT count(*) FROM config WHERE name='ci-lock-{M2}'"+ " AND json_extract(value, '$.login')='bob'"+ " AND mtime < strftime('%s', 'now') - 20", 1)), }} } // serverPragmaLockCases continues serverPragmaCases with the rest of the // commit lock rows of Section 9.4; the table is split to stay short. func serverPragmaLockCases() []syncServerCase { return []syncServerCase{{ // §9.4: the dispatcher applies ci-lock with no Write-capability test, // so nobody, who cannot commit, still takes the lock. Case: Case{ID: "sync.9.4.ci-lock-no-write-gate", Req: "SP-9.4-1"}, setup: closed, body: []string{"{CV}", "pragma ci-lock {M2} aaaa", "{R}"}, want: all(completed, noErrors, noCard("pragma ci-lock"), repoCount("SELECT count(*) FROM config WHERE name='ci-lock-{M2}'"+ " AND json_type(value, '$.login')='null'", 1)), }, { // §9.4: ci-unlock has no Write-capability test either, so nobody // on a closed repository drops client Z1's lock and any row that is // not JSON. Case: Case{ID: "sync.9.4.ci-unlock-no-write-gate", Req: "SP-9.4-2"}, setup: append(append([]string{}, closed...), "INSERT INTO config(name, value, mtime) VALUES('ci-lock-{M2}',"+ " json_object('login', 'bob', 'clientid', '{Z1}'), strftime('%s', 'now'))", "INSERT INTO config(name, value, mtime) VALUES('ci-lock-{A1}', 'junk', 1)"), body: []string{"{CV}", "pragma ci-unlock {Z1}", "{R}"}, want: all(completed, noErrors, noCard("pragma ci-lock"), repoCount("SELECT count(*) FROM config WHERE name GLOB 'ci-lock-*'", 0)), }, { // §9.4: a lock is stale once it is the configured lock-timeout // old. fossil reads the setting with db_get_int, so 0 makes every // lock stale and bob's 30-second-old lock gives way to alice's. Case: Case{ID: "sync.9.4.ci-lock-timeout-setting", Req: "SP-9.4-1"}, setup: []string{"REPLACE INTO config(name, value, mtime) VALUES('lock-timeout', '0', 1)", "INSERT INTO config(name, value, mtime) VALUES('ci-lock-{M2}'," + " json_object('login', 'bob', 'clientid', 'aaaa'), strftime('%s', 'now') - 30)"}, body: []string{"{LOGIN alice secret}", "{CV}", pushCard, "pragma ci-lock {M2} bbbb", "{R}"}, want: all(completed, noErrors, noCard("pragma ci-lock-fail"), repoCount("SELECT count(*) FROM config WHERE name='ci-lock-{M2}'"+ " AND json_extract(value, '$.login')='alice'", 1)), }, { // §9.4: ci-lock-fail carries the fossilized LOGIN, so a login with // a space stays one token. Case: Case{ID: "sync.9.4.ci-lock-fail-fossilized", Req: "SP-9.4-1"}, setup: []string{"INSERT INTO config(name, value, mtime) VALUES('ci-lock-{M2}'," + " json_object('login', 'bob smith', 'clientid', 'aaaa'), strftime('%s', 'now'))"}, body: []string{"{LOGIN alice secret}", "{CV}", pushCard, "pragma ci-lock {M2} bbbb", "{R}"}, want: all(completed, count("pragma ci-lock-fail ", 1), matches("pragma ci-lock-fail ", `^pragma ci-lock-fail bob\\ssmith \d+$`)), }, { // §9.4: the pragma has exactly an artifact-hash HASH argument, so a // token that is no hash writes no lock. Case: Case{ID: "sync.9.4.ci-lock-not-a-hash", Req: "SP-9.4-1"}, body: []string{"{LOGIN alice secret}", "{CV}", pushCard, "pragma ci-lock x aaaa", "{R}"}, want: all(completed, noErrors, repoCount("SELECT count(*) FROM config WHERE name GLOB 'ci-lock-*'", 0)), }} } // forbidOn sets forbid-delta-manifests on explicitly, for the cases about // something other than its default, which fossil(1) 2.28 reads as off. var forbidOn = []string{"REPLACE INTO config(name, value, mtime)" + " VALUES('forbid-delta-manifests', 'on', 1)"} // defaultOff is why fossil(1) 2.28 sends no avoid-delta-manifests when the // setting is absent or unreadable. const defaultOff = "fossil(1) 2.28 reads forbid-delta-manifests with a default of off; " + "Section 9.4 and fossil 2.29 (xfer.c:1831, db.c:4899) default it on" // serverAvoidDeltaCases are the avoid-delta-manifests pragma of Section 9.4: // every ci-lock that passes the argument guard is answered with it while // forbid-delta-manifests is on, its default, after any ci-lock-fail. func serverAvoidDeltaCases() []syncServerCase { return []syncServerCase{{ // No lock is held: the lock is written, and the pragma is the only // card the ci-lock draws. Case: Case{ID: "sync.9.4.avoid-delta-manifests", Req: "SP-9.4-3"}, body: []string{"{LOGIN alice secret}", "{CV}", pushCard, "pragma ci-lock {M2} aaaa", "{R}"}, want: all(completed, noErrors, count("pragma avoid-delta-manifests", 1), noCard("pragma ci-lock")), fossil: defaultOff, }, { // Each ci-lock draws one; the second, refused, puts it after its // ci-lock-fail. Case: Case{ID: "sync.9.4.avoid-delta-manifests-after-fail", Req: "SP-9.4-3"}, setup: append([]string{"INSERT INTO config(name, value, mtime) VALUES('ci-lock-{M2}'," + " json_object('login', 'bob', 'clientid', 'aaaa'), strftime('%s', 'now'))"}, forbidOn...), body: []string{"{LOGIN alice secret}", "{CV}", pushCard, "pragma ci-lock {M2} bbbb", "{R}"}, want: all(completed, count("pragma avoid-delta-manifests", 1), before("pragma ci-lock-fail ", "pragma avoid-delta-manifests")), }, { // The client already holding the lock gets no lock result, but the // pragma still. Case: Case{ID: "sync.9.4.avoid-delta-manifests-same-client", Req: "SP-9.4-3"}, setup: forbidOn, body: []string{"{LOGIN alice secret}", "{CV}", pushCard, "pragma ci-lock {M2} aaaa", "pragma ci-lock {M2} aaaa", "{R}"}, want: all(completed, noErrors, count("pragma avoid-delta-manifests", 2), noCard("pragma ci-lock-fail")), }, { Case: Case{ID: "sync.9.4.avoid-delta-manifests-off", Req: "SP-9.4-3"}, setup: []string{"REPLACE INTO config(name, value, mtime)" + " VALUES('forbid-delta-manifests', 'off', 1)"}, body: []string{"{LOGIN alice secret}", "{CV}", pushCard, "pragma ci-lock {M2} aaaa", "{R}"}, want: all(completed, noErrors, noCard("pragma avoid-delta-manifests")), }, { // A value fossil reads as neither true nor false keeps the default. Case: Case{ID: "sync.9.4.avoid-delta-manifests-unreadable-setting", Req: "SP-9.4-3"}, setup: []string{"REPLACE INTO config(name, value, mtime)" + " VALUES('forbid-delta-manifests', 'maybe', 1)"}, body: []string{"{LOGIN alice secret}", "{CV}", pushCard, "pragma ci-lock {M2} aaaa", "{R}"}, want: all(completed, noErrors, count("pragma avoid-delta-manifests", 1)), fossil: defaultOff, }, { // A ci-lock that fails the guard, its HASH no hash, draws nothing. Case: Case{ID: "sync.9.4.avoid-delta-manifests-guard", Req: "SP-9.4-3"}, setup: forbidOn, body: []string{"{LOGIN alice secret}", "{CV}", pushCard, "pragma ci-lock x aaaa", "pragma ci-lock {M2}", "{R}"}, want: all(completed, noErrors, noCard("pragma avoid-delta-manifests")), }} } // serverConfigCases are Section 9.3. func serverConfigCases() []syncServerCase { return []syncServerCase{{ Case: Case{ID: "sync.9.3.reqconfig-no-read", Req: "SP-9.3-1"}, setup: closed, body: []string{"{CV}", "reqconfig /project", "{R}"}, want: all(completed, noErrors, noCard("config ")), }, { Case: Case{ID: "sync.9.3.reqconfig-unknown", Req: "SP-9.3-2"}, body: []string{"{CV}", pullCard, "reqconfig /bogus", "{R}"}, want: all(completed, noErrors, noCard("config ")), }, { Case: Case{ID: "sync.9.3.scalar-form", Req: "SP-9.3-3"}, body: []string{"{CV}", pullCard, "reqconfig /project", "{R}"}, want: all(completed, present("config /config "), eachCard(scalarConfig)), }, { // A signed SIZE is not a config card at all, so no payload is // read: the igot after it is the next card. Case: Case{ID: "sync.9.3.config-signed-size", Req: "SP-9.3-5"}, body: []string{"{CV}", pushCard, "config /config -1", "igot {U}", "{R}"}, want: all(completed, onlyError("bad command: config /config -1\n"), withCard("gimme {U}")), }} } // scalarConfig checks a config card is the Section 9.3 scalar form: // config /config SIZE, then MTIME 'NAME' value 'VALUE' and one LF. func scalarConfig(c wireCard) error { if c.op() != "config" { return nil } if !strings.HasPrefix(c.line, "config /config ") { return expectf("%q is not config /config", c.line) } text := string(c.payload) if !scalarPayload.MatchString(text) { return expectf("%q: payload %q is not MTIME 'NAME' value 'VALUE' LF", c.line, text) } return nil }