@tenonhq/dovetail-core 0.0.113 → 0.0.115
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.
- package/dist/allScopesCommands.js +15 -8
- package/dist/appUtils.js +174 -47
- package/dist/commander.js +12 -0
- package/dist/commands.js +9 -0
- package/dist/createRecordCommand.js +26 -13
- package/dist/reconcile/baseline.js +3 -3
- package/dist/reconcile/fields.js +2 -2
- package/dist/reconcile/recordSource.js +6 -3
- package/dist/tests/allScopesMetadata.test.js +193 -0
- package/dist/tests/stampMetadataContent.test.js +63 -5
- package/dist/tests/syncManifestRecord.test.js +234 -0
- package/dist/tests/syncManifestRefresh.test.js +159 -0
- package/package.json +1 -1
- package/release-manifest.json +592 -635
|
@@ -285,3 +285,162 @@ describe("syncManifest — refresh pulls instance edits down", function () {
|
|
|
285
285
|
expect(Object.keys(missingArg.sys_script_include).sort()).toEqual(["sysid_RecA", "sysid_RecB"]);
|
|
286
286
|
});
|
|
287
287
|
});
|
|
288
|
+
/**
|
|
289
|
+
* `--metadata-only` exists so a mirror can be settled after a change to the
|
|
290
|
+
* metadata writer without the source churn `--force` produces. The guarantee it
|
|
291
|
+
* has to make is narrow and total: the resulting diff is metaData.json and
|
|
292
|
+
* nothing else — no field file, no manifest, no new record directory.
|
|
293
|
+
*/
|
|
294
|
+
describe("syncManifest — --metadata-only rewrites metadata and nothing else", function () {
|
|
295
|
+
beforeEach(function () {
|
|
296
|
+
jest.clearAllMocks();
|
|
297
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sinc-metaonly-test-"));
|
|
298
|
+
mockConfig.getSourcePathForScope.mockReturnValue(tmpRoot);
|
|
299
|
+
mockConfig.getSourcePath.mockReturnValue(tmpRoot);
|
|
300
|
+
});
|
|
301
|
+
afterEach(function () {
|
|
302
|
+
try {
|
|
303
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
304
|
+
}
|
|
305
|
+
catch (e) { }
|
|
306
|
+
});
|
|
307
|
+
function respondWith(record, files) {
|
|
308
|
+
var records = {};
|
|
309
|
+
records[record] = { name: record, sys_id: "sysid_" + record, files: files };
|
|
310
|
+
mockClient.getMissingFiles.mockResolvedValue({
|
|
311
|
+
sys_script_include: { records: records },
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
test("rewrites metaData.json even though no field file changed", async function () {
|
|
315
|
+
var identical = "var same = true;";
|
|
316
|
+
writeLocal("Rec", "script", "js", identical);
|
|
317
|
+
mockClient.getManifest.mockResolvedValue(setupScope([{ record: "Rec", name: "script", type: "js", content: "" }]));
|
|
318
|
+
respondWith("Rec", [
|
|
319
|
+
{ name: "script", type: "js", content: identical },
|
|
320
|
+
{
|
|
321
|
+
name: "metaData",
|
|
322
|
+
type: "json",
|
|
323
|
+
content: JSON.stringify({
|
|
324
|
+
sys_updated_on: { value: "2026-04-29 14:38:51", display_value: "2026-04-29 07:38:51" },
|
|
325
|
+
_record_link: "https://tenonworkstudio.service-now.com/sys_script_include.do?sys_id=1",
|
|
326
|
+
_lastUpdatedOn: "2026-04-29 14:38:51",
|
|
327
|
+
}),
|
|
328
|
+
},
|
|
329
|
+
]);
|
|
330
|
+
await AppUtils.syncManifest("x_cadso_core", { metadataOnly: true });
|
|
331
|
+
var meta = readLocal("Rec", "metaData", "json");
|
|
332
|
+
expect(meta).not.toBeNull();
|
|
333
|
+
var parsed = JSON.parse(meta);
|
|
334
|
+
// Normalized on the way in — the whole reason to regenerate.
|
|
335
|
+
expect(parsed._record_link).toBe("/sys_script_include.do?sys_id=1");
|
|
336
|
+
expect(parsed._lastUpdatedOn).toBeUndefined();
|
|
337
|
+
expect(meta).not.toContain("display_value");
|
|
338
|
+
});
|
|
339
|
+
test("does not touch field files, even when the instance content differs", async function () {
|
|
340
|
+
writeLocal("Rec", "script", "js", "var local = 1;");
|
|
341
|
+
var beforeMtime = statMtime("Rec", "script", "js");
|
|
342
|
+
mockClient.getManifest.mockResolvedValue(setupScope([{ record: "Rec", name: "script", type: "js", content: "" }]));
|
|
343
|
+
respondWith("Rec", [
|
|
344
|
+
{ name: "script", type: "js", content: "var fromInstance = 2;" },
|
|
345
|
+
{ name: "metaData", type: "json", content: JSON.stringify({ sys_id: { value: "1" } }) },
|
|
346
|
+
]);
|
|
347
|
+
await new Promise(function (r) { setTimeout(r, 10); });
|
|
348
|
+
await AppUtils.syncManifest("x_cadso_core", { metadataOnly: true });
|
|
349
|
+
// Unchanged on disk AND untouched — a rewrite with identical bytes would
|
|
350
|
+
// still be a write, and this mode promises none.
|
|
351
|
+
expect(readLocal("Rec", "script", "js")).toBe("var local = 1;");
|
|
352
|
+
expect(statMtime("Rec", "script", "js")).toBe(beforeMtime);
|
|
353
|
+
});
|
|
354
|
+
test("skips records that are not checked out locally (no orphan directories)", async function () {
|
|
355
|
+
// Writing metaData for a record with no directory would leave a folder
|
|
356
|
+
// holding a lone metaData.json and zero field files.
|
|
357
|
+
//
|
|
358
|
+
// The absent-record check MUST NOT throw. syncManifest swallows errors into
|
|
359
|
+
// logger.error, so an exception here would abort the whole scope while this
|
|
360
|
+
// test's file assertions still passed — "no directory, no metaData" is
|
|
361
|
+
// trivially true when nothing ran. The logger.error assertion is what makes
|
|
362
|
+
// this test real: it caught fUtils.isDirectory rejecting on ENOENT.
|
|
363
|
+
mockClient.getManifest.mockResolvedValue(setupScope([{ record: "NotOnDisk", name: "script", type: "js", content: "" }]));
|
|
364
|
+
respondWith("NotOnDisk", [
|
|
365
|
+
{ name: "script", type: "js", content: "// new on the instance" },
|
|
366
|
+
{ name: "metaData", type: "json", content: JSON.stringify({ sys_id: { value: "1" } }) },
|
|
367
|
+
]);
|
|
368
|
+
await AppUtils.syncManifest("x_cadso_core", { metadataOnly: true });
|
|
369
|
+
expect(mockLogger.error).not.toHaveBeenCalled();
|
|
370
|
+
expect(fs.existsSync(path.join(tmpRoot, "sys_script_include", "NotOnDisk"))).toBe(false);
|
|
371
|
+
expect(readLocal("NotOnDisk", "metaData", "json")).toBeNull();
|
|
372
|
+
});
|
|
373
|
+
test("an absent record does not stop the records that ARE checked out", async function () {
|
|
374
|
+
// The real shape of a metadata-only run: the instance holds records this
|
|
375
|
+
// branch never pulled, interleaved with ones it did. An abort on the first
|
|
376
|
+
// absent record would silently settle only part of the mirror.
|
|
377
|
+
writeLocal("OnDisk", "script", "js", "var x = 1;");
|
|
378
|
+
mockClient.getManifest.mockResolvedValue(setupScope([
|
|
379
|
+
{ record: "Absent", name: "script", type: "js", content: "" },
|
|
380
|
+
{ record: "OnDisk", name: "script", type: "js", content: "" },
|
|
381
|
+
]));
|
|
382
|
+
mockClient.getMissingFiles.mockResolvedValue({
|
|
383
|
+
sys_script_include: {
|
|
384
|
+
records: {
|
|
385
|
+
Absent: {
|
|
386
|
+
name: "Absent",
|
|
387
|
+
sys_id: "sysid_Absent",
|
|
388
|
+
files: [
|
|
389
|
+
{ name: "script", type: "js", content: "// only on the instance" },
|
|
390
|
+
{ name: "metaData", type: "json", content: JSON.stringify({ sys_id: { value: "a" } }) },
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
OnDisk: {
|
|
394
|
+
name: "OnDisk",
|
|
395
|
+
sys_id: "sysid_OnDisk",
|
|
396
|
+
files: [
|
|
397
|
+
{ name: "script", type: "js", content: "var x = 1;" },
|
|
398
|
+
{
|
|
399
|
+
name: "metaData",
|
|
400
|
+
type: "json",
|
|
401
|
+
content: JSON.stringify({
|
|
402
|
+
sys_id: { value: "b" },
|
|
403
|
+
_record_link: "https://tenonworkstudio.service-now.com/x.do?sys_id=b",
|
|
404
|
+
}),
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
await AppUtils.syncManifest("x_cadso_core", { metadataOnly: true });
|
|
412
|
+
expect(mockLogger.error).not.toHaveBeenCalled();
|
|
413
|
+
// The absent one stayed absent...
|
|
414
|
+
expect(fs.existsSync(path.join(tmpRoot, "sys_script_include", "Absent"))).toBe(false);
|
|
415
|
+
// ...and the present one was still settled.
|
|
416
|
+
var meta = readLocal("OnDisk", "metaData", "json");
|
|
417
|
+
expect(meta).not.toBeNull();
|
|
418
|
+
expect(JSON.parse(meta)._record_link).toBe("/x.do?sys_id=b");
|
|
419
|
+
});
|
|
420
|
+
test("does not persist the manifest", async function () {
|
|
421
|
+
writeLocal("Rec", "script", "js", "var x = 1;");
|
|
422
|
+
mockClient.getManifest.mockResolvedValue(setupScope([{ record: "Rec", name: "script", type: "js", content: "" }]));
|
|
423
|
+
respondWith("Rec", [
|
|
424
|
+
{ name: "script", type: "js", content: "var x = 1;" },
|
|
425
|
+
{ name: "metaData", type: "json", content: JSON.stringify({ sys_id: { value: "1" } }) },
|
|
426
|
+
]);
|
|
427
|
+
await AppUtils.syncManifest("x_cadso_core", { metadataOnly: true });
|
|
428
|
+
// A manifest write would put record adds/drops/renames into a diff that is
|
|
429
|
+
// supposed to be metaData.json only.
|
|
430
|
+
expect(mockConfig.updateManifest).not.toHaveBeenCalled();
|
|
431
|
+
});
|
|
432
|
+
test("a plain refresh still persists the manifest (the gate is scoped)", async function () {
|
|
433
|
+
writeLocal("Rec", "script", "js", "var x = 1;");
|
|
434
|
+
mockClient.getManifest.mockResolvedValue(setupScope([{ record: "Rec", name: "script", type: "js", content: "" }]));
|
|
435
|
+
respondWith("Rec", [{ name: "script", type: "js", content: "var x = 1;" }]);
|
|
436
|
+
await AppUtils.syncManifest("x_cadso_core");
|
|
437
|
+
expect(mockConfig.updateManifest).toHaveBeenCalled();
|
|
438
|
+
});
|
|
439
|
+
test("writes the deterministic placeholder when the server sends no metadata", async function () {
|
|
440
|
+
writeLocal("Rec", "script", "js", "var x = 1;");
|
|
441
|
+
mockClient.getManifest.mockResolvedValue(setupScope([{ record: "Rec", name: "script", type: "js", content: "" }]));
|
|
442
|
+
respondWith("Rec", [{ name: "script", type: "js", content: "var x = 1;" }]);
|
|
443
|
+
await AppUtils.syncManifest("x_cadso_core", { metadataOnly: true });
|
|
444
|
+
expect(readLocal("Rec", "metaData", "json")).toBe(AppUtils.EMPTY_METADATA_CONTENT);
|
|
445
|
+
});
|
|
446
|
+
});
|