Skip to content
File

Blob: src/workerd/io/compatibility-date-test.c++

23.6 KB
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#include "compatibility-date.h"
6 
7#include <workerd/io/maximum-compatibility-date.embed.h>
8 
9#include <capnp/message.h>
10#include <capnp/serialize-text.h>
11#include <kj/debug.h>
12#include <kj/test.h>
13 
14#include <chrono>
15#include <format>
16 
17namespace workerd {
18namespace {
19 
20KJ_TEST("compatibility date parsing") {
21 auto expectParseTo = [](kj::StringPtr input, kj::StringPtr expected) {
22 KJ_IF_SOME(actual, normalizeCompatDate(input)) {
23 KJ_EXPECT(actual == expected);
24 } else {
25 KJ_FAIL_EXPECT("couldn't parse", input);
26 }
27 };
28 
29 auto expectNoParse = [](kj::StringPtr input) {
30 KJ_IF_SOME(actual, normalizeCompatDate(input)) {
31 KJ_FAIL_EXPECT("expected couldn't parse", input, actual);
32 }
33 };
34 
35 expectParseTo("2021-05-17", "2021-05-17");
36 expectParseTo("2021-05-01", "2021-05-01");
37 expectParseTo("2000-01-01", "2000-01-01");
38 expectParseTo("2999-12-31", "2999-12-31");
39 expectParseTo("2024-02-29", "2024-02-29");
40 expectParseTo("2112-04-01", "2112-04-01");
41 
42 // Alas, strptime() accepts February 30 as a perfectly valid date.
43 //expectNoParse("2024-2-30");
44 //expectNoParse("2023-2-29");
45 
46 expectNoParse("2024-2-32");
47 expectNoParse("3000-01-01");
48 expectNoParse("1999-12-31");
49 expectNoParse("123-01-01");
50 expectNoParse("2021-13-01");
51 expectNoParse("2021-12-32");
52 expectNoParse("2021-00-01");
53 expectNoParse("2021-01-00");
54 
55 expectNoParse(" 2021-05-17");
56 expectNoParse("2021 -05-17");
57 expectNoParse("2021- 05-17");
58 expectNoParse("2021-05 -17");
59 expectNoParse("2021-05- 17");
60 expectNoParse("2021-05-17 ");
61 expectNoParse("2021/05/17");
62 expectNoParse("2021_05_17");
63 
64 expectNoParse("2021-5-07");
65 expectNoParse("2021-05-7");
66 expectNoParse("202-05-07");
67}
68 
69KJ_TEST("compatibility flag parsing") {
70 auto expectCompileCompatibilityFlags =
71 [](kj::StringPtr compatDate, kj::ArrayPtr<const kj::StringPtr> featureFlags,
72 kj::StringPtr expectedOutput, kj::ArrayPtr<const kj::StringPtr> expectedErrors = nullptr,
73 CompatibilityDateValidation dateValidation = CompatibilityDateValidation::FUTURE_FOR_TEST,
74 bool r2InternalBetaApiSet = false, bool experimental = false) {
75 capnp::MallocMessageBuilder message;
76 auto orphanage = message.getOrphanage();
77 
78 auto flagListOrphan = orphanage.newOrphan<capnp::List<capnp::Text>>(featureFlags.size());
79 auto flagList = flagListOrphan.get();
80 for (auto i: kj::indices(featureFlags)) {
81 flagList.set(i, featureFlags.begin()[i]);
82 }
83 
84 auto outputOrphan = orphanage.newOrphan<CompatibilityFlags>();
85 auto output = outputOrphan.get();
86 
87 SimpleWorkerErrorReporter errorReporter;
88 compileCompatibilityFlags(
89 compatDate, flagList.asReader(), output, errorReporter, experimental, dateValidation);
90 
91 capnp::TextCodec codec;
92 auto parsedExpectedOutput = codec.decode<CompatibilityFlags>(expectedOutput, orphanage);
93 
94 if (!r2InternalBetaApiSet) {
95 // The r2PublicBetaApi is always expected by default regardless of compat date unless
96 // explicitly disabled.
97 parsedExpectedOutput.get().setR2PublicBetaApi(true);
98 }
99 
100 // If errors are expected, then the output is irrelevant.
101 if (expectedErrors.size() == 0) {
102 KJ_EXPECT(kj::str(output) == kj::str(parsedExpectedOutput.getReader()));
103 }
104 KJ_EXPECT(kj::strArray(errorReporter.errors, "\n") == kj::strArray(expectedErrors, "\n"));
105 };
106 
107 expectCompileCompatibilityFlags("2021-05-17", {}, "()");
108 
109 expectCompileCompatibilityFlags("2021-11-02", {}, "(formDataParserSupportsFiles = false)");
110 expectCompileCompatibilityFlags("2021-11-03", {}, "(formDataParserSupportsFiles = true)");
111 expectCompileCompatibilityFlags("2021-11-04", {}, "(formDataParserSupportsFiles = true)");
112 expectCompileCompatibilityFlags("2021-11-03", {"formdata_parser_converts_files_to_strings"},
113 "(formDataParserSupportsFiles = false)");
114 
115 // Test compatibility flag overrides.
116 expectCompileCompatibilityFlags(
117 "2021-05-17", {"formdata_parser_supports_files"_kj}, "(formDataParserSupportsFiles = true)");
118 expectCompileCompatibilityFlags("2021-05-17", {"fetch_refuses_unknown_protocols"_kj},
119 "(fetchRefusesUnknownProtocols = true)");
120 expectCompileCompatibilityFlags("2021-05-17",
121 {"formdata_parser_supports_files"_kj, "fetch_refuses_unknown_protocols"_kj},
122 "(formDataParserSupportsFiles = true, fetchRefusesUnknownProtocols = true)");
123 expectCompileCompatibilityFlags("2021-11-04", {"fetch_refuses_unknown_protocols"_kj},
124 "(formDataParserSupportsFiles = true, fetchRefusesUnknownProtocols = true)");
125 
126 // Test errors.
127 expectCompileCompatibilityFlags("abcd", {}, "()", {"Invalid compatibility date: abcd"});
128 expectCompileCompatibilityFlags("2021-05-17",
129 {"formdata_parser_supports_files"_kj, "formdata_parser_supports_files"_kj},
130 "(formDataParserSupportsFiles = true)",
131 {"Compatibility flag specified multiple times: formdata_parser_supports_files"});
132 expectCompileCompatibilityFlags("2021-05-17",
133 {"formdata_parser_supports_files"_kj, "formdata_parser_converts_files_to_strings"_kj},
134 "(formDataParserSupportsFiles = true)",
135 {"Compatibility flags are mutually contradictory: "
136 "formdata_parser_supports_files vs formdata_parser_converts_files_to_strings"});
137 expectCompileCompatibilityFlags("2021-11-04", {"formdata_parser_supports_files"_kj},
138 "(formDataParserSupportsFiles = true)",
139 {"The compatibility flag formdata_parser_supports_files became the default as of "
140 "2021-11-03 so does not need to be specified anymore."},
141 CompatibilityDateValidation::CURRENT_DATE_FOR_CLOUDFLARE);
142 expectCompileCompatibilityFlags(
143 "2021-05-17", {"unknown_feature"_kj}, "()", {"No such compatibility flag: unknown_feature"});
144 
145 expectCompileCompatibilityFlags("2252-04-01", {}, "()",
146 {"Can't set compatibility date in the future: 2252-04-01"},
147 CompatibilityDateValidation::CURRENT_DATE_FOR_CLOUDFLARE);
148 
149 expectCompileCompatibilityFlags("2252-04-01", {}, "()",
150 {kj::str("This Worker requires compatibility date \"2252-04-01\", but the newest date "
151 "supported by this server binary is \"",
152 MAXIMUM_COMPATIBILITY_DATE, "\"."),
153 kj::str("Can't set compatibility date in the future: \"2252-04-01\". Today's date "
154 "(UTC) is \"",
155 currentDateStr(), "\".")},
156 CompatibilityDateValidation::CODE_VERSION);
157 
158 // Test experimental requirement using durable_object_rename as it is obsolete
159 expectCompileCompatibilityFlags("2020-01-01", {"durable_object_rename"_kj}, "(obsolete19 = true)",
160 {"The compatibility flag durable_object_rename is experimental and may break or be removed "
161 "in a future version of workerd. To use this flag, you must pass --experimental on the "
162 "command line."_kj},
163 CompatibilityDateValidation::CODE_VERSION, false, false);
164 expectCompileCompatibilityFlags("2020-01-01", {"durable_object_rename"_kj}, "(obsolete19 = true)",
165 {}, CompatibilityDateValidation::CODE_VERSION, false, true);
166 
167 // Test experimental requirement using the durable_object_alarms flag since we know this flag
168 // is obsolete and will never have a date set. (Should always pass, even if experimental flags
169 // aren't allowed)
170 expectCompileCompatibilityFlags("2020-01-01", {"durable_object_alarms"_kj}, "(obsolete14 = true)",
171 {}, CompatibilityDateValidation::CODE_VERSION, false, false);
172 expectCompileCompatibilityFlags("2020-01-01", {"durable_object_alarms"_kj}, "(obsolete14 = true)",
173 {}, CompatibilityDateValidation::CODE_VERSION, false, true);
174 
175 // Multiple errors.
176 expectCompileCompatibilityFlags("abcd",
177 {"formdata_parser_supports_files"_kj, "fetch_refuses_unknown_protocols"_kj,
178 "unknown_feature"_kj, "fetch_refuses_unknown_protocols"_kj, "another_feature"_kj,
179 "formdata_parser_supports_files"_kj},
180 "(formDataParserSupportsFiles = true, fetchRefusesUnknownProtocols = true)",
181 {"Compatibility flag specified multiple times: fetch_refuses_unknown_protocols",
182 "Compatibility flag specified multiple times: formdata_parser_supports_files",
183 "Invalid compatibility date: abcd", "No such compatibility flag: another_feature",
184 "No such compatibility flag: unknown_feature"});
185 
186 // Can explicitly disable flag that's enabled for all dates.s
187 expectCompileCompatibilityFlags("2021-05-17", {"r2_internal_beta_bindings"}, "()", {},
188 CompatibilityDateValidation::FUTURE_FOR_TEST, true, false);
189 
190 // nodejs_compat implies nodejs_compat_v2 on or after 2024-09-23
191 expectCompileCompatibilityFlags("2024-09-23", {"nodejs_compat"},
192 "(formDataParserSupportsFiles = true,"
193 " fetchRefusesUnknownProtocols = true,"
194 " esiIncludeIsVoidTag = false,"
195 " obsolete3 = false,"
196 " durableObjectFetchRequiresSchemeAuthority = true,"
197 " streamsByobReaderDetachesBuffer = true,"
198 " streamsJavaScriptControllers = true,"
199 " jsgPropertyOnPrototypeTemplate = true,"
200 " minimalSubrequests = true,"
201 " noCotsOnExternalFetch = true,"
202 " specCompliantUrl = true,"
203 " globalNavigator = true,"
204 " captureThrowsAsRejections = true,"
205 " r2PublicBetaApi = true,"
206 " obsolete14 = false,"
207 " noSubstituteNull = true,"
208 " transformStreamJavaScriptControllers = true,"
209 " r2ListHonorIncludeFields = true,"
210 " exportCommonJsDefaultNamespace = true,"
211 " obsolete19 = false,"
212 " webSocketCompression = true,"
213 " nodeJsCompat = true,"
214 " obsolete22 = false,"
215 " specCompliantResponseRedirect = true,"
216 " workerdExperimental = false,"
217 " durableObjectGetExisting = false,"
218 " httpHeadersGetSetCookie = true,"
219 " dispatchExceptionTunneling = true,"
220 " serviceBindingExtraHandlers = false,"
221 " noCfBotManagementDefault = true,"
222 " urlSearchParamsDeleteHasValueArg = true,"
223 " strictCompression = true,"
224 " brotliContentEncoding = true,"
225 " strictCrypto = true,"
226 " rttiApi = false,"
227 " obsolete35 = false,"
228 " cryptoPreservePublicExponent = true,"
229 " vectorizeQueryMetadataOptional = true,"
230 " unsafeModule = false,"
231 " jsRpc = false,"
232 " noImportScripts = true,"
233 " nodeJsAls = false,"
234 " queuesJsonMessages = true,"
235 " pythonWorkers = false,"
236 " fetcherNoGetPutDelete = true,"
237 " unwrapCustomThenables = true,"
238 " fetcherRpc = true,"
239 " internalStreamByobReturn = true,"
240 " blobStandardMimeType = true,"
241 " fetchStandardUrl = true,"
242 " nodeJsCompatV2 = true,"
243 " globalFetchStrictlyPublic = false,"
244 " newModuleRegistry = false,"
245 " allowCustomPorts = true,"
246 " internalWritableStreamAbortClearsQueue = true,"
247 " nodeJsZlib = true)",
248 {}, CompatibilityDateValidation::FUTURE_FOR_TEST, false, false);
249 expectCompileCompatibilityFlags("2024-09-22", {"nodejs_compat"},
250 "(formDataParserSupportsFiles = true,"
251 " fetchRefusesUnknownProtocols = true,"
252 " esiIncludeIsVoidTag = false,"
253 " obsolete3 = false,"
254 " durableObjectFetchRequiresSchemeAuthority = true,"
255 " streamsByobReaderDetachesBuffer = true,"
256 " streamsJavaScriptControllers = true,"
257 " jsgPropertyOnPrototypeTemplate = true,"
258 " minimalSubrequests = true,"
259 " noCotsOnExternalFetch = true,"
260 " specCompliantUrl = true,"
261 " globalNavigator = true,"
262 " captureThrowsAsRejections = true,"
263 " r2PublicBetaApi = true,"
264 " obsolete14 = false,"
265 " noSubstituteNull = true,"
266 " transformStreamJavaScriptControllers = true,"
267 " r2ListHonorIncludeFields = true,"
268 " exportCommonJsDefaultNamespace = true,"
269 " obsolete19 = false,"
270 " webSocketCompression = true,"
271 " nodeJsCompat = true,"
272 " obsolete22 = false,"
273 " specCompliantResponseRedirect = true,"
274 " workerdExperimental = false,"
275 " durableObjectGetExisting = false,"
276 " httpHeadersGetSetCookie = true,"
277 " dispatchExceptionTunneling = true,"
278 " serviceBindingExtraHandlers = false,"
279 " noCfBotManagementDefault = true,"
280 " urlSearchParamsDeleteHasValueArg = true,"
281 " strictCompression = true,"
282 " brotliContentEncoding = true,"
283 " strictCrypto = true,"
284 " rttiApi = false,"
285 " obsolete35 = false,"
286 " cryptoPreservePublicExponent = true,"
287 " vectorizeQueryMetadataOptional = true,"
288 " unsafeModule = false,"
289 " jsRpc = false,"
290 " noImportScripts = true,"
291 " nodeJsAls = false,"
292 " queuesJsonMessages = true,"
293 " pythonWorkers = false,"
294 " fetcherNoGetPutDelete = true,"
295 " unwrapCustomThenables = true,"
296 " fetcherRpc = true,"
297 " internalStreamByobReturn = true,"
298 " blobStandardMimeType = true,"
299 " fetchStandardUrl = true,"
300 " nodeJsCompatV2 = false,"
301 " globalFetchStrictlyPublic = false,"
302 " newModuleRegistry = false,"
303 " cacheOptionEnabled = false,"
304 " kvDirectBinding = false,"
305 " allowCustomPorts = true,"
306 " increaseWebsocketMessageSize = false,"
307 " internalWritableStreamAbortClearsQueue = true,"
308 " pythonWorkersDevPyodide = false,"
309 " nodeJsZlib = false)",
310 {}, CompatibilityDateValidation::FUTURE_FOR_TEST, false, false);
311}
312 
313KJ_TEST("encode to flag list for FL") {
314 capnp::MallocMessageBuilder message;
315 auto orphanage = message.getOrphanage();
316 
317 auto compileOwnFeatureFlags =
318 [&](kj::StringPtr compatDate, kj::ArrayPtr<const kj::StringPtr> featureFlags,
319 CompatibilityDateValidation dateValidation = CompatibilityDateValidation::FUTURE_FOR_TEST,
320 bool experimental = false) {
321 auto flagListOrphan = orphanage.newOrphan<capnp::List<capnp::Text>>(featureFlags.size());
322 auto flagList = flagListOrphan.get();
323 for (auto i: kj::indices(featureFlags)) {
324 flagList.set(i, featureFlags.begin()[i]);
325 }
326 
327 auto outputOrphan = orphanage.newOrphan<CompatibilityFlags>();
328 auto output = outputOrphan.get();
329 
330 SimpleWorkerErrorReporter errorReporter;
331 
332 compileCompatibilityFlags(
333 compatDate, flagList.asReader(), output, errorReporter, experimental, dateValidation);
334 KJ_ASSERT(errorReporter.errors.empty());
335 
336 return kj::mv(outputOrphan);
337 };
338 
339 {
340 // Disabled by date.
341 auto featureFlagsOrphan = compileOwnFeatureFlags("2021-05-17", {});
342 auto featureFlags = featureFlagsOrphan.get();
343 auto strings = decompileCompatibilityFlagsForFl(featureFlags);
344 KJ_EXPECT(strings.size() == 0);
345 }
346 
347 {
348 // Disabled by date, enabled by flag.
349 auto featureFlagsOrphan = compileOwnFeatureFlags("2021-05-17", {"minimal_subrequests"_kj});
350 auto featureFlags = featureFlagsOrphan.get();
351 auto strings = decompileCompatibilityFlagsForFl(featureFlags);
352 KJ_EXPECT(strings.size() == 1);
353 KJ_EXPECT(strings[0] == "minimal_subrequests"_kj);
354 }
355 
356 {
357 // Enabled by date.
358 auto featureFlagsOrphan = compileOwnFeatureFlags("2022-07-01", {});
359 auto featureFlags = featureFlagsOrphan.get();
360 auto strings = decompileCompatibilityFlagsForFl(featureFlags);
361 KJ_EXPECT(strings.size() == 2);
362 KJ_EXPECT(strings[0] == "minimal_subrequests"_kj);
363 KJ_EXPECT(strings[1] == "no_cots_on_external_fetch"_kj);
364 }
365 
366 {
367 // Enabled by date, disabled by flag.
368 auto featureFlagsOrphan = compileOwnFeatureFlags("2022-07-01", {"cots_on_external_fetch"});
369 auto featureFlags = featureFlagsOrphan.get();
370 auto strings = decompileCompatibilityFlagsForFl(featureFlags);
371 KJ_EXPECT(strings.size() == 1);
372 KJ_EXPECT(strings[0] == "minimal_subrequests"_kj);
373 }
374}
375 
376KJ_TEST("compatibility dates must be Tuesday, Wednesday, or Thursday") {
377 // List of specific flags that are allowed to use non-conformant dates
378 // (already deployed and can't be changed for compatibility reasons)
379 kj::HashSet<kj::StringPtr> allowedFlagExceptions;
380 allowedFlagExceptions.insertAll(std::initializer_list<kj::StringPtr>{
381 // Existing non-conformant dates that are already deployed
382 "jsgPropertyOnPrototypeTemplate"_kj, // 2022-01-31 (Monday)
383 "specCompliantUrl"_kj, // 2022-10-31 (Monday)
384 "globalNavigator"_kj, // 2022-03-21 (Monday)
385 "captureThrowsAsRejections"_kj, // 2022-10-31 (Monday)
386 "exportCommonJsDefaultNamespace"_kj, // 2022-10-31 (Monday)
387 "urlSearchParamsDeleteHasValueArg"_kj, // 2023-07-01 (Saturday)
388 "brotliContentEncoding"_kj, // 2024-04-29 (Monday)
389 "cryptoPreservePublicExponent"_kj, // 2023-12-01 (Friday)
390 "noImportScripts"_kj, // 2024-03-04 (Monday)
391 "queuesJsonMessages"_kj, // 2024-03-18 (Monday)
392 "unwrapCustomThenables"_kj, // 2024-04-01 (Monday)
393 "internalStreamByobReturn"_kj, // 2024-05-13 (Monday)
394 "blobStandardMimeType"_kj, // 2024-06-03 (Monday)
395 "fetchStandardUrl"_kj, // 2024-06-03 (Monday)
396 "cacheOptionEnabled"_kj, // 2024-11-11 (Monday)
397 "allowCustomPorts"_kj, // 2024-09-02 (Monday)
398 "internalWritableStreamAbortClearsQueue"_kj, // 2024-09-02 (Monday)
399 "handleCrossRequestPromiseResolution"_kj, // 2024-10-14 (Monday)
400 "upperCaseAllHttpMethods"_kj, // 2024-10-14 (Monday)
401 "noTopLevelAwaitInRequire"_kj, // 2024-12-02 (Monday)
402 "fixupTransformStreamBackpressure"_kj, // 2024-12-16 (Monday)
403 "obsolete74"_kj, // 2025-03-10 (Monday)
404 "cacheApiRequestCfOverridesCacheRules"_kj, // 2025-05-19 (Monday)
405 "cacheApiCompatFlags"_kj, // 2025-04-19 (Saturday)
406 "jsWeakRef"_kj, // 2025-05-05 (Monday)
407 "enableNavigatorLanguage"_kj, // 2025-05-19 (Monday)
408 "allowEvalDuringStartup"_kj, // 2025-06-01 (Sunday)
409 "bindAsyncLocalStorageSnapshot"_kj, // 2025-06-16 (Monday)
410 "throwOnUnrecognizedImportAssertion"_kj, // 2025-06-16 (Monday)
411 "setEventTargetThis"_kj, // 2025-08-01 (Friday)
412 "enableForwardableEmailFullHeaders"_kj, // 2025-08-01 (Friday)
413 "exposeGlobalMessageChannel"_kj, // 2025-08-15 (Friday)
414 "pythonWorkersForceNewVendorPath"_kj, // 2025-08-11 (Monday)
415 "enableWorkflowScriptValidation"_kj, // 2025-09-20 (Saturday)
416 "stripAuthorizationOnCrossOriginRedirect"_kj, // 2025-09-01 (Monday)
417 "enableCtxExports"_kj, // 2025-11-17 (Monday)
418 
419 // Non-conformant dates via impliedByAfterDate
420 "pythonWorkers"_kj, // 2000-01-01 (Saturday) via impliedByAfterDate
421 "nodeJsCompatV2"_kj, // 2024-09-23 (Monday) via impliedByAfterDate
422 "nodeJsZlib"_kj, // 2024-09-23 (Monday) via impliedByAfterDate
423 "enableNodejsHttpModules"_kj, // 2025-08-15 (Friday) via impliedByAfterDate
424 "enableNodejsHttpServerModules"_kj, // 2025-09-01 (Monday) via impliedByAfterDate
425 "removeNodejsCompatEOL"_kj, // 2025-09-01 (Monday) via impliedByAfterDate
426 "enableNodeJsHttp2Module"_kj, // 2025-09-01 (Monday) via impliedByAfterDate
427 "removeNodejsCompatEOLv23"_kj, // 2025-09-01 (Monday) via impliedByAfterDate
428 "enableNodeJsProcessV2"_kj, // 2025-09-15 (Monday) via impliedByAfterDate
429 "enableNodeJsFsModule"_kj, // 2025-09-15 (Monday) via impliedByAfterDate
430 "enableNodeJsOsModule"_kj, // 2025-09-15 (Monday) via impliedByAfterDate
431 "pythonWorkflows"_kj, // 2025-09-20 (Saturday) via impliedByAfterDate
432 "enableNodeJsConsoleModule"_kj, // 2025-09-21 (Sunday) via impliedByAfterDate
433 "pythonWorkers20250116"_kj, // 2025-09-29 (Monday) via impliedByAfterDate
434 "removeNodejsCompatEOLv22"_kj, // 2027-04-30 (Friday) via impliedByAfterDate
435 "removeNodejsCompatEOLv24"_kj, // 2028-04-30 (Sunday) via impliedByAfterDate
436 });
437 
438 // Helper function to suggest the next valid date (Tuesday, Wednesday, or Thursday)
439 auto suggestNextValidDate =
440 [](const std::chrono::year_month_day& ymd) -> std::chrono::year_month_day {
441 auto currentDate = std::chrono::sys_days{ymd};
442 auto wd = std::chrono::weekday{currentDate};
443 
444 // Calculate days until next Tuesday (2), Wednesday (3), or Thursday (4)
445 int currentDay = wd.c_encoding(); // 0 = Sunday, 1 = Monday, etc.
446 int daysToAdd;
447 
448 if (currentDay <= 1) {
449 // Sunday (0) or Monday (1): next valid day is Tuesday
450 daysToAdd = 2 - currentDay;
451 } else if (currentDay >= 5) {
452 // Friday (5) or Saturday (6): next valid day is Tuesday of next week
453 daysToAdd = (7 - currentDay) + 2;
454 } else {
455 // Already Tuesday/Wednesday/Thursday - this shouldn't happen
456 daysToAdd = 0;
457 }
458 
459 return std::chrono::year_month_day{currentDate + std::chrono::days{daysToAdd}};
460 };
461 
462 // Helper function to parse and validate date
463 auto parseDate = [](kj::StringPtr dateStr) -> kj::Maybe<std::chrono::year_month_day> {
464 // First validate the date using normalizeCompatDate
465 KJ_IF_SOME(normalized, normalizeCompatDate(dateStr)) {
466 // Parse the validated date string
467 int year, month, day;
468 KJ_ASSERT(sscanf(normalized.cStr(), "%d-%d-%d", &year, &month, &day) == 3);
469 
470 // Create year_month_day
471 auto ymd = std::chrono::year_month_day{
472 std::chrono::year{year},
473 std::chrono::month{static_cast<unsigned>(month)},
474 std::chrono::day{static_cast<unsigned>(day)},
475 };
476 KJ_ASSERT(ymd.ok());
477 
478 return ymd;
479 } else {
480 return kj::none;
481 }
482 };
483 
484 // Check all compatibility flag fields
485 auto schema = capnp::Schema::from<CompatibilityFlags>();
486 auto fields = schema.getFields();
487 
488 kj::Vector<kj::String> violations;
489 
490 for (auto field: fields) {
491 auto fieldName = field.getProto().getName();
492 
493 // Skip if this specific flag is in the allowed exceptions list
494 if (allowedFlagExceptions.contains(fieldName)) {
495 continue;
496 }
497 
498 for (auto annotation: field.getProto().getAnnotations()) {
499 kj::Maybe<kj::StringPtr> maybeDateStr;
500 
501 if (annotation.getId() == COMPAT_ENABLE_DATE_ANNOTATION_ID) {
502 maybeDateStr = annotation.getValue().getText();
503 } else if (annotation.getId() == IMPLIED_BY_AFTER_DATE_ANNOTATION_ID) {
504 auto value = annotation.getValue();
505 auto s = value.getStruct().as<workerd::ImpliedByAfterDate>();
506 maybeDateStr = s.getDate();
507 }
508 
509 KJ_IF_SOME(dateStr, maybeDateStr) {
510 auto ymd = KJ_REQUIRE_NONNULL(
511 parseDate(dateStr), "Invalid compatibility flag date format: ", dateStr);
512 auto suggestedYmd = suggestNextValidDate(ymd);
513 
514 // If suggestNextValidDate returns a different date, the original date was invalid
515 if (ymd != suggestedYmd) {
516 static const char* dayNames[] = {
517 "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
518 
519 auto wd = std::chrono::weekday{std::chrono::sys_days{ymd}};
520 int dayOfWeek = wd.c_encoding();
521 
522 auto suggestedWd = std::chrono::weekday{std::chrono::sys_days{suggestedYmd}};
523 int suggestedDayOfWeek = suggestedWd.c_encoding();
524 
525 auto suggestedDateStr = std::format("{:%F}", suggestedYmd);
526 violations.add(kj::str("Field '", fieldName, "' has date ", dateStr, " which is a ",
527 dayNames[dayOfWeek], ". Dates must be Tuesday, Wednesday, or Thursday. ",
528 "Suggestion: use ", suggestedDateStr.c_str(), " (", dayNames[suggestedDayOfWeek],
529 ") instead."));
530 }
531 }
532 }
533 }
534 
535 if (!violations.empty()) {
536 KJ_FAIL_ASSERT("Compatibility date violations found:\n", kj::strArray(violations, "\n"));
537 }
538}
539 
540} // namespace
541} // namespace workerd