@tenonhq/dovetail-core 0.0.114 → 0.0.116

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.
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ /**
3
+ * Regression: `dove refresh --metadata-only` must not write the scope manifest.
4
+ *
5
+ * The mode's contract — stated in its CLI help and in CLAUDE.md — is that the
6
+ * resulting git diff is `metaData.json` and nothing else. The first
7
+ * implementation gated `ConfigManager.updateManifest` (the in-MEMORY manifest
8
+ * update) but not `fUtils.writeScopeManifest` (the actual FILE write), so a real
9
+ * run against tenonworkstudio rewrote all 8 `dove.manifest.x_cadso_*.json`
10
+ * files with unrelated record-name disambiguation drift.
11
+ *
12
+ * The companion tests in syncManifestRefresh.test.ts could not catch this: that
13
+ * suite mocks `writeScopeManifest` to a no-op, so nothing there ever observes
14
+ * the write. That mock is why the bug shipped.
15
+ *
16
+ * So this file deliberately leaves FileUtils REAL and asserts against the
17
+ * filesystem — the manifest file on disk must be byte-identical after a
18
+ * metadata-only run, and must still be updated by a plain refresh (proving the
19
+ * gate is scoped, not a blanket disable).
20
+ */
21
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ var desc = Object.getOwnPropertyDescriptor(m, k);
24
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
25
+ desc = { enumerable: true, get: function() { return m[k]; } };
26
+ }
27
+ Object.defineProperty(o, k2, desc);
28
+ }) : (function(o, m, k, k2) {
29
+ if (k2 === undefined) k2 = k;
30
+ o[k2] = m[k];
31
+ }));
32
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
33
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
34
+ }) : function(o, v) {
35
+ o["default"] = v;
36
+ });
37
+ var __importStar = (this && this.__importStar) || (function () {
38
+ var ownKeys = function(o) {
39
+ ownKeys = Object.getOwnPropertyNames || function (o) {
40
+ var ar = [];
41
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
42
+ return ar;
43
+ };
44
+ return ownKeys(o);
45
+ };
46
+ return function (mod) {
47
+ if (mod && mod.__esModule) return mod;
48
+ var result = {};
49
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
50
+ __setModuleDefault(result, mod);
51
+ return result;
52
+ };
53
+ })();
54
+ Object.defineProperty(exports, "__esModule", { value: true });
55
+ const fs = __importStar(require("fs"));
56
+ const os = __importStar(require("os"));
57
+ const path = __importStar(require("path"));
58
+ // ---------- mocks (must come before importing the module under test) ----------
59
+ var mockLogger = {
60
+ info: jest.fn(),
61
+ debug: jest.fn(),
62
+ warn: jest.fn(),
63
+ error: jest.fn(),
64
+ success: jest.fn(),
65
+ getLogLevel: function () { return "warn"; },
66
+ };
67
+ var mockFileLogger = {
68
+ debug: jest.fn(),
69
+ info: jest.fn(),
70
+ warn: jest.fn(),
71
+ error: jest.fn(),
72
+ };
73
+ var mockClient = {
74
+ getManifest: jest.fn(),
75
+ getMissingFiles: jest.fn(),
76
+ };
77
+ jest.mock("../Logger", function () { return { logger: mockLogger }; });
78
+ jest.mock("../FileLogger", function () { return { fileLogger: mockFileLogger }; });
79
+ jest.mock("../snClient", function () {
80
+ return {
81
+ defaultClient: function () { return mockClient; },
82
+ unwrapSNResponse: function (p) { return Promise.resolve(p); },
83
+ processPushResponse: jest.fn(),
84
+ retryOnErr: jest.fn(),
85
+ retryOnHttpErr: jest.fn(),
86
+ unwrapTableAPIFirstItem: jest.fn(),
87
+ };
88
+ });
89
+ var manifestPath = "";
90
+ var mockConfig = {
91
+ getConfig: jest.fn().mockReturnValue({
92
+ scopes: { x_cadso_core: {} },
93
+ tableOptions: {},
94
+ }),
95
+ getManifest: jest.fn().mockResolvedValue({
96
+ x_cadso_core: { scope: "x_cadso_core", tables: {} },
97
+ }),
98
+ getSourcePathForScope: jest.fn(),
99
+ getSourcePath: jest.fn(),
100
+ getManifestPath: jest.fn(function () { return manifestPath; }),
101
+ // The real writeScopeManifest resolves its target through this.
102
+ getScopeManifestPath: jest.fn(function () { return manifestPath; }),
103
+ resolveConfigForScope: jest.fn().mockReturnValue({
104
+ tables: ["sys_script_include"],
105
+ fieldOverrides: {},
106
+ apiIncludes: {},
107
+ apiExcludes: {},
108
+ }),
109
+ isMultiScopeManifest: jest.fn().mockReturnValue(true),
110
+ updateManifest: jest.fn(),
111
+ };
112
+ jest.mock("../config", function () { return mockConfig; });
113
+ jest.mock("progress", function () {
114
+ return jest.fn().mockImplementation(function () {
115
+ return { tick: jest.fn() };
116
+ });
117
+ });
118
+ // NOTE: FileUtils is deliberately NOT mocked. Stubbing writeScopeManifest is
119
+ // exactly what hid this bug.
120
+ const AppUtils = __importStar(require("../appUtils"));
121
+ // ---------- scaffolding ----------
122
+ var tmpRoot;
123
+ var SENTINEL = JSON.stringify({ tables: { sentinel: "untouched" } }, null, 2);
124
+ function writeLocalRecord(record, content) {
125
+ var recDir = path.join(tmpRoot, "sys_script_include", record);
126
+ fs.mkdirSync(recDir, { recursive: true });
127
+ fs.writeFileSync(path.join(recDir, "script.js"), content);
128
+ }
129
+ function serverResponse(record, scriptContent) {
130
+ var records = {};
131
+ records[record] = {
132
+ name: record,
133
+ sys_id: "sysid_" + record,
134
+ files: [
135
+ { name: "script", type: "js", content: scriptContent },
136
+ {
137
+ name: "metaData",
138
+ type: "json",
139
+ content: JSON.stringify({
140
+ sys_updated_on: { value: "2026-04-29 14:38:51" },
141
+ _record_link: "https://tenonworkstudio.service-now.com/x.do?sys_id=1",
142
+ }),
143
+ },
144
+ ],
145
+ };
146
+ return { sys_script_include: { records: records } };
147
+ }
148
+ describe("--metadata-only does not write the scope manifest (real FileUtils)", function () {
149
+ beforeEach(function () {
150
+ jest.clearAllMocks();
151
+ tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "dove-manifest-gate-"));
152
+ manifestPath = path.join(tmpRoot, "dove.manifest.x_cadso_core.json");
153
+ fs.writeFileSync(manifestPath, SENTINEL);
154
+ mockConfig.getSourcePathForScope.mockReturnValue(tmpRoot);
155
+ mockConfig.getSourcePath.mockReturnValue(tmpRoot);
156
+ // The manifest the "instance" returns differs from the sentinel on disk, so
157
+ // any write at all is detectable.
158
+ mockClient.getManifest.mockResolvedValue({
159
+ scope: "x_cadso_core",
160
+ tables: {
161
+ sys_script_include: {
162
+ records: {
163
+ Rec: { name: "Rec", sys_id: "sysid_Rec", files: [{ name: "script", type: "js" }] },
164
+ },
165
+ },
166
+ },
167
+ });
168
+ mockClient.getMissingFiles.mockResolvedValue(serverResponse("Rec", "var x = 1;"));
169
+ });
170
+ afterEach(function () {
171
+ try {
172
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
173
+ }
174
+ catch (e) { }
175
+ });
176
+ test("leaves the manifest file byte-identical", async function () {
177
+ writeLocalRecord("Rec", "var x = 1;");
178
+ await AppUtils.syncManifest("x_cadso_core", { metadataOnly: true });
179
+ expect(mockLogger.error).not.toHaveBeenCalled();
180
+ expect(fs.readFileSync(manifestPath, "utf8")).toBe(SENTINEL);
181
+ });
182
+ test("still rewrites the record's metaData.json (the gate is narrow)", async function () {
183
+ writeLocalRecord("Rec", "var x = 1;");
184
+ await AppUtils.syncManifest("x_cadso_core", { metadataOnly: true });
185
+ var meta = fs.readFileSync(path.join(tmpRoot, "sys_script_include", "Rec", "metaData.json"), "utf8");
186
+ expect(JSON.parse(meta)._record_link).toBe("/x.do?sys_id=1");
187
+ });
188
+ test("a plain refresh DOES write the manifest", async function () {
189
+ // Proves the gate is conditional, not a blanket disable — without this a
190
+ // hard-coded skip would pass the test above.
191
+ writeLocalRecord("Rec", "var x = 1;");
192
+ await AppUtils.syncManifest("x_cadso_core");
193
+ expect(fs.readFileSync(manifestPath, "utf8")).not.toBe(SENTINEL);
194
+ expect(JSON.parse(fs.readFileSync(manifestPath, "utf8")).tables).toHaveProperty("sys_script_include");
195
+ });
196
+ });
@@ -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.116",
4
4
  "description": "Next-gen file syncer",
5
5
  "license": "GPL-3.0",
6
6
  "main": "./dist/index.js",