@milaboratories/pl-middle-layer 1.46.28 → 1.46.29

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.
@@ -3,10 +3,10 @@ import { getQuickJS } from "quickjs-emscripten";
3
3
  import { expect, test } from "vitest";
4
4
  import { outputRef } from "../model/args";
5
5
  import { ProjectHelper } from "../model/project_helper";
6
- import { projectFieldName } from "../model/project_model";
6
+ import { blockArgsAuthorKey, projectFieldName } from "../model/project_model";
7
7
  import { TestBPPreparer } from "../test/block_packs";
8
8
  import { createProject, ProjectMutator } from "./project";
9
- import type { BlockPackSpec } from "@milaboratories/pl-model-middle-layer";
9
+ import type { AuthorMarker, BlockPackSpec } from "@milaboratories/pl-model-middle-layer";
10
10
  import path from "node:path";
11
11
 
12
12
  // V3 block specs - using dev-v2 type with local folders
@@ -283,3 +283,205 @@ test("v3 blocks: prerunArgs skip test", async () => {
283
283
  });
284
284
  });
285
285
  });
286
+
287
+ test("v3 blocks: migrateBlockPack preserves state and re-derives args and prerunArgs", async () => {
288
+ const quickJs = await getQuickJS();
289
+
290
+ await TestHelpers.withTempRoot(async (pl) => {
291
+ const prj = await pl.withWriteTx("CreatingProject", async (tx) => {
292
+ const prjRef = await createProject(tx);
293
+ tx.createField(field(tx.clientRoot, "prj"), "Dynamic", prjRef);
294
+ await tx.commit();
295
+ return await toGlobalResourceId(prjRef);
296
+ });
297
+
298
+ // Add enter-numbers-v3 block and set data with even numbers
299
+ await pl.withWriteTx("AddBlock", async (tx) => {
300
+ const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
301
+ mut.addBlock(
302
+ { id: "enter1", label: "Enter Numbers V3", renderingMode: "Heavy" },
303
+ {
304
+ storageMode: "fromModel",
305
+ blockPack: await TestBPPreparer.prepare(BPSpecEnterV3),
306
+ },
307
+ );
308
+ mut.setStates([
309
+ {
310
+ modelAPIVersion: 2,
311
+ blockId: "enter1",
312
+ payload: { operation: "update-data", value: { numbers: [4, 2, 6] } },
313
+ },
314
+ ]);
315
+ mut.save();
316
+ await tx.commit();
317
+ });
318
+
319
+ // Verify initial state
320
+ await poll(pl, async (tx) => {
321
+ const prjR = await tx.get(prj);
322
+
323
+ const currentArgs = await prjR.get(projectFieldName("enter1", "currentArgs"));
324
+ const argsData = JSON.parse(Buffer.from(currentArgs.data.data!).toString());
325
+ expect(argsData).toStrictEqual({ numbers: [2, 4, 6] });
326
+
327
+ const currentPrerunArgs = await prjR.get(projectFieldName("enter1", "currentPrerunArgs"));
328
+ const prerunData = JSON.parse(Buffer.from(currentPrerunArgs.data.data!).toString());
329
+ expect(prerunData).toStrictEqual({ evenNumbers: [2, 4, 6] });
330
+ });
331
+
332
+ // Call migrateBlockPack without newClearState (state-preserved path)
333
+ await pl.withWriteTx("MigrateBlockPack", async (tx) => {
334
+ const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
335
+ mut.migrateBlockPack("enter1", await TestBPPreparer.prepare(BPSpecEnterV3));
336
+ mut.save();
337
+ await tx.commit();
338
+ });
339
+
340
+ // Verify: storage preserved, args and prerunArgs re-derived
341
+ await poll(pl, async (tx) => {
342
+ const prjR = await tx.get(prj);
343
+
344
+ const blockStorage = await prjR.get(projectFieldName("enter1", "blockStorage"));
345
+ const storageData = JSON.parse(Buffer.from(blockStorage.data.data!).toString());
346
+ expect(storageData.__data).toStrictEqual({ numbers: [4, 2, 6] });
347
+
348
+ const currentArgs = await prjR.get(projectFieldName("enter1", "currentArgs"));
349
+ const argsData = JSON.parse(Buffer.from(currentArgs.data.data!).toString());
350
+ expect(argsData).toStrictEqual({ numbers: [2, 4, 6] });
351
+
352
+ const currentPrerunArgs = await prjR.get(projectFieldName("enter1", "currentPrerunArgs"));
353
+ const prerunData = JSON.parse(Buffer.from(currentPrerunArgs.data.data!).toString());
354
+ expect(prerunData).toStrictEqual({ evenNumbers: [2, 4, 6] });
355
+ });
356
+ });
357
+ });
358
+
359
+ test("v3 blocks: migrateBlockPack with storage migration re-derives args and prerunArgs", async () => {
360
+ const quickJs = await getQuickJS();
361
+
362
+ await TestHelpers.withTempRoot(async (pl) => {
363
+ const prj = await pl.withWriteTx("CreatingProject", async (tx) => {
364
+ const prjRef = await createProject(tx);
365
+ tx.createField(field(tx.clientRoot, "prj"), "Dynamic", prjRef);
366
+ await tx.commit();
367
+ return await toGlobalResourceId(prjRef);
368
+ });
369
+
370
+ // Add enter-numbers-v3 block with initial data
371
+ await pl.withWriteTx("AddBlock", async (tx) => {
372
+ const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
373
+ mut.addBlock(
374
+ { id: "enter1", label: "Enter Numbers V3", renderingMode: "Heavy" },
375
+ {
376
+ storageMode: "fromModel",
377
+ blockPack: await TestBPPreparer.prepare(BPSpecEnterV3),
378
+ },
379
+ );
380
+ mut.setStates([
381
+ {
382
+ modelAPIVersion: 2,
383
+ blockId: "enter1",
384
+ payload: { operation: "update-data", value: { numbers: [1] } },
385
+ },
386
+ ]);
387
+ mut.save();
388
+ await tx.commit();
389
+ });
390
+
391
+ // Overwrite blockStorage with v1-format data (simulating old block version)
392
+ await pl.withWriteTx("DowngradeStorage", async (tx) => {
393
+ const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
394
+ const v1Storage = JSON.stringify({
395
+ __pl_a7f3e2b9__: "v1",
396
+ __dataVersion: "v1",
397
+ __data: { numbers: [3, 1, 5] },
398
+ });
399
+ mut.setBlockStorageRaw("enter1", v1Storage);
400
+ mut.save();
401
+ await tx.commit();
402
+ });
403
+
404
+ // Call migrateBlockPack (state-preserved) — triggers v1→v2→v3 migration
405
+ await pl.withWriteTx("MigrateBlockPack", async (tx) => {
406
+ const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
407
+ mut.migrateBlockPack("enter1", await TestBPPreparer.prepare(BPSpecEnterV3));
408
+ mut.save();
409
+ await tx.commit();
410
+ });
411
+
412
+ // Verify migrated storage + re-derived args + prerunArgs
413
+ await poll(pl, async (tx) => {
414
+ const prjR = await tx.get(prj);
415
+
416
+ // Storage migrated to v3: v1→v2 sorts + adds labels, v2→v3 adds description
417
+ const blockStorage = await prjR.get(projectFieldName("enter1", "blockStorage"));
418
+ const storageData = JSON.parse(Buffer.from(blockStorage.data.data!).toString());
419
+ expect(storageData.__dataVersion).toBe("v3");
420
+ expect(storageData.__data).toStrictEqual({
421
+ numbers: [1, 3, 5],
422
+ labels: ["migrated-from-v1"],
423
+ description: "Migrated: migrated-from-v1",
424
+ });
425
+
426
+ // currentArgs derived from migrated storage: args() sorts numbers
427
+ const currentArgs = await prjR.get(projectFieldName("enter1", "currentArgs"));
428
+ const argsData = JSON.parse(Buffer.from(currentArgs.data.data!).toString());
429
+ expect(argsData).toStrictEqual({ numbers: [1, 3, 5] });
430
+
431
+ // currentPrerunArgs derived: prerunArgs() filters even numbers
432
+ const currentPrerunArgs = await prjR.get(projectFieldName("enter1", "currentPrerunArgs"));
433
+ const prerunData = JSON.parse(Buffer.from(currentPrerunArgs.data.data!).toString());
434
+ expect(prerunData).toStrictEqual({ evenNumbers: [] });
435
+ });
436
+ });
437
+ });
438
+
439
+ test("v3 blocks: migrateBlockPack assigns author marker", async () => {
440
+ const quickJs = await getQuickJS();
441
+
442
+ await TestHelpers.withTempRoot(async (pl) => {
443
+ const prj = await pl.withWriteTx("CreatingProject", async (tx) => {
444
+ const prjRef = await createProject(tx);
445
+ tx.createField(field(tx.clientRoot, "prj"), "Dynamic", prjRef);
446
+ await tx.commit();
447
+ return await toGlobalResourceId(prjRef);
448
+ });
449
+
450
+ // Add block with data
451
+ await pl.withWriteTx("AddBlock", async (tx) => {
452
+ const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
453
+ mut.addBlock(
454
+ { id: "enter1", label: "Enter Numbers V3", renderingMode: "Heavy" },
455
+ {
456
+ storageMode: "fromModel",
457
+ blockPack: await TestBPPreparer.prepare(BPSpecEnterV3),
458
+ },
459
+ );
460
+ mut.setStates([
461
+ {
462
+ modelAPIVersion: 2,
463
+ blockId: "enter1",
464
+ payload: { operation: "update-data", value: { numbers: [1, 2, 3] } },
465
+ },
466
+ ]);
467
+ mut.save();
468
+ await tx.commit();
469
+ });
470
+
471
+ // Call migrateBlockPack with an author marker
472
+ const testAuthor: AuthorMarker = { authorId: "test-author-123", localVersion: 1 };
473
+ await pl.withWriteTx("MigrateWithAuthor", async (tx) => {
474
+ const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj, testAuthor);
475
+ mut.migrateBlockPack("enter1", await TestBPPreparer.prepare(BPSpecEnterV3));
476
+ mut.save();
477
+ await tx.commit();
478
+ });
479
+
480
+ // Verify the author marker KV is set
481
+ await poll(pl, async (tx) => {
482
+ const prjR = await tx.get(prj);
483
+ const author = await prjR.getKValueObj<AuthorMarker>(blockArgsAuthorKey("enter1"));
484
+ expect(author).toStrictEqual(testAuthor);
485
+ });
486
+ });
487
+ });
@@ -1250,9 +1250,42 @@ export class ProjectMutator {
1250
1250
  );
