@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 +46 -3
- package/dist/tests/snClient.fallback.test.js +82 -2
- package/package.json +1 -1
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(
|
|
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
|
-
|
|
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
|
});
|