@tenonhq/dovetail-core 0.0.90 → 0.0.92

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/snClient.js CHANGED
@@ -75,9 +75,46 @@ function _resetDovetailApiFallback() {
75
75
  function _isUsingLegacyClaudePath() {
76
76
  return _dovetailApiUseLegacyClaudePath;
77
77
  }
78
+ // SN returns 400 with this body when a Scripted REST endpoint does not exist
79
+ // at the requested URI. The /dovetail -> /claude fallback above only fires on
80
+ // 404, so the same "endpoint missing" condition presents as an opaque 400 to
81
+ // callers. Detect it and surface a clear hint.
82
+ var _SN_MISSING_ENDPOINT_BODY = "Requested URI does not represent any resource";
83
+ function _summarizeResponseBody(data) {
84
+ if (data === undefined || data === null)
85
+ return "";
86
+ try {
87
+ var asJson = typeof data === "string" ? data : JSON.stringify(data);
88
+ return asJson.length > 500 ? asJson.slice(0, 500) + "…" : asJson;
89
+ }
90
+ catch (_) {
91
+ return String(data);
92
+ }
93
+ }
94
+ // Augment an axios error in place with a human-readable message describing
95
+ // the attempted Dovetail REST call. Preserves .response/.code/.config so
96
+ // existing callers and tests that inspect those fields keep working.
97
+ function _enrichDovetailApiError(e, attemptedEndpoint) {
98
+ if (!axios_1.default.isAxiosError(e))
99
+ return e;
100
+ var status = e.response ? e.response.status : undefined;
101
+ var body = e.response ? _summarizeResponseBody(e.response.data) : "";
102
+ var missingEndpoint = status === 400 && body.indexOf(_SN_MISSING_ENDPOINT_BODY) !== -1;
103
+ var hint = missingEndpoint
104
+ ? " — Scripted REST endpoint not found on this instance. Re-import the Dovetail Scripted REST API XML (see Dovetail/docs/dovetail-servicenow-migration.md)."
105
+ : "";
106
+ e.message =
107
+ "Dovetail REST call failed: " +
108
+ (status ? "HTTP " + status : "no response") +
109
+ " " + attemptedEndpoint +
110
+ (body ? " — " + body : "") +
111
+ hint;
112
+ return e;
113
+ }
78
114
  async function _callDovetailApi(op, call) {
115
+ var endpoint = _dovetailEndpoint(op);
79
116
  try {
80
- return await call(_dovetailEndpoint(op));
117
+ return await call(endpoint);
81
118
  }
82
119
  catch (e) {
83
120
  if (!_dovetailApiUseLegacyClaudePath && _getHttpStatus(e) === 404) {
@@ -85,9 +122,15 @@ async function _callDovetailApi(op, call) {
85
122
  " returned 404. Falling back to legacy /api/cadso/claude/" + op +
86
123
  ". Re-import the Dovetail Scripted REST API XML on your ServiceNow instance to silence this warning.");
87
124
  _dovetailApiUseLegacyClaudePath = true;
88
- return await call(_dovetailEndpoint(op));
125
+ var legacyEndpoint = _dovetailEndpoint(op);
126
+ try {
127
+ return await call(legacyEndpoint);
128
+ }
129
+ catch (e2) {
130
+ throw _enrichDovetailApiError(e2, legacyEndpoint);
131
+ }
89
132
  }
90
- throw e;
133
+ throw _enrichDovetailApiError(e, endpoint);
91
134
  }
92
135
  }
93
136
  function _getRetryAfterMs(e) {
@@ -21,10 +21,10 @@ jest.mock("../genericUtils", function () {
21
21
  return { wait: jest.fn().mockResolvedValue(undefined) };
22
22
  });
23
23
  const snClient_1 = require("../snClient");
24
- function makeAxiosError(status) {
24
+ function makeAxiosError(status, data) {
25
25
  var error = new Error("Request failed with status " + status);
26
26
  error.isAxiosError = true;
27
- error.response = { status: status, headers: {}, data: {} };
27
+ error.response = { status: status, headers: {}, data: data === undefined ? {} : data };
28
28
  return error;
29
29
  }
30
30
  function makeAxiosResponse(data) {
@@ -117,4 +117,84 @@ describe("_callDovetailApi 404 fallback", function () {
117
117
  expect((0, snClient_1._isUsingLegacyClaudePath)()).toBe(false);
118
118
  expect(mockLogger.warn).not.toHaveBeenCalled();
119
119
  });
120
+ describe("descriptive error enrichment", function () {
121
+ it("includes status, endpoint, and response body on a 401", async function () {
122
+ var call = jest.fn(async function (_endpoint) {
123
+ throw makeAxiosError(401, { error: { message: "User Not Authenticated" } });
124
+ });
125
+ try {
126
+ await (0, snClient_1._callDovetailApi)("changeScope", call);
127
+ throw new Error("expected rejection");
128
+ }
129
+ catch (e) {
130
+ expect(e.response.status).toBe(401);
131
+ expect(e.message).toContain("HTTP 401");
132
+ expect(e.message).toContain("api/cadso/dovetail/changeScope");
133
+ expect(e.message).toContain("User Not Authenticated");
134
+ }
135
+ });
136
+ it("adds the missing-endpoint hint when SN returns its 400 'Requested URI does not represent any resource'", async function () {
137
+ var snBody = {
138
+ error: { message: "Requested URI does not represent any resource", detail: null },
139
+ status: "failure",
140
+ };
141
+ var call = jest.fn(async function (_endpoint) {
142
+ throw makeAxiosError(400, snBody);
143
+ });
144
+ try {
145
+ await (0, snClient_1._callDovetailApi)("changeScope", call);
146
+ throw new Error("expected rejection");
147
+ }
148
+ catch (e) {
149
+ expect(e.response.status).toBe(400);
150
+ expect(e.message).toContain("HTTP 400");
151
+ expect(e.message).toContain("api/cadso/dovetail/changeScope");
152
+ expect(e.message).toContain("Re-import the Dovetail Scripted REST API XML");
153
+ }
154
+ });
155
+ it("does NOT add the missing-endpoint hint for unrelated 400s", async function () {
156
+ var call = jest.fn(async function (_endpoint) {
157
+ throw makeAxiosError(400, { error: { message: "Invalid scope name" } });
158
+ });
159
+ try {
160
+ await (0, snClient_1._callDovetailApi)("changeScope", call);
161
+ throw new Error("expected rejection");
162
+ }
163
+ catch (e) {
164
+ expect(e.message).toContain("HTTP 400");
165
+ expect(e.message).toContain("Invalid scope name");
166
+ expect(e.message).not.toContain("Re-import the Dovetail Scripted REST API XML");
167
+ }
168
+ });
169
+ it("after fallback latches, errors from the legacy path are also enriched", async function () {
170
+ var calls = [];
171
+ var call = jest.fn(async function (endpoint) {
172
+ calls.push(endpoint);
173
+ if (endpoint.indexOf("api/cadso/dovetail/") === 0) {
174
+ throw makeAxiosError(404, {});
175
+ }
176
+ throw makeAxiosError(500, { error: { message: "boom" } });
177
+ });
178
+ try {
179
+ await (0, snClient_1._callDovetailApi)("changeScope", call);
180
+ throw new Error("expected rejection");
181
+ }
182
+ catch (e) {
183
+ expect(calls).toEqual([
184
+ "api/cadso/dovetail/changeScope",
185
+ "api/cadso/claude/changeScope",
186
+ ]);
187
+ expect(e.response.status).toBe(500);
188
+ expect(e.message).toContain("HTTP 500");
189
+ expect(e.message).toContain("api/cadso/claude/changeScope");
190
+ expect(e.message).toContain("boom");
191
+ }
192
+ });
193
+ it("leaves non-axios errors untouched", async function () {
194
+ var call = jest.fn(async function (_endpoint) {
195
+ throw new Error("ECONNREFUSED");
196
+ });
197
+ await expect((0, snClient_1._callDovetailApi)("changeScope", call)).rejects.toThrow("ECONNREFUSED");
198
+ });
199
+ });
120
200
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-core",
3
- "version": "0.0.90",
3
+ "version": "0.0.92",
4
4
  "description": "Next-gen file syncer",
5
5
  "license": "GPL-3.0",
6
6
  "main": "./dist/index.js",