@ftschopp/dynatable-migrations 1.2.4 → 1.2.6

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,cAAc,SAAS,CAAC;AAGxB,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAGrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAGlE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,cAAc,SAAS,CAAC;AAGxB,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,4BAA4B,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAGrF,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAGlE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
18
18
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
19
19
  };
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.initProject = exports.showStatus = exports.rollbackMigrations = exports.runMigrations = exports.createMigration = exports.generateMigrationTemplate = exports.createDynamoDBClient = exports.loadConfig = exports.ConfigLoader = exports.MigrationRunner = exports.MigrationLoader = exports.DynamoDBMigrationTracker = void 0;
21
+ exports.initProject = exports.showStatus = exports.rollbackMigrations = exports.runMigrations = exports.createMigration = exports.generateMigrationTemplate = exports.MigrationLockLostError = exports.MigrationAlreadyAppliedError = exports.createDynamoDBClient = exports.loadConfig = exports.ConfigLoader = exports.MigrationRunner = exports.MigrationLoader = exports.DynamoDBMigrationTracker = void 0;
22
22
  // Types
23
23
  __exportStar(require("./types"), exports);
24
24
  // Core
@@ -33,6 +33,9 @@ Object.defineProperty(exports, "ConfigLoader", { enumerable: true, get: function
33
33
  Object.defineProperty(exports, "loadConfig", { enumerable: true, get: function () { return config_1.loadConfig; } });
34
34
  var client_1 = require("./core/client");
35
35
  Object.defineProperty(exports, "createDynamoDBClient", { enumerable: true, get: function () { return client_1.createDynamoDBClient; } });
36
+ var errors_1 = require("./core/errors");
37
+ Object.defineProperty(exports, "MigrationAlreadyAppliedError", { enumerable: true, get: function () { return errors_1.MigrationAlreadyAppliedError; } });
38
+ Object.defineProperty(exports, "MigrationLockLostError", { enumerable: true, get: function () { return errors_1.MigrationLockLostError; } });
36
39
  // Template
37
40
  var migration_1 = require("./templates/migration");
38
41
  Object.defineProperty(exports, "generateMigrationTemplate", { enumerable: true, get: function () { return migration_1.generateMigrationTemplate; } });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;;;;;;;;;;;;;AAEH,QAAQ;AACR,0CAAwB;AAExB,OAAO;AACP,0CAA0D;AAAjD,mHAAA,wBAAwB,OAAA;AACjC,wCAAgD;AAAvC,yGAAA,eAAe,OAAA;AACxB,wCAAiE;AAAxD,yGAAA,eAAe,OAAA;AACxB,wCAAyD;AAAhD,sGAAA,YAAY,OAAA;AAAE,oGAAA,UAAU,OAAA;AACjC,wCAAqD;AAA5C,8GAAA,oBAAoB,OAAA;AAE7B,WAAW;AACX,mDAAkE;AAAzD,sHAAA,yBAAyB,OAAA;AAElC,kCAAkC;AAClC,4CAAoD;AAA3C,yGAAA,eAAe,OAAA;AACxB,oCAA8C;AAArC,mGAAA,aAAa,OAAA;AACtB,wCAAqD;AAA5C,0GAAA,kBAAkB,OAAA;AAC3B,4CAA+C;AAAtC,oGAAA,UAAU,OAAA;AACnB,wCAA8C;AAArC,mGAAA,WAAW,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;;;;;;;;;;;;;AAEH,QAAQ;AACR,0CAAwB;AAExB,OAAO;AACP,0CAA0D;AAAjD,mHAAA,wBAAwB,OAAA;AACjC,wCAAgD;AAAvC,yGAAA,eAAe,OAAA;AACxB,wCAAiE;AAAxD,yGAAA,eAAe,OAAA;AACxB,wCAAyD;AAAhD,sGAAA,YAAY,OAAA;AAAE,oGAAA,UAAU,OAAA;AACjC,wCAAqD;AAA5C,8GAAA,oBAAoB,OAAA;AAC7B,wCAAqF;AAA5E,sHAAA,4BAA4B,OAAA;AAAE,gHAAA,sBAAsB,OAAA;AAE7D,WAAW;AACX,mDAAkE;AAAzD,sHAAA,yBAAyB,OAAA;AAElC,kCAAkC;AAClC,4CAAoD;AAA3C,yGAAA,eAAe,OAAA;AACxB,oCAA8C;AAArC,mGAAA,aAAa,OAAA;AACtB,wCAAqD;AAA5C,0GAAA,kBAAkB,OAAA;AAC3B,4CAA+C;AAAtC,oGAAA,UAAU,OAAA;AACnB,wCAA8C;AAArC,mGAAA,WAAW,OAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ftschopp/dynatable-migrations",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "DynamoDB migration tool for single table design with schema versioning",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,45 @@
1
+ import { parsePositiveInt } from './cli-utils';
2
+
3
+ describe('parsePositiveInt', () => {
4
+ const parser = parsePositiveInt('limit');
5
+
6
+ test('accepts positive integers', () => {
7
+ expect(parser('1')).toBe(1);
8
+ expect(parser('42')).toBe(42);
9
+ expect(parser('1000')).toBe(1000);
10
+ });
11
+
12
+ test('trims surrounding whitespace before parsing', () => {
13
+ expect(parser(' 5 ')).toBe(5);
14
+ });
15
+
16
+ test('rejects 0', () => {
17
+ expect(() => parser('0')).toThrow(/--limit must be a positive integer/);
18
+ });
19
+
20
+ test('rejects negative numbers', () => {
21
+ expect(() => parser('-5')).toThrow(/--limit must be a positive integer/);
22
+ });
23
+
24
+ test('rejects non-numeric input', () => {
25
+ expect(() => parser('abc')).toThrow(/--limit must be a positive integer/);
26
+ });
27
+
28
+ test('rejects floats (1.5 would parseInt to 1, but the trailing decimals are still wrong)', () => {
29
+ expect(() => parser('1.5')).toThrow(/--limit must be a positive integer/);
30
+ });
31
+
32
+ test('rejects empty string', () => {
33
+ expect(() => parser('')).toThrow(/--limit must be a positive integer/);
34
+ });
35
+
36
+ test('rejects strings with trailing junk that parseInt would silently swallow', () => {
37
+ // parseInt('5abc', 10) === 5, but we should refuse it because the input
38
+ // is clearly not a positive integer.
39
+ expect(() => parser('5abc')).toThrow(/--limit must be a positive integer/);
40
+ });
41
+
42
+ test('error mentions the flag name passed to the factory', () => {
43
+ expect(() => parsePositiveInt('steps')('-1')).toThrow(/--steps/);
44
+ });
45
+ });
@@ -0,0 +1,18 @@
1
+ import { InvalidArgumentError } from 'commander';
2
+
3
+ /**
4
+ * Commander value parser that accepts only positive integers. Used for
5
+ * `--limit` and `--steps` so `0`, negative numbers, and non-numeric input
6
+ * fail at parse time with a clean usage error instead of silently feeding
7
+ * `Array.prototype.slice` and producing surprising rollbacks.
8
+ */
9
+ export function parsePositiveInt(name: string): (raw: string) => number {
10
+ return (raw: string) => {
11
+ const trimmed = raw.trim();
12
+ const n = Number.parseInt(trimmed, 10);
13
+ if (!Number.isInteger(n) || n <= 0 || String(n) !== trimmed) {
14
+ throw new InvalidArgumentError(`--${name} must be a positive integer (got "${raw}").`);
15
+ }
16
+ return n;
17
+ };
18
+ }
package/src/cli.ts CHANGED
@@ -7,6 +7,7 @@ import { runMigrations } from './commands/up';
7
7
  import { rollbackMigrations } from './commands/down';
8
8
  import { showStatus } from './commands/status';
9
9
  import { initProject } from './commands/init';
10
+ import { parsePositiveInt } from './cli-utils';
10
11
 
11
12
  // Read version from package.json
12
13
  const packageJson = require('../package.json');
@@ -62,13 +63,12 @@ program
62
63
  .command('up')
63
64
  .description('Run pending migrations')
64
65
  .option('-c, --config <path>', 'Path to config file')
65
- .option('-l, --limit <number>', 'Limit number of migrations to run')
66
+ .option('-l, --limit <number>', 'Limit number of migrations to run', parsePositiveInt('limit'))
66
67
  .option('-d, --dry-run', 'Show what would be done without making changes')
67
68
  .action(async (options) => {
68
69
  try {
69
70
  const config = await loadConfig(options.config);
70
- const limit = options.limit ? parseInt(options.limit, 10) : undefined;
71
- await runMigrations(config, limit, options.dryRun);
71
+ await runMigrations(config, options.limit, options.dryRun);
72
72
  } catch (error: any) {
73
73
  console.error(`Error: ${error.message}`);
74
74
  process.exit(1);
@@ -80,13 +80,12 @@ program
80
80
  .command('down')
81
81
  .description('Rollback migrations')
82
82
  .option('-c, --config <path>', 'Path to config file')
83
- .option('-s, --steps <number>', 'Number of migrations to rollback', '1')
83
+ .option('-s, --steps <number>', 'Number of migrations to rollback', parsePositiveInt('steps'), 1)
84
84
  .option('-d, --dry-run', 'Show what would be done without making changes')
85
85
  .action(async (options) => {
86
86
  try {
87
87
  const config = await loadConfig(options.config);
88
- const steps = parseInt(options.steps, 10);
89
- await rollbackMigrations(config, steps, options.dryRun);
88
+ await rollbackMigrations(config, options.steps, options.dryRun);
90
89
  } catch (error: any) {
91
90
  console.error(`Error: ${error.message}`);
92
91
  process.exit(1);
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Thrown by `markAsApplied` when the version already has a tracking record
3
+ * in a non-applied state (e.g. `failed`, `rolled_back`) and therefore
4
+ * cannot be silently treated as an idempotent re-apply. Surfaces the
5
+ * existing state so the caller can decide whether to retry, recover, or
6
+ * roll back.
7
+ */
8
+ export class MigrationAlreadyAppliedError extends Error {
9
+ public readonly version: string;
10
+ public readonly currentStatus: string | undefined;
11
+
12
+ constructor(version: string, currentStatus: string | undefined) {
13
+ super(
14
+ `Migration "${version}" already has a tracking record in state ` +
15
+ `"${currentStatus ?? 'unknown'}" — cannot mark as applied. ` +
16
+ `If you want to re-apply this version, roll it back first ` +
17
+ `(or recover the failed run) and try again.`
18
+ );
19
+ this.name = 'MigrationAlreadyAppliedError';
20
+ this.version = version;
21
+ this.currentStatus = currentStatus;
22
+ if (typeof (Error as unknown as { captureStackTrace?: unknown }).captureStackTrace === 'function') {
23
+ (
24
+ Error as unknown as { captureStackTrace: (target: object, ctor: unknown) => void }
25
+ ).captureStackTrace(this, MigrationAlreadyAppliedError);
26
+ }
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Thrown when a tracker write is cancelled because the migration lock was
32
+ * taken by another process between `acquireLock()` and the write itself
33
+ * (e.g. the original lock TTL expired and a second worker took over).
34
+ *
35
+ * The transaction was rolled back atomically — no partial state was
36
+ * written. The safe action is to stop the current `up()` / `down()` run.
37
+ */
38
+ export class MigrationLockLostError extends Error {
39
+ public readonly version: string;
40
+
41
+ constructor(version: string) {
42
+ super(
43
+ `Migration lock was lost during markAsApplied("${version}"). ` +
44
+ `Another worker may have taken over. The transaction was rolled ` +
45
+ `back atomically; no partial state was written.`
46
+ );
47
+ this.name = 'MigrationLockLostError';
48
+ this.version = version;
49
+ if (typeof (Error as unknown as { captureStackTrace?: unknown }).captureStackTrace === 'function') {
50
+ (
51
+ Error as unknown as { captureStackTrace: (target: object, ctor: unknown) => void }
52
+ ).captureStackTrace(this, MigrationLockLostError);
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,51 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3
+ import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
4
+ import { MigrationRunner } from './runner';
5
+ import type { MigrationConfig } from '../types';
6
+
7
+ const baseConfig: MigrationConfig = {
8
+ tableName: 'TestTable',
9
+ client: { region: 'us-east-1' },
10
+ };
11
+
12
+ function makeRunner() {
13
+ const client = DynamoDBDocumentClient.from(new DynamoDBClient({ region: 'us-east-1' }));
14
+ return new MigrationRunner(client, baseConfig);
15
+ }
16
+
17
+ describe('MigrationRunner.up - input validation', () => {
18
+ test('throws when limit is 0', async () => {
19
+ await expect(makeRunner().up({ limit: 0 })).rejects.toThrow(/positive integer/);
20
+ });
21
+
22
+ test('throws when limit is negative', async () => {
23
+ await expect(makeRunner().up({ limit: -3 })).rejects.toThrow(/positive integer/);
24
+ });
25
+
26
+ test('throws when limit is NaN', async () => {
27
+ await expect(makeRunner().up({ limit: NaN })).rejects.toThrow(/positive integer/);
28
+ });
29
+
30
+ test('throws when limit is a non-integer (1.5)', async () => {
31
+ await expect(makeRunner().up({ limit: 1.5 })).rejects.toThrow(/positive integer/);
32
+ });
33
+ });
34
+
35
+ describe('MigrationRunner.down - input validation', () => {
36
+ test('throws when steps is 0', async () => {
37
+ await expect(makeRunner().down(0)).rejects.toThrow(/positive integer/);
38
+ });
39
+
40
+ test('throws when steps is negative', async () => {
41
+ await expect(makeRunner().down(-5)).rejects.toThrow(/positive integer/);
42
+ });
43
+
44
+ test('throws when steps is NaN', async () => {
45
+ await expect(makeRunner().down(Number.NaN)).rejects.toThrow(/positive integer/);
46
+ });
47
+
48
+ test('throws when steps is a non-integer', async () => {
49
+ await expect(makeRunner().down(2.5)).rejects.toThrow(/positive integer/);
50
+ });
51
+ });
@@ -48,6 +48,12 @@ export class MigrationRunner {
48
48
  async up(options: RunOptions = {}): Promise<MigrationFile[]> {
49
49
  const { limit, dryRun = false } = options;
50
50
 
51
+ if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {
52
+ throw new Error(
53
+ `runner.up({ limit }) requires a positive integer (got ${JSON.stringify(limit)}).`
54
+ );
55
+ }
56
+
51
57
  await this.initialize();
52
58
 
53
59
  // Acquire lock unless dry run
@@ -149,6 +155,12 @@ export class MigrationRunner {
149
155
  * Rollback last migration
150
156
  */
151
157
  async down(steps: number = 1, dryRun: boolean = false): Promise<MigrationFile[]> {
158
+ if (!Number.isInteger(steps) || steps <= 0) {
159
+ throw new Error(
160
+ `runner.down(steps) requires a positive integer (got ${JSON.stringify(steps)}).`
161
+ );
162
+ }
163
+
152
164
  await this.initialize();
153
165
 
154
166
  // Acquire lock unless dry run
@@ -148,3 +148,134 @@ describe('DynamoDBMigrationTracker - tracker writes are gated by lock ownership'
148
148
  expect(put.Put.ConditionExpression).toBe('attribute_not_exists(SK)');
149
149
  });
150
150
  });
151
+
152
+ describe('DynamoDBMigrationTracker - markAsApplied idempotency (#10)', () => {
153
+ function cancelledTransactError(reasons: Array<{ Code?: string }>) {
154
+ return Object.assign(new Error('Transaction cancelled'), {
155
+ name: 'TransactionCanceledException',
156
+ CancellationReasons: reasons,
157
+ });
158
+ }
159
+
160
+ test('returns silently when the cancellation is because the row is already in status=applied', async () => {
161
+ ddbMock.on(PutCommand).resolves({}); // acquireLock
162
+ // First getMigration → no record (so we take the Put branch)
163
+ // Second getMigration (after cancellation) → already applied
164
+ ddbMock
165
+ .on(GetCommand)
166
+ .resolvesOnce({ Item: undefined })
167
+ .resolvesOnce({
168
+ Item: {
169
+ PK: '_SCHEMA#VERSION',
170
+ SK: '0.1.0',
171
+ version: '0.1.0',
172
+ status: 'applied',
173
+ name: 'init',
174
+ },
175
+ });
176
+
177
+ // Migration row condition fails (reasons[1]); lock check passed (reasons[0] = None)
178
+ ddbMock.on(TransactWriteCommand).rejects(
179
+ cancelledTransactError([{ Code: 'None' }, { Code: 'ConditionalCheckFailed' }])
180
+ );
181
+
182
+ const tracker = makeTracker();
183
+ await tracker.acquireLock();
184
+
185
+ await expect(tracker.markAsApplied('0.1.0', 'init')).resolves.toBeUndefined();
186
+ });
187
+
188
+ test('throws MigrationAlreadyAppliedError when the row exists in a non-applied state', async () => {
189
+ ddbMock.on(PutCommand).resolves({});
190
+ ddbMock
191
+ .on(GetCommand)
192
+ .resolvesOnce({ Item: undefined })
193
+ .resolvesOnce({
194
+ Item: {
195
+ PK: '_SCHEMA#VERSION',
196
+ SK: '0.1.0',
197
+ version: '0.1.0',
198
+ status: 'failed',
199
+ name: 'init',
200
+ },
201
+ });
202
+
203
+ ddbMock.on(TransactWriteCommand).rejects(
204
+ cancelledTransactError([{ Code: 'None' }, { Code: 'ConditionalCheckFailed' }])
205
+ );
206
+
207
+ const tracker = makeTracker();
208
+ await tracker.acquireLock();
209
+
210
+ await expect(tracker.markAsApplied('0.1.0', 'init')).rejects.toMatchObject({
211
+ name: 'MigrationAlreadyAppliedError',
212
+ version: '0.1.0',
213
+ currentStatus: 'failed',
214
+ });
215
+ });
216
+
217
+ test('throws MigrationLockLostError when the lock-row ConditionCheck is what failed', async () => {
218
+ ddbMock.on(PutCommand).resolves({});
219
+ ddbMock.on(GetCommand).resolves({ Item: undefined });
220
+
221
+ ddbMock.on(TransactWriteCommand).rejects(
222
+ cancelledTransactError([{ Code: 'ConditionalCheckFailed' }, { Code: 'None' }])
223
+ );
224
+
225
+ const tracker = makeTracker();
226
+ await tracker.acquireLock();
227
+
228
+ await expect(tracker.markAsApplied('0.1.0', 'init')).rejects.toMatchObject({
229
+ name: 'MigrationLockLostError',
230
+ version: '0.1.0',
231
+ });
232
+ });
233
+
234
+ test('re-throws unexpected errors unchanged', async () => {
235
+ ddbMock.on(PutCommand).resolves({});
236
+ ddbMock.on(GetCommand).resolves({ Item: undefined });
237
+
238
+ const boom = Object.assign(new Error('upstream blew up'), { name: 'InternalServerError' });
239
+ ddbMock.on(TransactWriteCommand).rejects(boom);
240
+
241
+ const tracker = makeTracker();
242
+ await tracker.acquireLock();
243
+
244
+ await expect(tracker.markAsApplied('0.1.0', 'init')).rejects.toBe(boom);
245
+ });
246
+
247
+ test('idempotent path also covers the Update branch (existing record was rolled_back, applied by a racing worker)', async () => {
248
+ ddbMock.on(PutCommand).resolves({});
249
+ // First getMigration → existing rolled_back record (so we take the Update branch)
250
+ // Second getMigration (after cancellation) → already applied (race)
251
+ ddbMock
252
+ .on(GetCommand)
253
+ .resolvesOnce({
254
+ Item: {
255
+ PK: '_SCHEMA#VERSION',
256
+ SK: '0.1.0',
257
+ version: '0.1.0',
258
+ status: 'rolled_back',
259
+ name: 'init',
260
+ },
261
+ })
262
+ .resolvesOnce({
263
+ Item: {
264
+ PK: '_SCHEMA#VERSION',
265
+ SK: '0.1.0',
266
+ version: '0.1.0',
267
+ status: 'applied',
268
+ name: 'init',
269
+ },
270
+ });
271
+
272
+ ddbMock.on(TransactWriteCommand).rejects(
273
+ cancelledTransactError([{ Code: 'None' }, { Code: 'ConditionalCheckFailed' }])
274
+ );
275
+
276
+ const tracker = makeTracker();
277
+ await tracker.acquireLock();
278
+
279
+ await expect(tracker.markAsApplied('0.1.0', 'init')).resolves.toBeUndefined();
280
+ });
281
+ });