1251
1251
  }
1252
1252
  this.setBlockStorageRaw(blockId, migrationResult.newStorageJson);
1253
+
1254
+ // Re-derive currentArgs from migrated storage (new block code + migrated data)
1255
+ const deriveArgsResult = this.projectHelper.deriveArgsFromStorage(
1256
+ newConfig,
1257
+ migrationResult.newStorageJson,
1258
+ );
1259
+ if (!deriveArgsResult.error) {
1260
+ this.setBlockFieldObj(
1261
+ blockId,
1262
+ "currentArgs",
1263
+ this.createJsonFieldValue(deriveArgsResult.value),
1264
+ );
1265
+ }
1266
+
1267
+ // Derive prerunArgs from the migrated storage so staging can re-render
1268
+ const prerunArgs = this.projectHelper.derivePrerunArgsFromStorage(
1269
+ newConfig,
1270
+ migrationResult.newStorageJson,
1271
+ );
1272
+ if (prerunArgs !== undefined) {
1273
+ this.setBlockFieldObj(
1274
+ blockId,
1275
+ "currentPrerunArgs",
1276
+ this.createJsonFieldValue(prerunArgs),
1277
+ );
1278
+ }
1279
+ }
1280
+ } else {
1281
+ // Legacy blocks (modelAPIVersion 1): prerunArgs = currentArgs
1282
+ if (info.fields.currentArgs !== undefined) {
1283
+ this.setBlockFieldObj(blockId, "currentPrerunArgs", info.fields.currentArgs);
1253
1284
  }
1254
1285
  }
1255
1286
 
1287
+ this.blocksWithChangedInputs.add(blockId);
1288
+
1256
1289
  // resetting staging outputs for all downstream blocks
1257
1290
  this.getStagingGraph().traverse("downstream", [blockId], ({ id }) => this.resetStaging(id));
1258
1291
  }