@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 +10 -4
- package/dist/tests/metadataOnlyManifestGate.test.js +196 -0
- package/package.json +1 -1
- package/release-manifest.json +43 -23
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
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
|
|
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
package/release-manifest.json
CHANGED
|
@@ -1,7 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "dovetail-manifest/v1",
|
|
3
|
-
"generated_at": "2026-07-
|
|
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
|
}
|