@@ -62,4 +62,63 @@ struct SyncRecordTypeTests {
6262 let prefixes = SyncRecordType . allCases. map ( \. recordNamePrefix)
6363 #expect( Set ( prefixes) . count == prefixes. count)
6464 }
65+
66+ @Test ( " A name that already fits is returned unchanged " , arguments: SyncRecordType . allCases)
67+ func namesThatFitAreUnchanged( _ type: SyncRecordType ) {
68+ let id = String ( repeating: " a " , count: SyncRecordName . maximumLength - type. recordNamePrefix. count)
69+ let name = type. recordName ( for: id)
70+ #expect( name == type. recordNamePrefix + id)
71+ #expect( ( name as NSString ) . length == SyncRecordName . maximumLength)
72+ }
73+
74+ @Test ( " A name one unit too long is shortened " , arguments: SyncRecordType . allCases)
75+ func namesPastTheLimitAreShortened( _ type: SyncRecordType ) {
76+ let id = String ( repeating: " a " , count: SyncRecordName . maximumLength - type. recordNamePrefix. count + 1 )
77+ let name = type. recordName ( for: id)
78+ #expect( ( name as NSString ) . length <= SyncRecordName . maximumLength)
79+ #expect( name. hasPrefix ( type. recordNamePrefix + SyncRecordName. digestPrefix) )
80+ }
81+
82+ /// The limit CloudKit enforces counts UTF-16 code units, so a name of 128 emoji is over it at
83+ /// 128 characters. `scripts/check-cloudkit-record-name-limit.sh` measures that.
84+ @Test ( " The limit counts UTF-16 code units, not characters " )
85+ func theLimitCountsUTF16CodeUnits( ) {
86+ let id = String ( repeating: " 😀 " , count: 200 )
87+ #expect( id. count < SyncRecordName . maximumLength)
88+ let name = SyncRecordType . settings. recordName ( for: id)
89+ #expect( ( name as NSString ) . length <= SyncRecordName . maximumLength)
90+ #expect( name. hasPrefix ( " Settings_ " + SyncRecordName. digestPrefix) )
91+ }
92+
93+ @Test ( " Shortening is stable, so two devices agree on the record " )
94+ func shorteningIsDeterministic( ) {
95+ let id = String ( repeating: " path/to/database.sqlite " , count: 40 )
96+ #expect( SyncRecordType . settings. recordName ( for: id) == SyncRecordType . settings. recordName ( for: id) )
97+ #expect(
98+ SyncRecordType . settings. recordName ( for: id) == " Settings_sha256- "
99+ + SyncRecordName. digest ( of: id)
100+ )
101+ }
102+
103+ @Test ( " Two long identifiers do not collapse onto one record " )
104+ func distinctLongIdentifiersStayDistinct( ) {
105+ let base = String ( repeating: " a " , count: 300 )
106+ #expect( SyncRecordType . settings. recordName ( for: base) != SyncRecordType . settings. recordName ( for: base + " b " ) )
107+ }
108+
109+ /// The column layout category that produced #2575: a connection UUID, a percent-encoded
110+ /// SQLite file path, an empty schema and a table name.
111+ @Test ( " A long SQLite path produces a name CloudKit accepts " )
112+ func aLongSQLitePathFits( ) {
113+ let path = " /Users/example/projects/acme/api/.wrangler/state/v3/d1 "
114+ + " /miniflare-D1DatabaseObject/ "
115+ + String( repeating: " f " , count: 64 ) + " .sqlite "
116+ let parts = [ UUID ( ) . uuidString, path, " " , " d1_migrations " ]
117+ . map { $0. addingPercentEncoding ( withAllowedCharacters: . alphanumerics) ?? $0 }
118+ let category = " columnLayout. " + parts. joined ( separator: " . " )
119+
120+ #expect( ( ( " Settings_ " + category) as NSString ) . length > SyncRecordName . maximumLength)
121+ #expect( ( SyncRecordType . settings. recordName ( for: category) as NSString ) . length
122+ <= SyncRecordName . maximumLength)
123+ }
65124}
0 commit comments