@_nazmiforreal/flutter-ota 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +496 -0
  2. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +743 -0
  3. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +7 -6
  4. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +46 -49
  5. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +44 -23
  6. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +27 -17
  7. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +11 -6
  8. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +39 -22
  9. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +22 -13
  10. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +117 -36
  11. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +15 -9
  12. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +14 -8
  13. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +37 -12
  14. package/dart-src/packages/cli-tools/lib/src/config.dart +13 -3
  15. package/dart-src/packages/cli-tools/lib/src/operations.dart +45 -0
  16. package/dart-src/packages/cli-tools/lib/src/pack.dart +9 -0
  17. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +234 -0
  18. package/dart-src/packages/cli-tools/pubspec.lock +638 -0
  19. package/dart-src/packages/cli-tools/test/backends_test.dart +207 -0
  20. package/dart-src/packages/cli-tools/test/build_pack_test.dart +101 -0
  21. package/dart-src/packages/cli-tools/test/cli_test.dart +442 -0
  22. package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +83 -0
  23. package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +256 -0
  24. package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +347 -0
  25. package/dart-src/packages/cli-tools/test/standalone_backend_test.dart +315 -0
  26. package/dart-src/packages/core/test/rollout_test.dart +128 -0
  27. package/dart-src/packages/core/test/semver_test.dart +52 -0
  28. package/dart-src/packages/core/test/semver_vectors.json +1 -0
  29. package/dart-src/packages/core/test/types_test.dart +129 -0
  30. package/dart-src/packages/core/test/uuid_test.dart +38 -0
  31. package/dart-src/plugins/aws/test/aws_database_test.dart +167 -0
  32. package/dart-src/plugins/aws/test/aws_lambda_edge_storage_test.dart +190 -0
  33. package/dart-src/plugins/aws/test/aws_storage_test.dart +73 -0
  34. package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +83 -0
  35. package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +122 -0
  36. package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +106 -0
  37. package/dart-src/plugins/cloudflare/test/d1_database_test.dart +190 -0
  38. package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +256 -0
  39. package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +83 -0
  40. package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +77 -0
  41. package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +166 -0
  42. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +74 -0
  43. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +332 -0
  44. package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +711 -0
  45. package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +294 -0
  46. package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +192 -0
  47. package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +51 -0
  48. package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +1600 -0
  49. package/dart-src/plugins/plugin-core/test/helpers_test.dart +259 -0
  50. package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +139 -0
  51. package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +86 -0
  52. package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +77 -0
  53. package/dart-src/plugins/postgres/test/mock_postgres_client.dart +297 -0
  54. package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +140 -0
  55. package/dart-src/plugins/postgres/test/postgres_database_test.dart +181 -0
  56. package/dart-src/plugins/standalone/test/standalone_test.dart +264 -0
  57. package/dart-src/plugins/supabase/test/mock_supabase_client.dart +654 -0
  58. package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +112 -0
  59. package/dart-src/plugins/supabase/test/supabase_database_test.dart +207 -0
  60. package/dart-src/plugins/supabase/test/supabase_storage_test.dart +175 -0
  61. package/package.json +1 -1
  62. package/scripts/vendor.sh +29 -0
