@tenonhq/dovetail-core 0.0.103 → 0.0.105
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/commander.js +34 -0
- package/dist/reconcile/applyPlan.js +59 -0
- package/dist/reconcile/baseline.js +126 -0
- package/dist/reconcile/deleteOrder.js +52 -0
- package/dist/reconcile/fields.js +28 -0
- package/dist/reconcile/gitignore.js +68 -0
- package/dist/reconcile/recordDiff.js +103 -0
- package/dist/reconcile/recordSource.js +231 -0
- package/dist/reconcile/report.js +193 -0
- package/dist/reconcile/types.js +6 -0
- package/dist/reconcileCommand.js +409 -0
- package/dist/snClient.js +11 -0
- package/dist/tests/reconcileApplyPlan.test.js +98 -0
- package/dist/tests/reconcileBaseline.test.js +103 -0
- package/dist/tests/reconcileDeleteOrder.test.js +70 -0
- package/dist/tests/reconcileFields.test.js +24 -0
- package/dist/tests/reconcileGitignore.test.js +51 -0
- package/dist/tests/reconcileRecordDiff.test.js +125 -0
- package/dist/tests/reconcileReport.test.js +136 -0
- package/package.json +1 -1
- package/release-manifest.json +597 -27
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tests for the multi-pass delete loop: clean deletes, emergent FK ordering
|
|
3
|
+
// (a record blocked until its dependent is gone), and a genuine failure that
|
|
4
|
+
// survives a no-progress pass and is reported.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const deleteOrder_1 = require("../reconcile/deleteOrder");
|
|
7
|
+
const del = (sys_id) => ({
|
|
8
|
+
kind: "delete",
|
|
9
|
+
table: "sys_script",
|
|
10
|
+
scope: "x_cadso_core",
|
|
11
|
+
sys_id,
|
|
12
|
+
name: sys_id,
|
|
13
|
+
fieldDeltas: [],
|
|
14
|
+
deleteDisposition: "tracked",
|
|
15
|
+
});
|
|
16
|
+
describe("runMultiPassDeletes", () => {
|
|
17
|
+
it("deletes everything in a single clean pass", async () => {
|
|
18
|
+
const calls = [];
|
|
19
|
+
const exec = async (c) => {
|
|
20
|
+
calls.push(c.sys_id);
|
|
21
|
+
return { ok: true };
|
|
22
|
+
};
|
|
23
|
+
const out = await (0, deleteOrder_1.runMultiPassDeletes)([del("a"), del("b")], exec);
|
|
24
|
+
expect(out.every((o) => o.ok)).toBe(true);
|
|
25
|
+
expect(calls.sort()).toEqual(["a", "b"]);
|
|
26
|
+
});
|
|
27
|
+
it("resolves FK ordering: parent succeeds only after the child is gone", async () => {
|
|
28
|
+
// "parent" cannot delete while "child" still exists.
|
|
29
|
+
const deleted = new Set();
|
|
30
|
+
const exec = async (c) => {
|
|
31
|
+
if (c.sys_id === "parent" && !deleted.has("child")) {
|
|
32
|
+
return { ok: false, error: "still referenced by child" };
|
|
33
|
+
}
|
|
34
|
+
deleted.add(c.sys_id);
|
|
35
|
+
return { ok: true };
|
|
36
|
+
};
|
|
37
|
+
// parent is attempted first, fails, then succeeds on the pass after child.
|
|
38
|
+
const out = await (0, deleteOrder_1.runMultiPassDeletes)([del("parent"), del("child")], exec);
|
|
39
|
+
expect(out.filter((o) => o.ok).map((o) => o.change.sys_id).sort()).toEqual([
|
|
40
|
+
"child",
|
|
41
|
+
"parent",
|
|
42
|
+
]);
|
|
43
|
+
expect(out.every((o) => o.ok)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
it("reports a record that fails for a non-ordering reason", async () => {
|
|
46
|
+
const exec = async (c) => {
|
|
47
|
+
if (c.sys_id === "locked") {
|
|
48
|
+
return { ok: false, error: "ACL blocked" };
|
|
49
|
+
}
|
|
50
|
+
return { ok: true };
|
|
51
|
+
};
|
|
52
|
+
const out = await (0, deleteOrder_1.runMultiPassDeletes)([del("ok"), del("locked")], exec);
|
|
53
|
+
const failed = out.filter((o) => !o.ok);
|
|
54
|
+
expect(failed).toHaveLength(1);
|
|
55
|
+
expect(failed[0].change.sys_id).toBe("locked");
|
|
56
|
+
expect(failed[0].error).toBe("ACL blocked");
|
|
57
|
+
});
|
|
58
|
+
it("treats a thrown executor error as a failure", async () => {
|
|
59
|
+
const exec = async () => {
|
|
60
|
+
throw new Error("network down");
|
|
61
|
+
};
|
|
62
|
+
const out = await (0, deleteOrder_1.runMultiPassDeletes)([del("a")], exec);
|
|
63
|
+
expect(out[0].ok).toBe(false);
|
|
64
|
+
expect(out[0].error).toBe("network down");
|
|
65
|
+
});
|
|
66
|
+
it("returns [] for no changes", async () => {
|
|
67
|
+
const out = await (0, deleteOrder_1.runMultiPassDeletes)([], async () => ({ ok: true }));
|
|
68
|
+
expect(out).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tests for the field-comparison rules: metaData is excluded from the content
|
|
3
|
+
// diff; everything else is a comparable field with a stable "<name>.<type>" key.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
const fields_1 = require("../reconcile/fields");
|
|
6
|
+
describe("isComparableField", () => {
|
|
7
|
+
it("excludes the metaData.json bookkeeping file", () => {
|
|
8
|
+
expect((0, fields_1.isComparableField)({ name: "metaData", type: "json" })).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
it("includes a real field file", () => {
|
|
11
|
+
expect((0, fields_1.isComparableField)({ name: "script", type: "js" })).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
it("includes a json field that is not metaData", () => {
|
|
14
|
+
expect((0, fields_1.isComparableField)({ name: "composition", type: "json" })).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
it("includes a field that happens to be named metaData but is not json", () => {
|
|
17
|
+
expect((0, fields_1.isComparableField)({ name: "metaData", type: "txt" })).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe("fieldKey", () => {
|
|
21
|
+
it("joins name and type", () => {
|
|
22
|
+
expect((0, fields_1.fieldKey)({ name: "script", type: "js" })).toBe("script.js");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tests for the gitignore-ensure compute step plus the I/O wrapper.
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const gitignore_1 = require("../reconcile/gitignore");
|
|
11
|
+
const ENTRY = ".dove-reconcile-baseline.json";
|
|
12
|
+
describe("hasGitignoreEntry", () => {
|
|
13
|
+
it("detects a present entry ignoring surrounding whitespace", () => {
|
|
14
|
+
expect((0, gitignore_1.hasGitignoreEntry)("node_modules\n" + ENTRY + "\n", ENTRY)).toBe(true);
|
|
15
|
+
expect((0, gitignore_1.hasGitignoreEntry)(" " + ENTRY + " \n", ENTRY)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
it("does not match a commented line", () => {
|
|
18
|
+
expect((0, gitignore_1.hasGitignoreEntry)("# " + ENTRY + "\n", ENTRY)).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
it("returns false when absent", () => {
|
|
21
|
+
expect((0, gitignore_1.hasGitignoreEntry)("node_modules\n", ENTRY)).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe("ensureEntryContent", () => {
|
|
25
|
+
it("creates content when the file does not exist", () => {
|
|
26
|
+
const out = (0, gitignore_1.ensureEntryContent)(null, ENTRY);
|
|
27
|
+
expect(out.changed).toBe(true);
|
|
28
|
+
expect(out.content).toBe(ENTRY + "\n");
|
|
29
|
+
});
|
|
30
|
+
it("appends with a separating newline when the file lacks a trailing one", () => {
|
|
31
|
+
const out = (0, gitignore_1.ensureEntryContent)("node_modules", ENTRY);
|
|
32
|
+
expect(out.changed).toBe(true);
|
|
33
|
+
expect(out.content).toBe("node_modules\n" + ENTRY + "\n");
|
|
34
|
+
});
|
|
35
|
+
it("is a no-op when the entry is already present", () => {
|
|
36
|
+
const existing = "node_modules\n" + ENTRY + "\n";
|
|
37
|
+
const out = (0, gitignore_1.ensureEntryContent)(existing, ENTRY);
|
|
38
|
+
expect(out.changed).toBe(false);
|
|
39
|
+
expect(out.content).toBe(existing);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe("ensureGitignored — I/O", () => {
|
|
43
|
+
it("creates .gitignore with the entry, then is idempotent", () => {
|
|
44
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "dove-gitignore-"));
|
|
45
|
+
const first = (0, gitignore_1.ensureGitignored)(root, ENTRY);
|
|
46
|
+
expect(first.changed).toBe(true);
|
|
47
|
+
expect(fs_1.default.readFileSync(path_1.default.join(root, ".gitignore"), "utf8")).toContain(ENTRY);
|
|
48
|
+
const second = (0, gitignore_1.ensureGitignored)(root, ENTRY);
|
|
49
|
+
expect(second.changed).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tests for the pure reconcile record-diff engine — CRUD classification by
|
|
3
|
+
// sys_id, field-level deltas, and delete disambiguation against the baseline.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
const recordDiff_1 = require("../reconcile/recordDiff");
|
|
6
|
+
const rec = (over) => ({
|
|
7
|
+
table: "sys_script_include",
|
|
8
|
+
scope: "x_cadso_core",
|
|
9
|
+
name: over.sys_id,
|
|
10
|
+
updatedOn: "2026-06-01 00:00:00",
|
|
11
|
+
fields: { "script.js": "var a = 1;" },
|
|
12
|
+
...over,
|
|
13
|
+
});
|
|
14
|
+
describe("diffRecords — classification", () => {
|
|
15
|
+
it("marks a branch-only record as create", () => {
|
|
16
|
+
const diff = (0, recordDiff_1.diffRecords)({
|
|
17
|
+
branch: [rec({ sys_id: "a" })],
|
|
18
|
+
live: [],
|
|
19
|
+
baselineSysIds: new Set(),
|
|
20
|
+
});
|
|
21
|
+
expect(diff.creates.map((c) => c.sys_id)).toEqual(["a"]);
|
|
22
|
+
expect(diff.updates).toHaveLength(0);
|
|
23
|
+
expect(diff.deletes).toHaveLength(0);
|
|
24
|
+
});
|
|
25
|
+
it("marks identical records as unchanged (not update)", () => {
|
|
26
|
+
const diff = (0, recordDiff_1.diffRecords)({
|
|
27
|
+
branch: [rec({ sys_id: "a", fields: { "script.js": "X" } })],
|
|
28
|
+
live: [rec({ sys_id: "a", fields: { "script.js": "X" } })],
|
|
29
|
+
baselineSysIds: new Set(["a"]),
|
|
30
|
+
});
|
|
31
|
+
expect(diff.unchangedCount).toBe(1);
|
|
32
|
+
expect(diff.updates).toHaveLength(0);
|
|
33
|
+
expect(diff.creates).toHaveLength(0);
|
|
34
|
+
expect(diff.deletes).toHaveLength(0);
|
|
35
|
+
});
|
|
36
|
+
it("marks a content difference as update with a changed field delta", () => {
|
|
37
|
+
const diff = (0, recordDiff_1.diffRecords)({
|
|
38
|
+
branch: [rec({ sys_id: "a", fields: { "script.js": "NEW" } })],
|
|
39
|
+
live: [rec({ sys_id: "a", fields: { "script.js": "OLD" } })],
|
|
40
|
+
baselineSysIds: new Set(["a"]),
|
|
41
|
+
});
|
|
42
|
+
expect(diff.updates).toHaveLength(1);
|
|
43
|
+
const deltas = diff.updates[0].fieldDeltas;
|
|
44
|
+
expect(deltas).toHaveLength(1);
|
|
45
|
+
expect(deltas[0]).toMatchObject({
|
|
46
|
+
field: "script.js",
|
|
47
|
+
onBranch: true,
|
|
48
|
+
onLive: true,
|
|
49
|
+
changed: true,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
it("reports a field present only on the branch and only on the instance", () => {
|
|
53
|
+
const diff = (0, recordDiff_1.diffRecords)({
|
|
54
|
+
branch: [rec({ sys_id: "a", fields: { "script.js": "X", "extra.js": "Y" } })],
|
|
55
|
+
live: [rec({ sys_id: "a", fields: { "script.js": "X", "old.js": "Z" } })],
|
|
56
|
+
baselineSysIds: new Set(["a"]),
|
|
57
|
+
});
|
|
58
|
+
const deltas = diff.updates[0].fieldDeltas;
|
|
59
|
+
const byField = Object.fromEntries(deltas.map((d) => [d.field, d]));
|
|
60
|
+
expect(byField["extra.js"]).toMatchObject({ onBranch: true, onLive: false, changed: false });
|
|
61
|
+
expect(byField["old.js"]).toMatchObject({ onBranch: false, onLive: true, changed: false });
|
|
62
|
+
// unchanged shared field is not emitted
|
|
63
|
+
expect(byField["script.js"]).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe("diffRecords — delete disambiguation", () => {
|
|
67
|
+
it("marks an instance-only record in the baseline as a tracked delete", () => {
|
|
68
|
+
const diff = (0, recordDiff_1.diffRecords)({
|
|
69
|
+
branch: [],
|
|
70
|
+
live: [rec({ sys_id: "gone" })],
|
|
71
|
+
baselineSysIds: new Set(["gone"]),
|
|
72
|
+
});
|
|
73
|
+
expect(diff.deletes).toHaveLength(1);
|
|
74
|
+
expect(diff.deletes[0].deleteDisposition).toBe("tracked");
|
|
75
|
+
});
|
|
76
|
+
it("marks an instance-only record absent from the baseline as local-new (drift)", () => {
|
|
77
|
+
const diff = (0, recordDiff_1.diffRecords)({
|
|
78
|
+
branch: [],
|
|
79
|
+
live: [rec({ sys_id: "mine" })],
|
|
80
|
+
baselineSysIds: new Set(["other"]),
|
|
81
|
+
});
|
|
82
|
+
expect(diff.deletes[0].deleteDisposition).toBe("local-new");
|
|
83
|
+
});
|
|
84
|
+
it("marks every instance-only record no-baseline when no baseline exists", () => {
|
|
85
|
+
const diff = (0, recordDiff_1.diffRecords)({
|
|
86
|
+
branch: [],
|
|
87
|
+
live: [rec({ sys_id: "x" }), rec({ sys_id: "y" })],
|
|
88
|
+
baselineSysIds: null,
|
|
89
|
+
});
|
|
90
|
+
expect(diff.deletes.map((d) => d.deleteDisposition)).toEqual([
|
|
91
|
+
"no-baseline",
|
|
92
|
+
"no-baseline",
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe("diffRecords — ordering & convergence", () => {
|
|
97
|
+
it("sorts each bucket by table then name", () => {
|
|
98
|
+
const diff = (0, recordDiff_1.diffRecords)({
|
|
99
|
+
branch: [
|
|
100
|
+
rec({ sys_id: "2", table: "sys_script", name: "b" }),
|
|
101
|
+
rec({ sys_id: "1", table: "sys_script", name: "a" }),
|
|
102
|
+
rec({ sys_id: "3", table: "sys_script_include", name: "a" }),
|
|
103
|
+
],
|
|
104
|
+
live: [],
|
|
105
|
+
baselineSysIds: new Set(),
|
|
106
|
+
});
|
|
107
|
+
expect(diff.creates.map((c) => c.table + "/" + c.name)).toEqual([
|
|
108
|
+
"sys_script/a",
|
|
109
|
+
"sys_script/b",
|
|
110
|
+
"sys_script_include/a",
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
it("an already-converged instance produces an empty diff (idempotent)", () => {
|
|
114
|
+
const records = [rec({ sys_id: "a" }), rec({ sys_id: "b" })];
|
|
115
|
+
const diff = (0, recordDiff_1.diffRecords)({
|
|
116
|
+
branch: records,
|
|
117
|
+
live: records,
|
|
118
|
+
baselineSysIds: new Set(["a", "b"]),
|
|
119
|
+
});
|
|
120
|
+
expect(diff.creates).toHaveLength(0);
|
|
121
|
+
expect(diff.updates).toHaveLength(0);
|
|
122
|
+
expect(diff.deletes).toHaveLength(0);
|
|
123
|
+
expect(diff.unchangedCount).toBe(2);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tests for the pure reconcile report renderer — convergence message, drift
|
|
3
|
+
// banner, instance deep-links, delete partitioning, and the report-only schema
|
|
4
|
+
// section.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const report_1 = require("../reconcile/report");
|
|
7
|
+
const emptyDiff = () => ({
|
|
8
|
+
creates: [],
|
|
9
|
+
updates: [],
|
|
10
|
+
deletes: [],
|
|
11
|
+
unchangedCount: 0,
|
|
12
|
+
});
|
|
13
|
+
const change = (over) => ({
|
|
14
|
+
table: "sys_script_include",
|
|
15
|
+
scope: "x_cadso_core",
|
|
16
|
+
name: over.sys_id,
|
|
17
|
+
fieldDeltas: [],
|
|
18
|
+
...over,
|
|
19
|
+
});
|
|
20
|
+
const scope = (over) => ({
|
|
21
|
+
scope: "x_cadso_core",
|
|
22
|
+
diff: emptyDiff(),
|
|
23
|
+
dirty: [],
|
|
24
|
+
hasBaseline: true,
|
|
25
|
+
schema: null,
|
|
26
|
+
...over,
|
|
27
|
+
});
|
|
28
|
+
describe("formatReconcileReport", () => {
|
|
29
|
+
it("reports an in-sync scope and a zeroed summary", () => {
|
|
30
|
+
const out = (0, report_1.formatReconcileReport)({
|
|
31
|
+
instanceHost: "tenonworkstudio",
|
|
32
|
+
branchRef: "main",
|
|
33
|
+
scopes: [scope({ diff: { ...emptyDiff(), unchangedCount: 12 } })],
|
|
34
|
+
schemaSkippedReason: null,
|
|
35
|
+
});
|
|
36
|
+
expect(out).toContain("in sync (12 records match the instance)");
|
|
37
|
+
expect(out).toContain("Summary: 0 create, 0 update, 0 delete");
|
|
38
|
+
});
|
|
39
|
+
it("renders create/update/delete rows with instance deep links", () => {
|
|
40
|
+
const diff = {
|
|
41
|
+
creates: [change({ sys_id: "c1", kind: "create", name: "NewThing" })],
|
|
42
|
+
updates: [
|
|
43
|
+
change({
|
|
44
|
+
sys_id: "u1",
|
|
45
|
+
kind: "update",
|
|
46
|
+
name: "Edited",
|
|
47
|
+
fieldDeltas: [{ field: "script.js", onBranch: true, onLive: true, changed: true }],
|
|
48
|
+
}),
|
|
49
|
+
],
|
|
50
|
+
deletes: [change({ sys_id: "d1", kind: "delete", name: "Gone", deleteDisposition: "tracked" })],
|
|
51
|
+
unchangedCount: 0,
|
|
52
|
+
};
|
|
53
|
+
const out = (0, report_1.formatReconcileReport)({
|
|
54
|
+
instanceHost: "tenonworkstudio",
|
|
55
|
+
branchRef: "feat/x",
|
|
56
|
+
scopes: [scope({ diff })],
|
|
57
|
+
schemaSkippedReason: null,
|
|
58
|
+
});
|
|
59
|
+
expect(out).toContain("CREATE (1):");
|
|
60
|
+
expect(out).toContain("UPDATE (1):");
|
|
61
|
+
expect(out).toContain("DELETE (1):");
|
|
62
|
+
expect(out).toContain("https://tenonworkstudio.service-now.com/sys_script_include.do?sys_id=c1");
|
|
63
|
+
expect(out).toContain("- script.js (changed)");
|
|
64
|
+
expect(out).toContain("Summary: 1 create, 1 update, 1 delete");
|
|
65
|
+
});
|
|
66
|
+
it("keeps local-new instance-only records out of DELETE and never proposes deleting them", () => {
|
|
67
|
+
const diff = {
|
|
68
|
+
...emptyDiff(),
|
|
69
|
+
deletes: [change({ sys_id: "mine", kind: "delete", name: "DevMade", deleteDisposition: "local-new" })],
|
|
70
|
+
};
|
|
71
|
+
const out = (0, report_1.formatReconcileReport)({
|
|
72
|
+
instanceHost: "tenonworkstudio",
|
|
73
|
+
branchRef: "main",
|
|
74
|
+
scopes: [scope({ diff })],
|
|
75
|
+
schemaSkippedReason: null,
|
|
76
|
+
});
|
|
77
|
+
expect(out).toContain("INSTANCE-ONLY");
|
|
78
|
+
expect(out).not.toContain("DELETE (");
|
|
79
|
+
expect(out).toContain("Summary: 0 create, 0 update, 0 delete, 1 instance-only kept");
|
|
80
|
+
});
|
|
81
|
+
it("surfaces drift and warns that apply would refuse", () => {
|
|
82
|
+
const out = (0, report_1.formatReconcileReport)({
|
|
83
|
+
instanceHost: "tenonworkstudio",
|
|
84
|
+
branchRef: "main",
|
|
85
|
+
scopes: [
|
|
86
|
+
scope({
|
|
87
|
+
dirty: [
|
|
88
|
+
{ sys_id: "x", table: "sys_script", name: "Touched", reason: "changed-since-baseline" },
|
|
89
|
+
],
|
|
90
|
+
}),
|
|
91
|
+
],
|
|
92
|
+
schemaSkippedReason: null,
|
|
93
|
+
});
|
|
94
|
+
expect(out).toContain("DRIFT — instance changed since baseline");
|
|
95
|
+
expect(out).toContain("Apply would REFUSE: 1 record(s) changed on the instance since baseline");
|
|
96
|
+
});
|
|
97
|
+
it("warns when no baseline exists for the scope", () => {
|
|
98
|
+
const out = (0, report_1.formatReconcileReport)({
|
|
99
|
+
instanceHost: "tenonworkstudio",
|
|
100
|
+
branchRef: "main",
|
|
101
|
+
scopes: [scope({ hasBaseline: false })],
|
|
102
|
+
schemaSkippedReason: null,
|
|
103
|
+
});
|
|
104
|
+
expect(out).toContain("no baseline for this instance");
|
|
105
|
+
});
|
|
106
|
+
it("renders schema deltas as report-only with dictionary links", () => {
|
|
107
|
+
const schemaDiff = {
|
|
108
|
+
instance: "tenonworkstudio",
|
|
109
|
+
from: { ref: "snap", generated_at: "" },
|
|
110
|
+
to: { ref: "live", generated_at: "" },
|
|
111
|
+
scope: "x_cadso_core",
|
|
112
|
+
summary: { breaking: 1, warn: 0, info: 0 },
|
|
113
|
+
tables: [],
|
|
114
|
+
fields: [{ table: "x_cadso_thing", field: "old_col", change: "removed", severity: "BREAKING" }],
|
|
115
|
+
exit_code: 1,
|
|
116
|
+
};
|
|
117
|
+
const out = (0, report_1.formatReconcileReport)({
|
|
118
|
+
instanceHost: "tenonworkstudio",
|
|
119
|
+
branchRef: "main",
|
|
120
|
+
scopes: [scope({ schema: schemaDiff })],
|
|
121
|
+
schemaSkippedReason: null,
|
|
122
|
+
});
|
|
123
|
+
expect(out).toContain("SCHEMA — report-only");
|
|
124
|
+
expect(out).toContain("removed x_cadso_thing.old_col");
|
|
125
|
+
expect(out).toContain("sys_dictionary_list.do?sysparm_query=name=x_cadso_thing^element=old_col");
|
|
126
|
+
});
|
|
127
|
+
it("notes when the schema arm was skipped", () => {
|
|
128
|
+
const out = (0, report_1.formatReconcileReport)({
|
|
129
|
+
instanceHost: "tenonworkstudio",
|
|
130
|
+
branchRef: "main",
|
|
131
|
+
scopes: [scope({})],
|
|
132
|
+
schemaSkippedReason: "no snapshot found",
|
|
133
|
+
});
|
|
134
|
+
expect(out).toContain("Schema diff skipped: no snapshot found");
|
|
135
|
+
});
|
|
136
|
+
});
|