@tenonhq/dovetail-core 0.0.115 → 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.
package/dist/appUtils.js CHANGED
@@ -417,10 +417,16 @@ const syncManifest = async (scope, options = {}) => {
417
417
  }
418
418
  const refreshTableCount = Object.keys(newManifest.tables).length;
419
419
  FileLogger_1.fileLogger.debug("Refreshed manifest for " + scope + ": " + refreshTableCount + " tables");
420
- // The manifest is always written in FULL. Narrowing it to the --table
421
- // subset would drop every other table from dove.manifest.<scope>.json and
422
- // break push/watch, so the filter applies only to the file download below.
423
- await fUtils.writeScopeManifest(scope, newManifest);
420
+ // The manifest is written in FULL. Narrowing it to the --table subset
421
+ // would drop every other table from dove.manifest.<scope>.json and break
422
+ // push/watch, so the filter applies only to the file download below.
423
+ //
424
+ // metadataOnly skips the write entirely: this mode's contract is that the
425
+ // diff is metaData.json and nothing else, and the manifest carries record
426
+ // adds, drops and renames that belong in a real refresh.
427
+ if (!options.metadataOnly) {
428
+ await fUtils.writeScopeManifest(scope, newManifest);
429
+ }
424
430
  // --table gate: refresh file content for only the requested tables. Any
425
431
  // table outside this scope's `_tables` whitelist was already dropped above,
426
432
  // so intersecting the manifest is enough. `newManifest` is left intact —
@@ -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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-core",
3
- "version": "0.0.115",
3
+ "version": "0.0.116",
4
4
  "description": "Next-gen file syncer",
5
5
  "license": "GPL-3.0",
6
6
  "main": "./dist/index.js",
@@ -1,7 +1,49 @@
1
1
  {
2
2
  "$schema": "dovetail-manifest/v1",
3
- "generated_at": "2026-07-23T15:03:26.082Z",
3
+ "generated_at": "2026-07-28T03:15:38.027Z",
4
4
  "events": [
5
+ {
6
+ "$schema": "dovetail-release-event/v1",
7
+ "event_id": "dovetail-core@0.0.115",
8
+ "package": "@tenonhq/dovetail-core",
9
+ "version": "0.0.115",
10
+ "prev_version": "0.0.114",
11
+ "published_at": "2026-07-28T03:15:38.018Z",
12
+ "git": {
13
+ "sha": "4a8d2cc328b7fd09f56d2ab6cd66203d28e5b1ae",
14
+ "tag": "dovetail-core@0.0.115"
15
+ },
16
+ "semver_bump": "patch",
17
+ "commits": [
18
+ {
19
+ "sha": "4a8d2cc32",
20
+ "subject": "fix(core): make metaData.json deterministic on every write path (#246)",
21
+ "pr": 246
22
+ },
23
+ {
24
+ "sha": "1749ba4f0",
25
+ "subject": "chore(release): publish 2 packages [skip ci]"
26
+ },
27
+ {
28
+ "sha": "8d886d43d",
29
+ "subject": "chore(release): publish 2 packages [skip ci]"
30
+ },
31
+ {
32
+ "sha": "b79555b25",
33
+ "subject": "chore(release): publish 2 packages [skip ci]"
34
+ },
35
+ {
36
+ "sha": "02f562c32",
37
+ "subject": "chore(release): publish 2 packages [skip ci]"
38
+ },
39
+ {
40
+ "sha": "05e406b06",
41
+ "subject": "chore(release): @tenonhq/dovetail-core@0.0.114 [skip ci]"
42
+ }
43
+ ],
44
+ "exports_delta": null,
45
+ "narrative": null
46
+ },
5
47
  {
6
48
  "$schema": "dovetail-release-event/v1",
7
49
  "event_id": "dovetail-servicenow@0.0.34",
@@ -1212,28 +1254,6 @@
1212
1254
  ],
1213
1255
  "exports_delta": null,
1214
1256
  "narrative": null
1215
- },
1216
- {
1217
- "$schema": "dovetail-release-event/v1",
1218
- "event_id": "dovetail-servicenow@0.0.20",
1219
- "package": "@tenonhq/dovetail-servicenow",
1220
- "version": "0.0.20",
1221
- "prev_version": "0.0.19",
1222
- "published_at": "2026-06-14T00:08:21.397Z",
1223
- "git": {
1224
- "sha": "41295667a526f32eb7a5763ec1b416c19ef49bee",
1225
- "tag": "dovetail-servicenow@0.0.20"
1226
- },
1227
- "semver_bump": "patch",
1228
- "commits": [
1229
- {
1230
- "sha": "41295667a",
1231
- "subject": "chore(servicenow): release 0.0.20 — ship create-table fix to npm (#179)",
1232
- "pr": 179
1233
- }
1234
- ],
1235
- "exports_delta": null,
1236
- "narrative": null
1237
1257
  }
1238
1258
  ]
1239
1259
  }