@@ -0,0 +1,294 @@
1
+ import 'package:flutter_patcher_core/flutter_patcher_core.dart';
2
+ import 'package:flutter_patcher_plugin_core/src/bundle_unit_of_work.dart';
3
+ import 'package:flutter_patcher_plugin_core/src/bundle_unit_of_work_store.dart';
4
+ import 'package:flutter_patcher_plugin_core/src/create_database_plugin.dart';
5
+ import 'package:flutter_patcher_plugin_core/src/types.dart';
6
+ import 'package:test/test.dart';
7
+
8
+ class _MockDatabasePlugin extends AbstractDatabasePlugin {
9
+ _MockDatabasePlugin({required this.id, required this.bundles});
10
+
11
+ final String id;
12
+ final List<Bundle> bundles;
13
+
14
+ final List<BundleChange> committedChanges = [];
15
+
16
+ String get name => 'mock-$id';
17
+
18
+ @override
19
+ Future<Bundle?> getBundleById(String bundleId) async {
20
+ return bundles.where((b) => b.id == bundleId).firstOrNull;
21
+ }
22
+
23
+ @override
24
+ Future<Paginated<List<Bundle>>> getBundles(
25
+ DatabaseBundleQueryOptions options,
26
+ ) async {
27
+ final where = options.where;
28
+ var filtered = bundles;
29
+ if (where?.channel != null) {
30
+ filtered = filtered.where((b) => b.channel == where!.channel).toList();
31
+ }
32
+ return Paginated(
33
+ data: filtered,
34
+ pagination: PaginationInfo(
35
+ total: filtered.length,
36
+ hasNextPage: false,
37
+ hasPreviousPage: false,
38
+ currentPage: 1,
39
+ totalPages: 1,
40
+ ),
41
+ );
42
+ }
43
+
44
+ @override
45
+ Future<List<String>> getChannels() async => ['production'];
46
+
47
+ @override
48
+ Future<void> commitBundle({required List<BundleChange> changedSets}) async {
49
+ committedChanges.addAll(changedSets);
50
+ }
51
+
52
+ @override
53
+ Future<UpdateInfo?> getUpdateInfo(GetBundlesArgs args) async => null;
54
+
55
+ @override
56
+ Future<void> onUnmount() async {}
57
+ }
58
+
59
+ Bundle _bundle(String id, {String channel = 'production'}) => Bundle(
60
+ id: id,
61
+ platform: Platform.android,
62
+ shouldForceUpdate: false,
63
+ enabled: true,
64
+ fileHash: 'hash_$id',
65
+ storageUri: 's3://bucket/$id.zip',
66
+ channel: channel,
67
+ targetAppVersion: '1.0.0',
68
+ );
69
+
70
+ void main() {
71
+ setUp(() {
72
+ clearUnitOfWorkStore();
73
+ });
74
+
75
+ group('createDatabasePlugin', () {
76
+ late _MockDatabasePlugin mockImpl;
77
+
78
+ setUp(() {
79
+ mockImpl = _MockDatabasePlugin(
80
+ id: 'test',
81
+ bundles: [_bundle('b1'), _bundle('b2')],
82
+ );
83
+ });
84
+
85
+ test('returns a factory function', () {
86
+ final factory = createDatabasePlugin<String>(
87
+ name: 'testDb',
88
+ factory: (config) => _MockDatabasePlugin(
89
+ id: config, bundles: [_bundle('b1')],
90
+ ),
91
+ );
92
+ final createDb = factory('test');
93
+ expect(createDb, isA<Function>());
94
+ });
95
+
96
+ test('factory creates a DatabasePlugin', () {
97
+ final factory = createDatabasePlugin<String>(
98
+ name: 'testDb',
99
+ factory: (config) => _MockDatabasePlugin(
100
+ id: config, bundles: [_bundle('b1')],
101
+ ),
102
+ );
103
+ final db = factory('test')();
104
+ expect(db, isA<DatabasePlugin>());
105
+ expect(db.name, 'testDb');
106
+ });
107
+
108
+ test('getBundleById delegates to underlying impl', () async {
109
+ final factory = createDatabasePlugin<String>(
110
+ name: 'testDb',
111
+ factory: (config) => _MockDatabasePlugin(
112
+ id: config, bundles: [_bundle('b1'), _bundle('b2')],
113
+ ),
114
+ );
115
+ final db = factory('test')();
116
+ final result = await db.getBundleById('b1');
117
+ expect(result, isNotNull);
118
+ expect(result!.id, 'b1');
119
+ });
120
+
121
+ test('getBundleById returns null for missing', () async {
122
+ final factory = createDatabasePlugin<String>(
123
+ name: 'testDb',
124
+ factory: (config) => _MockDatabasePlugin(
125
+ id: config, bundles: [_bundle('b1')],
126
+ ),
127
+ );
128
+ final db = factory('test')();
129
+ final result = await db.getBundleById('missing');
130
+ expect(result, isNull);
131
+ });
132
+
133
+ test('getBundles returns paginated results', () async {
134
+ final factory = createDatabasePlugin<String>(
135
+ name: 'testDb',
136
+ factory: (config) => _MockDatabasePlugin(
137
+ id: config, bundles: [_bundle('b1'), _bundle('b2')],
138
+ ),
139
+ );
140
+ final db = factory('test')();
141
+ final result = await db.getBundles(
142
+ const DatabaseBundleQueryOptions(),
143
+ );
144
+ expect(result.data.length, 2);
145
+ expect(result.pagination.total, 2);
146
+ });
147
+
148
+ test('getChannels delegates correctly', () async {
149
+ final factory = createDatabasePlugin<String>(
150
+ name: 'testDb',
151
+ factory: (config) => _MockDatabasePlugin(
152
+ id: config, bundles: [_bundle('b1')],
153
+ ),
154
+ );
155
+ final db = factory('test')();
156
+ final channels = await db.getChannels();
157
+ expect(channels, ['production']);
158
+ });
159
+
160
+ test('appendBundle + commitBundle sends insert', () async {
161
+ final factory = createDatabasePlugin<String>(
162
+ name: 'testDb',
163
+ factory: (config) => mockImpl,
164
+ );
165
+ final db = factory('test')();
166
+
167
+ await db.appendBundle(_bundle('new'));
168
+ await db.commitBundle();
169
+
170
+ expect(mockImpl.committedChanges.length, 1);
171
+ expect(mockImpl.committedChanges.first.operation, BundleChangeOperation.insert);
172
+ expect(mockImpl.committedChanges.first.data.id, 'new');
173
+ });
174
+
175
+ test('updateBundle + commitBundle sends update', () async {
176
+ final factory = createDatabasePlugin<String>(
177
+ name: 'testDb',
178
+ factory: (config) => mockImpl,
179
+ );
180
+ final db = factory('test')();
181
+
182
+ await db.updateBundle('b1', {'enabled': false});
183
+ await db.commitBundle();
184
+
185
+ expect(mockImpl.committedChanges.length, 1);
186
+ expect(mockImpl.committedChanges.first.operation, BundleChangeOperation.update);
187
+ expect(mockImpl.committedChanges.first.data.enabled, false);
188
+ });
189
+
190
+ test('deleteBundle + commitBundle sends delete', () async {
191
+ final factory = createDatabasePlugin<String>(
192
+ name: 'testDb',
193
+ factory: (config) => mockImpl,
194
+ );
195
+ final db = factory('test')();
196
+
197
+ await db.deleteBundle(_bundle('b1'));
198
+ await db.commitBundle();
199
+
200
+ expect(mockImpl.committedChanges.length, 1);
201
+ expect(mockImpl.committedChanges.first.operation, BundleChangeOperation.delete);
202
+ });
203
+
204
+ test('commitBundle with no changes sends empty', () async {
205
+ final factory = createDatabasePlugin<String>(
206
+ name: 'testDb',
207
+ factory: (config) => mockImpl,
208
+ );
209
+ final db = factory('test')();
210
+
211
+ await db.commitBundle();
212
+ expect(mockImpl.committedChanges, isEmpty);
213
+ });
214
+
215
+ test('each factory call creates isolated UoW', () async {
216
+ final factory = createDatabasePlugin<String>(
217
+ name: 'testDb',
218
+ factory: (config) => mockImpl,
219
+ );
220
+ final db1 = factory('test')();
221
+ final db2 = factory('test')();
222
+
223
+ await db1.appendBundle(_bundle('only1'));
224
+ // db2 should not have db1's changes
225
+ await db2.commitBundle();
226
+ expect(mockImpl.committedChanges, isEmpty);
227
+ });
228
+
229
+ test('hook onDatabaseUpdated fires after commit', () async {
230
+ var hookCalled = false;
231
+ final factory = createDatabasePlugin<String>(
232
+ name: 'testDb',
233
+ factory: (config) => mockImpl,
234
+ );
235
+ final db = factory('test', DatabasePluginHooks(
236
+ onDatabaseUpdated: () async { hookCalled = true; },
237
+ ))();
238
+
239
+ await db.appendBundle(_bundle('x'));
240
+ await db.commitBundle();
241
+ expect(hookCalled, isTrue);
242
+ });
243
+
244
+ test('getUpdateInfo delegates to underlying impl', () async {
245
+ final factory = createDatabasePlugin<String>(
246
+ name: 'testDb',
247
+ factory: (config) => mockImpl,
248
+ );
249
+ final db = factory('test')();
250
+ final result = await db.getUpdateInfo(
251
+ FingerprintGetBundlesArgs(
252
+ platform: Platform.android,
253
+ bundleId: 'b1',
254
+ fingerprintHash: 'abc',
255
+ ),
256
+ );
257
+ expect(result, isNull);
258
+ });
259
+ });
260
+
261
+ group('mergeBundleUpdate', () {
262
+ test('replaces targetCohorts', () {
263
+ final base = _bundle('a');
264
+ final updated = mergeBundleUpdate(base, {
265
+ 'targetCohorts': ['cohort1'],
266
+ });
267
+ expect(updated.targetCohorts, ['cohort1']);
268
+ });
269
+
270
+ test('replaces patches', () {
271
+ final base = _bundle('a');
272
+ final updated = mergeBundleUpdate(base, {
273
+ 'patches': [],
274
+ });
275
+ expect(updated.patches, isEmpty);
276
+ });
277
+
278
+ test('updates scalar fields', () {
279
+ final base = _bundle('a');
280
+ final updated = mergeBundleUpdate(base, {
281
+ 'enabled': false,
282
+ });
283
+ expect(updated.enabled, false);
284
+ });
285
+
286
+ test('preserves existing fields not in patch', () {
287
+ final base = _bundle('a');
288
+ final updated = mergeBundleUpdate(base, {
289
+ 'enabled': false,
290
+ });
291
+ expect(updated.fileHash, 'hash_a');
292
+ });
293
+ });
294
+ }
@@ -0,0 +1,192 @@
1
+ import 'package:flutter_patcher_plugin_core/src/create_storage_plugin.dart';
2
+ import 'package:flutter_patcher_plugin_core/src/types.dart';
3
+ import 'package:test/test.dart';
4
+
5
+ class _MockNodeProfile implements NodeStorageProfile {
6
+ @override
7
+ Future<Map<String, String>> upload(String key, String filePath) async => {
8
+ 'storageUri': 'mock://$key',
9
+ };
10
+
11
+ @override
12
+ Future<bool> exists(String storageUri) async => true;
13
+
14
+ @override
15
+ Future<void> delete(String storageUri) async {}
16
+
17
+ @override
18
+ Future<void> downloadFile(String storageUri, String filePath) async {}
19
+
20
+ @override
21
+ Future<List<StorageObject>> listObjects([String? prefix]) async => [];
22
+
23
+ @override
24
+ Future<void> deleteObjects(List<String> keys) async {}
25
+ }
26
+
27
+ class _MockRuntimeProfile implements RuntimeStorageProfile {
28
+ @override
29
+ Future<Map<String, String>> getDownloadUrl(String storageUri) async => {
30
+ 'url': 'https://example.com/$storageUri',
31
+ };
32
+
33
+ @override
34
+ Future<String?> readText(String storageUri) async => 'content';
35
+ }
36
+
37
+ void main() {
38
+ group('createNodeStoragePlugin', () {
39
+ test('creates a storage plugin with node profile', () {
40
+ final factory = createNodeStoragePlugin<String>(
41
+ name: 'testStorage',
42
+ supportedProtocol: 'mock://',
43
+ factory: (config) => _MockNodeProfile(),
44
+ );
45
+ final plugin = factory('cfg');
46
+ expect(plugin.name, 'testStorage');
47
+ expect(plugin.supportedProtocol, 'mock://');
48
+ expect(plugin.profiles.hasNode, isTrue);
49
+ expect(plugin.profiles.hasRuntime, isFalse);
50
+ });
51
+
52
+ test('factory is lazy — not called until profile method invoked', () async {
53
+ var called = false;
54
+ final factory = createNodeStoragePlugin<String>(
55
+ name: 'testStorage',
56
+ supportedProtocol: 'mock://',
57
+ factory: (config) {
58
+ called = true;
59
+ return _MockNodeProfile();
60
+ },
61
+ );
62
+ final plugin = factory('cfg');
63
+ expect(called, isFalse);
64
+ // Calling a method on the profile triggers the factory
65
+ await plugin.profiles.node!.exists('uri');
66
+ expect(called, isTrue);
67
+ });
68
+
69
+ test('upload calls hook after completion', () async {
70
+ var hookCalled = false;
71
+ final factory = createNodeStoragePlugin<String>(
72
+ name: 'testStorage',
73
+ supportedProtocol: 'mock://',
74
+ factory: (config) => _MockNodeProfile(),
75
+ );
76
+ final plugin = factory('cfg', StoragePluginHooks(
77
+ onStorageUploaded: () async { hookCalled = true; },
78
+ ));
79
+ await plugin.profiles.node!.upload('key', '/path');
80
+ expect(hookCalled, isTrue);
81
+ });
82
+
83
+ test('without hook, upload works fine', () async {
84
+ final factory = createNodeStoragePlugin<String>(
85
+ name: 'testStorage',
86
+ supportedProtocol: 'mock://',
87
+ factory: (config) => _MockNodeProfile(),
88
+ );
89
+ final plugin = factory('cfg');
90
+ final result = await plugin.profiles.node!.upload('key', '/path');
91
+ expect(result['storageUri'], 'mock://key');
92
+ });
93
+
94
+ test('multiple method calls use same cached underlying profile', () async {
95
+ var callCount = 0;
96
+ final factory = createNodeStoragePlugin<String>(
97
+ name: 'testStorage',
98
+ supportedProtocol: 'mock://',
99
+ factory: (config) {
100
+ callCount++;
101
+ return _MockNodeProfile();
102
+ },
103
+ );
104
+ final plugin = factory('cfg');
105
+ // First method call triggers factory
106
+ await plugin.profiles.node!.exists('uri');
107
+ expect(callCount, 1);
108
+ // Second method call reuses the same cached profile
109
+ await plugin.profiles.node!.exists('uri2');
110
+ expect(callCount, 1);
111
+ });
112
+ });
113
+
114
+ group('createRuntimeStoragePlugin', () {
115
+ test('creates a storage plugin with runtime profile', () {
116
+ final factory = createRuntimeStoragePlugin<String>(
117
+ name: 'testRuntime',
118
+ supportedProtocol: 'mock://',
119
+ factory: (config) => _MockRuntimeProfile(),
120
+ );
121
+ final plugin = factory('cfg');
122
+ expect(plugin.name, 'testRuntime');
123
+ expect(plugin.profiles.hasNode, isFalse);
124
+ expect(plugin.profiles.hasRuntime, isTrue);
125
+ });
126
+
127
+ test('runtime profile delegates correctly', () async {
128
+ final factory = createRuntimeStoragePlugin<String>(
129
+ name: 'testRuntime',
130
+ supportedProtocol: 'mock://',
131
+ factory: (config) => _MockRuntimeProfile(),
132
+ );
133
+ final plugin = factory('cfg');
134
+ final url = await plugin.profiles.runtime!
135
+ .getDownloadUrl('mock://file.zip');
136
+ expect(url['url'], contains('file.zip'));
137
+ });
138
+ });
139
+
140
+ group('createUniversalStoragePlugin', () {
141
+ test('creates a storage plugin with both profiles', () {
142
+ final factory = createUniversalStoragePlugin<String>(
143
+ name: 'testUniversal',
144
+ supportedProtocol: 'mock://',
145
+ factory: (config) => (
146
+ node: _MockNodeProfile(),
147
+ runtime: _MockRuntimeProfile(),
148
+ ),
149
+ );
150
+ final plugin = factory('cfg');
151
+ expect(plugin.name, 'testUniversal');
152
+ expect(plugin.profiles.hasNode, isTrue);
153
+ expect(plugin.profiles.hasRuntime, isTrue);
154
+ });
155
+
156
+ test('both profiles work independently', () async {
157
+ final factory = createUniversalStoragePlugin<String>(
158
+ name: 'testUniversal',
159
+ supportedProtocol: 'mock://',
160
+ factory: (config) => (
161
+ node: _MockNodeProfile(),
162
+ runtime: _MockRuntimeProfile(),
163
+ ),
164
+ );
165
+ final plugin = factory('cfg');
166
+
167
+ final nodeResult = await plugin.profiles.node!.upload('k', '/p');
168
+ expect(nodeResult, isNotEmpty);
169
+
170
+ final runtimeUrl = await plugin.profiles.runtime!
171
+ .getDownloadUrl('mock://file');
172
+ expect(runtimeUrl, isNotEmpty);
173
+ });
174
+
175
+ test('factory is lazy', () async {
176
+ var called = false;
177
+ final factory = createUniversalStoragePlugin<String>(
178
+ name: 'testUniversal',
179
+ supportedProtocol: 'mock://',
180
+ factory: (config) {
181
+ called = true;
182
+ return (node: _MockNodeProfile(), runtime: _MockRuntimeProfile());
183
+ },
184
+ );
185
+ final plugin = factory('cfg');
186
+ expect(called, isFalse);
187
+ // Calling a method on the node profile triggers the factory
188
+ await plugin.profiles.node!.exists('uri');
189
+ expect(called, isTrue);
190
+ });
191
+ });
192
+ }
@@ -0,0 +1,51 @@
1
+ import 'package:flutter_patcher_plugin_core/src/generate_min_bundle_id.dart';
2
+ import 'package:flutter_patcher_plugin_core/src/uuidv7.dart';
3
+ import 'package:test/test.dart';
4
+
5
+ void main() {
6
+ group('generateMinBundleId', () {
7
+ test('produces valid UUID v7 format', () {
8
+ final id = generateMinBundleId();
9
+ expect(id, matches(RegExp(
10
+ r'^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
11
+ )));
12
+ });
13
+
14
+ test('has zeroed random bits', () {
15
+ final id = generateMinBundleId();
16
+ final parts = id.split('-');
17
+ // version+rand_a should be 7000 (version 7, rand_a=0)
18
+ expect(parts[2], '7000');
19
+ // variant+rand_b starts with 8, rest is 0
20
+ expect(parts[3], '8000');
21
+ // node is all zeros
22
+ expect(parts[4], '000000000000');
23
+ });
24
+
25
+ test('timestamp is close to current time', () {
26
+ final before = DateTime.now().millisecondsSinceEpoch;
27
+ final id = generateMinBundleId();
28
+ final after = DateTime.now().millisecondsSinceEpoch;
29
+ final ts = extractTimestampFromUUIDv7(id);
30
+ expect(ts, greaterThanOrEqualTo(before));
31
+ expect(ts, lessThanOrEqualTo(after));
32
+ });
33
+
34
+ test('sorts before a normal UUIDv7 at the same millisecond', () {
35
+ // Create a min bundle ID
36
+ final minId = generateMinBundleId();
37
+ // Create a normal UUIDv7 (with random bits)
38
+ final normalId = createUUIDv7();
39
+ // Both are at approximately the same time, but min should be <= normal
40
+ // when timestamps are close
41
+ final minTs = extractTimestampFromUUIDv7(minId);
42
+ final normalTs = extractTimestampFromUUIDv7(normalId);
43
+ // If timestamps are the same, min should sort first
44
+ if (minTs == normalTs) {
45
+ expect(minId.compareTo(normalId), lessThan(0));
46
+ }
47
+ // Otherwise just verify the format is valid
48
+ expect(minId.length, 36);
49
+ });
50
+ });
51
+ }