@tenonhq/dovetail-core 0.0.104 → 0.0.106

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,103 @@
1
+ "use strict";
2
+ // Tests for the reconcile baseline: defensive read (degrades to null, never
3
+ // throws), write round-trip, the pure dirty-check, and live-snapshot capture.
4
+ var __importDefault = (this && this.__importDefault) || function (mod) {
5
+ return (mod && mod.__esModule) ? mod : { "default": mod };
6
+ };
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const baseline_1 = require("../reconcile/baseline");
12
+ const live = (sys_id, updatedOn) => ({
13
+ table: "sys_script",
14
+ scope: "x_cadso_core",
15
+ name: sys_id,
16
+ sys_id,
17
+ updatedOn,
18
+ fields: {},
19
+ });
20
+ function tmpRoot() {
21
+ return fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "dove-reconcile-base-"));
22
+ }
23
+ describe("readBaseline — defensive", () => {
24
+ it("returns null when the file is absent", () => {
25
+ expect((0, baseline_1.readBaseline)(tmpRoot(), "tenonworkstudio")).toBeNull();
26
+ });
27
+ it("returns null on malformed JSON", () => {
28
+ const root = tmpRoot();
29
+ fs_1.default.writeFileSync(path_1.default.join(root, baseline_1.BASELINE_FILENAME), "{not json");
30
+ expect((0, baseline_1.readBaseline)(root, "tenonworkstudio")).toBeNull();
31
+ });
32
+ it("returns null on wrong version or non-string record map", () => {
33
+ const root = tmpRoot();
34
+ fs_1.default.writeFileSync(path_1.default.join(root, baseline_1.BASELINE_FILENAME), JSON.stringify({ version: 2, instance: "x", records: {} }));
35
+ expect((0, baseline_1.readBaseline)(root, "x")).toBeNull();
36
+ const root2 = tmpRoot();
37
+ fs_1.default.writeFileSync(path_1.default.join(root2, baseline_1.BASELINE_FILENAME), JSON.stringify({ version: 1, instance: "x", records: { a: 5 } }));
38
+ expect((0, baseline_1.readBaseline)(root2, "x")).toBeNull();
39
+ });
40
+ it("returns null when the baseline was captured against another instance", () => {
41
+ const root = tmpRoot();
42
+ (0, baseline_1.writeBaseline)(root, {
43
+ version: 1,
44
+ instance: "tenonworkshop",
45
+ records: { a: "t" },
46
+ });
47
+ expect((0, baseline_1.readBaseline)(root, "tenonworkstudio")).toBeNull();
48
+ });
49
+ it("round-trips a valid baseline", () => {
50
+ const root = tmpRoot();
51
+ (0, baseline_1.writeBaseline)(root, {
52
+ version: 1,
53
+ instance: "tenonworkstudio",
54
+ records: { a: "2026-06-01 00:00:00" },
55
+ });
56
+ const got = (0, baseline_1.readBaseline)(root, "tenonworkstudio");
57
+ expect(got).not.toBeNull();
58
+ expect(got.records.a).toBe("2026-06-01 00:00:00");
59
+ });
60
+ });
61
+ describe("baselineFromLive + baselineSysIds", () => {
62
+ it("captures sys_id -> updatedOn from the live set", () => {
63
+ const base = (0, baseline_1.baselineFromLive)("tenonworkstudio", [
64
+ live("a", "t1"),
65
+ live("b", "t2"),
66
+ ]);
67
+ expect(base.records).toEqual({ a: "t1", b: "t2" });
68
+ expect(base.instance).toBe("tenonworkstudio");
69
+ });
70
+ it("baselineSysIds is null for a null baseline and a Set otherwise", () => {
71
+ expect((0, baseline_1.baselineSysIds)(null)).toBeNull();
72
+ const set = (0, baseline_1.baselineSysIds)({ version: 1, instance: "x", records: { a: "t" } });
73
+ expect(set).not.toBeNull();
74
+ expect(set.has("a")).toBe(true);
75
+ });
76
+ });
77
+ describe("computeDirty", () => {
78
+ it("returns [] with no baseline", () => {
79
+ expect((0, baseline_1.computeDirty)({ baseline: null, live: [live("a", "t2")] })).toEqual([]);
80
+ });
81
+ it("flags a baseline record whose live updatedOn moved", () => {
82
+ const dirty = (0, baseline_1.computeDirty)({
83
+ baseline: { version: 1, instance: "x", records: { a: "t1", b: "t1" } },
84
+ live: [live("a", "t2"), live("b", "t1")],
85
+ });
86
+ expect(dirty).toHaveLength(1);
87
+ expect(dirty[0]).toMatchObject({ sys_id: "a", reason: "changed-since-baseline" });
88
+ });
89
+ it("does not flag records absent from the baseline (those are deletes/creates, not drift)", () => {
90
+ const dirty = (0, baseline_1.computeDirty)({
91
+ baseline: { version: 1, instance: "x", records: {} },
92
+ live: [live("new", "t9")],
93
+ });
94
+ expect(dirty).toEqual([]);
95
+ });
96
+ it("ignores empty timestamps rather than false-flagging", () => {
97
+ const dirty = (0, baseline_1.computeDirty)({
98
+ baseline: { version: 1, instance: "x", records: { a: "" } },
99
+ live: [live("a", "t2")],
100
+ });
101
+ expect(dirty).toEqual([]);
102
+ });
103
+ });
@@ -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,74 @@
1
+ "use strict";
2
+ // Tests for the generic multi-pass op loop: clean ops, emergent ordering (a
3
+ // record blocked until its dependency resolves), a genuine failure surviving a
4
+ // no-progress pass, and thrown-error handling. Exercised for both delete
5
+ // ordering (children before parents) and create ordering (parents before
6
+ // children).
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ const multiPass_1 = require("../reconcile/multiPass");
9
+ const rec = (sys_id, kind) => ({
10
+ kind,
11
+ table: "sys_script",
12
+ scope: "x_cadso_core",
13
+ sys_id,
14
+ name: sys_id,
15
+ fieldDeltas: [],
16
+ });
17
+ describe("runMultiPassOps", () => {
18
+ it("completes everything in a single clean pass", async () => {
19
+ const calls = [];
20
+ const exec = async (c) => {
21
+ calls.push(c.sys_id);
22
+ return { ok: true };
23
+ };
24
+ const out = await (0, multiPass_1.runMultiPassOps)([rec("a", "delete"), rec("b", "delete")], exec);
25
+ expect(out.every((o) => o.ok)).toBe(true);
26
+ expect(calls.sort()).toEqual(["a", "b"]);
27
+ });
28
+ it("resolves DELETE ordering: parent succeeds only after child is gone", async () => {
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
+ const out = await (0, multiPass_1.runMultiPassOps)([rec("parent", "delete"), rec("child", "delete")], exec);
38
+ expect(out.every((o) => o.ok)).toBe(true);
39
+ });
40
+ it("resolves CREATE ordering: child succeeds only after parent exists", async () => {
41
+ const created = new Set();
42
+ const exec = async (c) => {
43
+ if (c.sys_id === "child" && !created.has("parent")) {
44
+ return { ok: false, error: "parent does not exist yet" };
45
+ }
46
+ created.add(c.sys_id);
47
+ return { ok: true };
48
+ };
49
+ // child attempted first, fails, then succeeds on the pass after parent.
50
+ const out = await (0, multiPass_1.runMultiPassOps)([rec("child", "create"), rec("parent", "create")], exec);
51
+ expect(out.every((o) => o.ok)).toBe(true);
52
+ expect(created.has("child")).toBe(true);
53
+ });
54
+ it("reports a record that fails for a non-ordering reason", async () => {
55
+ const exec = async (c) => c.sys_id === "locked" ? { ok: false, error: "ACL blocked" } : { ok: true };
56
+ const out = await (0, multiPass_1.runMultiPassOps)([rec("ok", "delete"), rec("locked", "delete")], exec);
57
+ const failed = out.filter((o) => !o.ok);
58
+ expect(failed).toHaveLength(1);
59
+ expect(failed[0].change.sys_id).toBe("locked");
60
+ expect(failed[0].error).toBe("ACL blocked");
61
+ });
62
+ it("treats a thrown executor error as a failure", async () => {
63
+ const exec = async () => {
64
+ throw new Error("network down");
65
+ };
66
+ const out = await (0, multiPass_1.runMultiPassOps)([rec("a", "create")], exec);
67
+ expect(out[0].ok).toBe(false);
68
+ expect(out[0].error).toBe("network down");
69
+ });
70
+ it("returns [] for no changes", async () => {
71
+ const out = await (0, multiPass_1.runMultiPassOps)([], async () => ({ ok: true }));
72
+ expect(out).toEqual([]);
73
+ });
74
+ });
@@ -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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-core",
3
- "version": "0.0.104",
3
+ "version": "0.0.106",
4
4
  "description": "Next-gen file syncer",
5
5
  "license": "GPL-3.0",
6
6
  "main": "./dist/index.js",