@tenonhq/dovetail-core 0.0.114 → 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.
@@ -2,16 +2,23 @@
2
2
  // Tests for stampMetadataContent — the writer that finalizes each record's
3
3
  // metaData.json before it lands on disk.
4
4
  //
5
- // Two on-disk-stability concerns are covered:
6
- // 1. _lastUpdatedOn is sourced from the record's sys_updated_on (stable across
7
- // re-pulls) rather than a fresh wall-clock stamp.
5
+ // The contract is determinism: the same record must produce the same bytes on
6
+ // any instance, from any machine, on every pull. Three on-disk-stability
7
+ // concerns are covered:
8
+ // 1. No generation-time value survives. `_lastUpdatedOn` is deleted outright —
9
+ // it duplicated sys_updated_on.value exactly, and its no-sys_updated_on
10
+ // branch stamped a wall-clock that rewrote the file on every pull.
8
11
  // 2. _record_link is rewritten to an instance-relative path. The CADSO metadata
9
12
  // endpoint returns an absolute URL (https://<instance>.service-now.com/...);
10
13
  // persisting the host makes the artifact instance-bound and dirties every
11
14
  // file on a re-pull from a different instance. Stripping any *.service-now.com
12
15
  // host keeps the diff to genuine schema changes.
16
+ // 3. display_value is stripped — it is resolved live server-side, so renaming
17
+ // a referenced record re-churns every file pointing at it.
13
18
  Object.defineProperty(exports, "__esModule", { value: true });
14
19
  const appUtils_1 = require("../appUtils");
20
+ // Matches an ISO-8601 wall-clock stamp (what `new Date().toISOString()` emits).
21
+ const ISO_STAMP = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
15
22
  const metaFile = (content) => ({
16
23
  name: "metaData",
17
24
  type: "json",
@@ -46,13 +53,13 @@ describe("stampMetadataContent — _record_link host stripping", () => {
46
53
  const out = (0, appUtils_1.stampMetadataContent)(metaFile({ _record_link: null }));
47
54
  expect(parse(out)._record_link).toBeNull();
48
55
  });
49
- it("still sources _lastUpdatedOn from sys_updated_on while stripping the host", () => {
56
+ it("strips the host while leaving the record's own sys_updated_on intact", () => {
50
57
  const out = (0, appUtils_1.stampMetadataContent)(metaFile({
51
58
  sys_updated_on: { value: "2025-08-21 17:09:02" },
52
59
  _record_link: "https://tenonworkstudio.service-now.com/incident.do?sys_id=1",
53
60
  }));
54
61
  const parsed = parse(out);
55
- expect(parsed._lastUpdatedOn).toBe("2025-08-21 17:09:02");
62
+ expect(parsed.sys_updated_on).toEqual({ value: "2025-08-21 17:09:02" });
56
63
  expect(parsed._record_link).toBe("/incident.do?sys_id=1");
57
64
  });
58
65
  it("passes through non-metaData files verbatim", () => {
@@ -60,6 +67,57 @@ describe("stampMetadataContent — _record_link host stripping", () => {
60
67
  expect((0, appUtils_1.stampMetadataContent)(script)).toEqual(script);
61
68
  });
62
69
  });
70
+ describe("stampMetadataContent — no generation-time values", () => {
71
+ it("never writes _lastUpdatedOn, even when sys_updated_on is present", () => {
72
+ const out = (0, appUtils_1.stampMetadataContent)(metaFile({ sys_updated_on: { value: "2025-08-21 17:09:02" } }));
73
+ expect(parse(out)._lastUpdatedOn).toBeUndefined();
74
+ });
75
+ it("deletes a pre-existing _lastUpdatedOn so a re-stamp settles legacy content", () => {
76
+ // A branch pulled before this change carries the key on disk. Re-stamping
77
+ // has to remove it, otherwise the stale value persists forever and the
78
+ // mirror never converges.
79
+ const out = (0, appUtils_1.stampMetadataContent)(metaFile({
80
+ sys_updated_on: { value: "2025-08-21 17:09:02" },
81
+ _lastUpdatedOn: "2025-08-21 17:09:02",
82
+ _record_link: "/incident.do?sys_id=1",
83
+ }));
84
+ const parsed = parse(out);
85
+ expect(parsed._lastUpdatedOn).toBeUndefined();
86
+ expect(parsed._record_link).toBe("/incident.do?sys_id=1");
87
+ });
88
+ it("emits no wall-clock stamp when sys_updated_on is absent", () => {
89
+ // The old fallback stamped new Date().toISOString() here, which is what
90
+ // made every pull dirty the record.
91
+ const out = (0, appUtils_1.stampMetadataContent)(metaFile({ name: { value: "no audit columns" } }));
92
+ expect(out.content).not.toMatch(ISO_STAMP);
93
+ expect(parse(out)._lastUpdatedOn).toBeUndefined();
94
+ });
95
+ it("writes the deterministic placeholder for empty content", () => {
96
+ const out = (0, appUtils_1.stampMetadataContent)({ name: "metaData", type: "json", content: "" });
97
+ expect(out.content).toBe(appUtils_1.EMPTY_METADATA_CONTENT);
98
+ expect(out.content).not.toMatch(ISO_STAMP);
99
+ });
100
+ it("produces identical bytes across repeated runs (the whole point)", () => {
101
+ const server = {
102
+ sys_updated_on: { value: "2025-08-21 17:09:02", display_value: "2025-08-21 10:09:02" },
103
+ action: { value: "abc", display_value: "Some Record Name" },
104
+ _record_link: "https://tenonworkstudio.service-now.com/incident.do?sys_id=1",
105
+ _lastUpdatedOn: "2025-08-21 17:09:02",
106
+ };
107
+ // Two independent pulls of an unchanged record, one re-stamp of the result.
108
+ const first = (0, appUtils_1.stampMetadataContent)(metaFile(server));
109
+ const second = (0, appUtils_1.stampMetadataContent)(metaFile(server));
110
+ const restamped = (0, appUtils_1.stampMetadataContent)(first);
111
+ expect(second.content).toBe(first.content);
112
+ expect(restamped.content).toBe(first.content);
113
+ });
114
+ it("leaves non-object JSON alone rather than throwing", () => {
115
+ const bare = { name: "metaData", type: "json", content: '"just a string"' };
116
+ expect((0, appUtils_1.stampMetadataContent)(bare)).toEqual(bare);
117
+ const arr = { name: "metaData", type: "json", content: "[1,2,3]" };
118
+ expect((0, appUtils_1.stampMetadataContent)(arr)).toEqual(arr);
119
+ });
120
+ });
63
121
  describe("stampMetadataContent — display_value stripping", () => {
64
122
  it("drops display_value from field pairs, keeping value", () => {
65
123
  const out = (0, appUtils_1.stampMetadataContent)(metaFile({
@@ -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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-core",
3
- "version": "0.0.114",
3
+ "version": "0.0.115",
4
4
  "description": "Next-gen file syncer",
5
5
  "license": "GPL-3.0",
6
6
  "main": "./dist/index.js",