testit-js-commons 3.7.9 → 4.0.0

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.
Files changed (45) hide show
  1. package/lib/common/types/config.type.d.ts +8 -0
  2. package/lib/common/utils/index.d.ts +1 -0
  3. package/lib/common/utils/index.js +1 -0
  4. package/lib/common/utils/tms-load-test-run-debug.d.ts +3 -0
  5. package/lib/common/utils/tms-load-test-run-debug.js +20 -0
  6. package/lib/helpers/config/config.helper.js +14 -6
  7. package/lib/helpers/config/config.helper.test.d.ts +1 -0
  8. package/lib/helpers/config/config.helper.test.js +40 -0
  9. package/lib/services/attachments/attachments.service.js +89 -22
  10. package/lib/services/index.d.ts +1 -0
  11. package/lib/services/index.js +1 -0
  12. package/lib/services/syncstorage/index.d.ts +2 -0
  13. package/lib/services/syncstorage/index.js +18 -0
  14. package/lib/services/syncstorage/syncstorage.runner.d.ts +49 -0
  15. package/lib/services/syncstorage/syncstorage.runner.js +350 -0
  16. package/lib/services/syncstorage/syncstorage.runner.test.d.ts +1 -0
  17. package/lib/services/syncstorage/syncstorage.runner.test.js +78 -0
  18. package/lib/services/syncstorage/syncstorage.type.d.ts +21 -0
  19. package/lib/services/syncstorage/syncstorage.type.js +19 -0
  20. package/lib/services/testruns/testruns.converter.d.ts +2 -0
  21. package/lib/services/testruns/testruns.converter.js +3 -0
  22. package/lib/services/testruns/testruns.service.d.ts +2 -0
  23. package/lib/services/testruns/testruns.service.js +62 -1
  24. package/lib/services/testruns/testruns.type.d.ts +2 -0
  25. package/lib/strategy/base.strategy.d.ts +4 -0
  26. package/lib/strategy/base.strategy.js +97 -2
  27. package/lib/sync-storage/dist/ApiClient.js +655 -0
  28. package/lib/sync-storage/dist/api/CompletionApi.js +114 -0
  29. package/lib/sync-storage/dist/api/HealthApi.js +69 -0
  30. package/lib/sync-storage/dist/api/SystemApi.js +69 -0
  31. package/lib/sync-storage/dist/api/TestResultsApi.js +122 -0
  32. package/lib/sync-storage/dist/api/WorkersApi.js +113 -0
  33. package/lib/sync-storage/dist/index.js +118 -0
  34. package/lib/sync-storage/dist/model/CompletionResponse.js +86 -0
  35. package/lib/sync-storage/dist/model/HealthStatusResponse.js +86 -0
  36. package/lib/sync-storage/dist/model/InProgressPublishedResponse.js +74 -0
  37. package/lib/sync-storage/dist/model/RegisterRequest.js +114 -0
  38. package/lib/sync-storage/dist/model/RegisterResponse.js +122 -0
  39. package/lib/sync-storage/dist/model/SetWorkerStatusRequest.js +102 -0
  40. package/lib/sync-storage/dist/model/SetWorkerStatusResponse.js +90 -0
  41. package/lib/sync-storage/dist/model/ShutdownResponse.js +90 -0
  42. package/lib/sync-storage/dist/model/TestResultCutApiModel.js +122 -0
  43. package/lib/sync-storage/dist/model/TestResultSaveResponse.js +90 -0
  44. package/lib/sync-storage/index.d.ts +772 -0
  45. package/package.json +7 -4
@@ -0,0 +1,350 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.SyncStorageRunner = void 0;
13
+ const fs_1 = require("fs");
14
+ const promises_1 = require("fs/promises");
15
+ const https_1 = require("https");
16
+ const path_1 = require("path");
17
+ const process_1 = require("process");
18
+ const child_process_1 = require("child_process");
19
+ // Generated sync-storage client is bundled into lib/sync-storage/dist during build.
20
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
21
+ const SyncStorageClient = require("../../sync-storage/dist/index");
22
+ class SyncStorageRunner {
23
+ constructor(testRunId, config) {
24
+ var _a;
25
+ this.testRunId = testRunId;
26
+ this.config = config;
27
+ this.isMaster = false;
28
+ this.alreadyInProgress = false;
29
+ this.inProgressPublishing = false;
30
+ this.running = false;
31
+ this.workerPid = `worker-${process.pid}-${Date.now()}`;
32
+ this.baseUrl = config.url;
33
+ this.port = (_a = config.syncStoragePort) !== null && _a !== void 0 ? _a : "49152";
34
+ this.serviceUrl = `http://localhost:${this.port}`;
35
+ const apiClient = new SyncStorageClient.ApiClient(this.serviceUrl);
36
+ apiClient.timeout = SyncStorageRunner.REQUEST_TIMEOUT_MS;
37
+ this.healthApi = new SyncStorageClient.HealthApi(apiClient);
38
+ this.workersApi = new SyncStorageClient.WorkersApi(apiClient);
39
+ this.testResultsApi = new SyncStorageClient.TestResultsApi(apiClient);
40
+ this.completionApi = new SyncStorageClient.CompletionApi(apiClient);
41
+ this.registerRequestModel = SyncStorageClient.RegisterRequest;
42
+ this.setWorkerStatusRequestModel = SyncStorageClient.SetWorkerStatusRequest;
43
+ this.testResultCutModel = SyncStorageClient.TestResultCutApiModel;
44
+ }
45
+ start() {
46
+ return __awaiter(this, void 0, void 0, function* () {
47
+ try {
48
+ const healthy = yield this.healthcheck();
49
+ if (!healthy) {
50
+ const started = yield this.startLocalProcess();
51
+ if (!started) {
52
+ return false;
53
+ }
54
+ }
55
+ const request = this.registerRequestModel.constructFromObject({
56
+ pid: this.workerPid,
57
+ testRunId: this.testRunId,
58
+ baseUrl: this.baseUrl,
59
+ privateToken: this.config.privateToken,
60
+ });
61
+ const response = yield this.withRetry(() => __awaiter(this, void 0, void 0, function* () { return this.workersApi.registerPost(request); }), SyncStorageRunner.RETRY_COUNT);
62
+ this.isMaster = Boolean(response === null || response === void 0 ? void 0 : response.is_master);
63
+ this.running = true;
64
+ return true;
65
+ }
66
+ catch (error) {
67
+ console.warn(`Sync storage start failed: ${error}`);
68
+ return false;
69
+ }
70
+ });
71
+ }
72
+ isActive() {
73
+ return this.running;
74
+ }
75
+ isMasterWorker() {
76
+ return this.isMaster;
77
+ }
78
+ isAlreadyInProgress() {
79
+ return this.alreadyInProgress;
80
+ }
81
+ waitForInProgressPublished(timeoutMs) {
82
+ return __awaiter(this, void 0, void 0, function* () {
83
+ if (!this.running) {
84
+ return false;
85
+ }
86
+ if (this.alreadyInProgress) {
87
+ return true;
88
+ }
89
+ const deadline = Date.now() + Math.max(0, timeoutMs);
90
+ while (Date.now() < deadline) {
91
+ try {
92
+ const state = yield this.testResultsApi.inProgressPublishedGet(this.testRunId);
93
+ if (Boolean(state === null || state === void 0 ? void 0 : state.published)) {
94
+ this.alreadyInProgress = true;
95
+ return true;
96
+ }
97
+ }
98
+ catch (_a) {
99
+ // Endpoint can be temporarily unavailable; continue best-effort polling.
100
+ }
101
+ yield this.delay(SyncStorageRunner.IN_PROGRESS_POLL_MS);
102
+ }
103
+ return false;
104
+ });
105
+ }
106
+ sendInProgressTestResult(model) {
107
+ return __awaiter(this, void 0, void 0, function* () {
108
+ if (!this.running || !this.isMaster || this.alreadyInProgress || this.inProgressPublishing) {
109
+ console.log("[syncstorage] skip in-progress cut publish", {
110
+ reason: {
111
+ notRunning: !this.running,
112
+ notMaster: !this.isMaster,
113
+ alreadyInProgress: this.alreadyInProgress,
114
+ publishingInFlight: this.inProgressPublishing,
115
+ },
116
+ workerPid: this.workerPid,
117
+ });
118
+ return false;
119
+ }
120
+ if (!model.projectId || !model.autoTestExternalId || !model.statusCode || !model.statusType) {
121
+ console.warn("Sync storage in-progress payload is incomplete; skipping publish.");
122
+ console.log("[syncstorage] incomplete in-progress cut payload", {
123
+ hasProjectId: Boolean(model.projectId),
124
+ hasAutoTestExternalId: Boolean(model.autoTestExternalId),
125
+ hasStatusCode: Boolean(model.statusCode),
126
+ hasStatusType: Boolean(model.statusType),
127
+ });
128
+ return false;
129
+ }
130
+ this.inProgressPublishing = true;
131
+ try {
132
+ const request = this.testResultCutModel.constructFromObject({
133
+ projectId: model.projectId,
134
+ autoTestExternalId: model.autoTestExternalId,
135
+ statusCode: model.statusCode,
136
+ statusType: model.statusType,
137
+ startedOn: model.startedOn,
138
+ });
139
+ yield this.withRetry(() => __awaiter(this, void 0, void 0, function* () { return this.testResultsApi.inProgressTestResultPost(this.testRunId, request); }), SyncStorageRunner.RETRY_COUNT);
140
+ this.alreadyInProgress = true;
141
+ console.log("[syncstorage] alreadyInProgress set", {
142
+ workerPid: this.workerPid,
143
+ autoTestExternalId: model.autoTestExternalId,
144
+ });
145
+ console.log("[syncstorage] in-progress cut published", {
146
+ workerPid: this.workerPid,
147
+ autoTestExternalId: model.autoTestExternalId,
148
+ });
149
+ return true;
150
+ }
151
+ catch (error) {
152
+ console.warn(`Sync storage in-progress publish failed: ${error}`);
153
+ console.log("[syncstorage] in-progress cut publish failed", {
154
+ workerPid: this.workerPid,
155
+ autoTestExternalId: model.autoTestExternalId,
156
+ });
157
+ return false;
158
+ }
159
+ finally {
160
+ this.inProgressPublishing = false;
161
+ }
162
+ });
163
+ }
164
+ setWorkerStatus(status) {
165
+ return __awaiter(this, void 0, void 0, function* () {
166
+ if (!this.running) {
167
+ return;
168
+ }
169
+ try {
170
+ const request = this.setWorkerStatusRequestModel.constructFromObject({
171
+ pid: this.workerPid,
172
+ status,
173
+ testRunId: this.testRunId,
174
+ });
175
+ yield this.withRetry(() => __awaiter(this, void 0, void 0, function* () { return this.workersApi.setWorkerStatusPost(request); }), SyncStorageRunner.RETRY_COUNT);
176
+ }
177
+ catch (error) {
178
+ console.warn(`Sync storage set worker status failed: ${error}`);
179
+ }
180
+ });
181
+ }
182
+ completeProcessing() {
183
+ return __awaiter(this, void 0, void 0, function* () {
184
+ if (!this.running || !this.isMaster) {
185
+ return;
186
+ }
187
+ try {
188
+ yield this.withRetry(() => __awaiter(this, void 0, void 0, function* () { return this.completionApi.waitCompletionGet(this.testRunId); }), SyncStorageRunner.RETRY_COUNT);
189
+ return;
190
+ }
191
+ catch (_a) {
192
+ // Fallback to force completion when wait endpoint is unavailable or times out.
193
+ }
194
+ try {
195
+ yield this.withRetry(() => __awaiter(this, void 0, void 0, function* () { return this.completionApi.forceCompletionGet(this.testRunId); }), SyncStorageRunner.RETRY_COUNT);
196
+ }
197
+ catch (error) {
198
+ console.warn(`Sync storage completion failed: ${error}`);
199
+ }
200
+ });
201
+ }
202
+ healthcheck() {
203
+ return __awaiter(this, void 0, void 0, function* () {
204
+ try {
205
+ yield this.withRetry(() => __awaiter(this, void 0, void 0, function* () { return this.healthApi.healthGet(); }), 1);
206
+ return true;
207
+ }
208
+ catch (_a) {
209
+ return false;
210
+ }
211
+ });
212
+ }
213
+ startLocalProcess() {
214
+ return __awaiter(this, void 0, void 0, function* () {
215
+ try {
216
+ const executablePath = yield this.prepareExecutable();
217
+ const command = [
218
+ "--testRunId", this.testRunId,
219
+ "--port", this.port,
220
+ "--baseURL", this.baseUrl,
221
+ "--privateToken", this.config.privateToken,
222
+ ];
223
+ this.syncStorageProcess = (0, child_process_1.spawn)(executablePath, command, {
224
+ cwd: (0, path_1.join)(process.cwd(), "build", ".caches"),
225
+ stdio: "ignore",
226
+ detached: process_1.platform === "win32",
227
+ });
228
+ const started = yield this.waitForStartup();
229
+ if (started) {
230
+ yield this.delay(SyncStorageRunner.PROCESS_WARMUP_MS);
231
+ }
232
+ return started;
233
+ }
234
+ catch (error) {
235
+ console.warn(`Sync storage local process start failed: ${error}`);
236
+ return false;
237
+ }
238
+ });
239
+ }
240
+ waitForStartup() {
241
+ return __awaiter(this, void 0, void 0, function* () {
242
+ const deadline = Date.now() + SyncStorageRunner.STARTUP_TIMEOUT_MS;
243
+ while (Date.now() < deadline) {
244
+ if (yield this.healthcheck()) {
245
+ return true;
246
+ }
247
+ yield this.delay(SyncStorageRunner.STARTUP_POLL_MS);
248
+ }
249
+ return false;
250
+ });
251
+ }
252
+ prepareExecutable() {
253
+ return __awaiter(this, void 0, void 0, function* () {
254
+ const cachesDir = (0, path_1.join)(process.cwd(), "build", ".caches");
255
+ if (!(0, fs_1.existsSync)(cachesDir)) {
256
+ (0, fs_1.mkdirSync)(cachesDir, { recursive: true });
257
+ }
258
+ const fileName = this.getBinaryName();
259
+ const targetPath = (0, path_1.join)(cachesDir, fileName);
260
+ if (!(0, fs_1.existsSync)(targetPath)) {
261
+ const url = `${SyncStorageRunner.REPO_BASE}/${SyncStorageRunner.VERSION}/${fileName}`;
262
+ yield this.downloadFile(url, targetPath);
263
+ }
264
+ if (process_1.platform !== "win32") {
265
+ yield (0, promises_1.chmod)(targetPath, 0o755);
266
+ }
267
+ return targetPath;
268
+ });
269
+ }
270
+ getBinaryName() {
271
+ const osPart = this.getOsPart();
272
+ const archPart = this.getArchPart();
273
+ const ext = osPart === "windows" ? ".exe" : "";
274
+ return `syncstorage-${SyncStorageRunner.VERSION}-${osPart}_${archPart}${ext}`;
275
+ }
276
+ getOsPart() {
277
+ if (process_1.platform === "win32")
278
+ return "windows";
279
+ if (process_1.platform === "darwin")
280
+ return "darwin";
281
+ if (process_1.platform === "linux")
282
+ return "linux";
283
+ throw new Error(`Unsupported OS: ${process_1.platform}`);
284
+ }
285
+ getArchPart() {
286
+ if (process_1.arch === "x64")
287
+ return "amd64";
288
+ if (process_1.arch === "arm64")
289
+ return "arm64";
290
+ throw new Error(`Unsupported arch: ${process_1.arch}`);
291
+ }
292
+ downloadFile(url, targetPath) {
293
+ return __awaiter(this, void 0, void 0, function* () {
294
+ yield new Promise((resolve, reject) => {
295
+ const file = (0, fs_1.createWriteStream)(targetPath);
296
+ const request = (0, https_1.get)(url, { headers: { "User-Agent": "testit-js-commons" } }, (response) => {
297
+ if (response.statusCode &&
298
+ response.statusCode >= 300 &&
299
+ response.statusCode < 400 &&
300
+ response.headers.location) {
301
+ file.close();
302
+ this.downloadFile(response.headers.location, targetPath).then(resolve).catch(reject);
303
+ return;
304
+ }
305
+ if (!response.statusCode || response.statusCode >= 400) {
306
+ file.close();
307
+ reject(new Error(`Download failed: HTTP ${response.statusCode}`));
308
+ return;
309
+ }
310
+ response.pipe(file);
311
+ file.on("finish", () => {
312
+ file.close();
313
+ resolve();
314
+ });
315
+ });
316
+ request.on("error", (err) => reject(err));
317
+ file.on("error", (err) => reject(err));
318
+ });
319
+ });
320
+ }
321
+ delay(ms) {
322
+ return new Promise((resolve) => setTimeout(resolve, ms));
323
+ }
324
+ withRetry(fn, retries) {
325
+ return __awaiter(this, void 0, void 0, function* () {
326
+ let lastError;
327
+ for (let i = 0; i < retries; i++) {
328
+ try {
329
+ return yield fn();
330
+ }
331
+ catch (error) {
332
+ lastError = error;
333
+ if (i < retries - 1) {
334
+ yield this.delay(300);
335
+ }
336
+ }
337
+ }
338
+ throw lastError;
339
+ });
340
+ }
341
+ }
342
+ exports.SyncStorageRunner = SyncStorageRunner;
343
+ SyncStorageRunner.VERSION = "v0.2.12";
344
+ SyncStorageRunner.STARTUP_TIMEOUT_MS = 30000;
345
+ SyncStorageRunner.STARTUP_POLL_MS = 1000;
346
+ SyncStorageRunner.PROCESS_WARMUP_MS = 2000;
347
+ SyncStorageRunner.REQUEST_TIMEOUT_MS = 15000;
348
+ SyncStorageRunner.RETRY_COUNT = 3;
349
+ SyncStorageRunner.IN_PROGRESS_POLL_MS = 200;
350
+ SyncStorageRunner.REPO_BASE = "https://github.com/testit-tms/sync-storage-public/releases/download";
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const syncstorage_runner_1 = require("./syncstorage.runner");
13
+ function makeConfig() {
14
+ return {
15
+ url: "http://localhost:8080",
16
+ privateToken: "token",
17
+ projectId: "11111111-1111-1111-1111-111111111111",
18
+ configurationId: "22222222-2222-2222-2222-222222222222",
19
+ testRunId: "33333333-3333-3333-3333-333333333333",
20
+ syncStorageEnabled: true,
21
+ syncStoragePort: "49152",
22
+ };
23
+ }
24
+ describe("SyncStorageRunner", () => {
25
+ it("should mark runner as active and resolve master status after start", () => __awaiter(void 0, void 0, void 0, function* () {
26
+ const runner = new syncstorage_runner_1.SyncStorageRunner("run-1", makeConfig());
27
+ const internal = runner;
28
+ internal.healthcheck = jest.fn().mockResolvedValue(true);
29
+ internal.workersApi = { registerPost: jest.fn().mockResolvedValue({ is_master: true }) };
30
+ const started = yield runner.start();
31
+ expect(started).toBe(true);
32
+ expect(runner.isActive()).toBe(true);
33
+ expect(runner.isMasterWorker()).toBe(true);
34
+ }));
35
+ it("should send in-progress result only once", () => __awaiter(void 0, void 0, void 0, function* () {
36
+ const runner = new syncstorage_runner_1.SyncStorageRunner("run-1", makeConfig());
37
+ const internal = runner;
38
+ internal.healthcheck = jest.fn().mockResolvedValue(true);
39
+ internal.workersApi = { registerPost: jest.fn().mockResolvedValue({ is_master: true }) };
40
+ internal.testResultsApi = { inProgressTestResultPost: jest.fn().mockResolvedValue({ status: "ok" }) };
41
+ internal.testResultCutModel = { constructFromObject: (obj) => obj };
42
+ yield runner.start();
43
+ const first = yield runner.sendInProgressTestResult({
44
+ projectId: "11111111-1111-1111-1111-111111111111",
45
+ autoTestExternalId: "A",
46
+ statusCode: "Passed",
47
+ statusType: "Succeeded",
48
+ startedOn: new Date(),
49
+ });
50
+ const second = yield runner.sendInProgressTestResult({
51
+ projectId: "11111111-1111-1111-1111-111111111111",
52
+ autoTestExternalId: "B",
53
+ statusCode: "Passed",
54
+ statusType: "Succeeded",
55
+ startedOn: new Date(),
56
+ });
57
+ expect(first).toBe(true);
58
+ expect(second).toBe(false);
59
+ }));
60
+ it("should skip publish when payload is incomplete", () => __awaiter(void 0, void 0, void 0, function* () {
61
+ const runner = new syncstorage_runner_1.SyncStorageRunner("run-1", makeConfig());
62
+ const internal = runner;
63
+ internal.healthcheck = jest.fn().mockResolvedValue(true);
64
+ internal.workersApi = { registerPost: jest.fn().mockResolvedValue({ is_master: true }) };
65
+ internal.testResultsApi = { inProgressTestResultPost: jest.fn().mockResolvedValue({ status: "ok" }) };
66
+ internal.testResultCutModel = { constructFromObject: (obj) => obj };
67
+ yield runner.start();
68
+ const ok = yield runner.sendInProgressTestResult({
69
+ projectId: "",
70
+ autoTestExternalId: "A",
71
+ statusCode: "Passed",
72
+ statusType: "Succeeded",
73
+ startedOn: new Date(),
74
+ });
75
+ expect(ok).toBe(false);
76
+ expect(internal.testResultsApi.inProgressTestResultPost).not.toHaveBeenCalled();
77
+ }));
78
+ });
@@ -0,0 +1,21 @@
1
+ import { Outcome } from "../../common";
2
+ import { AutotestResult } from "../testruns";
3
+ export type WorkerStatus = "in_progress" | "completed";
4
+ export interface TestResultCutModel {
5
+ projectId: string;
6
+ autoTestExternalId: string;
7
+ statusCode: Outcome;
8
+ statusType: string;
9
+ startedOn?: Date;
10
+ }
11
+ export interface ISyncStorageRunner {
12
+ start(): Promise<boolean>;
13
+ isActive(): boolean;
14
+ isMasterWorker(): boolean;
15
+ isAlreadyInProgress(): boolean;
16
+ waitForInProgressPublished(timeoutMs: number): Promise<boolean>;
17
+ sendInProgressTestResult(model: TestResultCutModel): Promise<boolean>;
18
+ setWorkerStatus(status: WorkerStatus): Promise<void>;
19
+ completeProcessing(): Promise<void>;
20
+ }
21
+ export declare function toTestResultCutModel(result: AutotestResult, projectId: string): TestResultCutModel;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toTestResultCutModel = toTestResultCutModel;
4
+ /** Same mapping as TestRunConverter.mapToStatusType — TMS expects both fields on the cut model. */
5
+ const OUTCOME_TO_STATUS_TYPE = {
6
+ Passed: "Succeeded",
7
+ Failed: "Failed",
8
+ Blocked: "Incomplete",
9
+ Skipped: "Incomplete",
10
+ };
11
+ function toTestResultCutModel(result, projectId) {
12
+ return {
13
+ projectId,
14
+ autoTestExternalId: result.autoTestExternalId,
15
+ statusCode: result.outcome,
16
+ statusType: OUTCOME_TO_STATUS_TYPE[result.outcome],
17
+ startedOn: result.startedOn,
18
+ };
19
+ }
@@ -6,6 +6,7 @@ export interface ITestRunConverter {
6
6
  toLocalState(state: TestRunState): RunState;
7
7
  toLocalTestRun(testRun: TestRunV2ApiResult): TestRunGet;
8
8
  toOriginAutotestResult(autotest: AutotestResult): AutoTestResultsForTestRunModel;
9
+ toOriginAutotestResultInProgress(autotest: AutotestResult): AutoTestResultsForTestRunModel;
9
10
  }
10
11
  export declare class TestRunConverter extends BaseConverter implements ITestRunConverter {
11
12
  private autotestConverter;
@@ -13,6 +14,7 @@ export declare class TestRunConverter extends BaseConverter implements ITestRunC
13
14
  toLocalState(state: TestRunState): RunState;
14
15
  toOriginState(state: RunState): TestRunState;
15
16
  mapToStatusType(status: Outcome): string;
17
+ toOriginAutotestResultInProgress(autotest: AutotestResult): AutoTestResultsForTestRunModel;
16
18
  toOriginAutotestResult(autotest: AutotestResult): AutoTestResultsForTestRunModel;
17
19
  toLocalTestRun(testRun: TestRunV2ApiResult): TestRunGet;
18
20
  }
@@ -26,6 +26,9 @@ class TestRunConverter extends common_1.BaseConverter {
26
26
  };
27
27
  return statusMap[status];
28
28
  }
29
+ toOriginAutotestResultInProgress(autotest) {
30
+ return Object.assign(Object.assign({}, this.toOriginAutotestResult(autotest)), { statusType: "InProgress" });
31
+ }
29
32
  toOriginAutotestResult(autotest) {
30
33
  var _a, _b, _c, _d;
31
34
  const model = {
@@ -12,5 +12,7 @@ export declare class TestRunsService extends BaseService implements ITestRunsSer
12
12
  updateTestRun(testRun: TestRunGet): Promise<void>;
13
13
  startTestRun(testRunId: TestRunId): Promise<void>;
14
14
  completeTestRun(testRunId: TestRunId): Promise<void>;
15
+ postInProgressAutotestResult(testRunId: string, result: AutotestResult): Promise<void>;
15
16
  loadAutotests(testRunId: string, results: Array<AutotestResult>): Promise<void>;
17
+ private sendAutotestResultWithRetry;
16
18
  }
@@ -138,12 +138,73 @@ class TestRunsService extends common_1.BaseService {
138
138
  }
139
139
  });
140
140
  }
141
+ postInProgressAutotestResult(testRunId, result) {
142
+ return __awaiter(this, void 0, void 0, function* () {
143
+ const model = this._converter.toOriginAutotestResultInProgress(result);
144
+ (0, utils_1.escapeHtmlInObjectArray)([model]);
145
+ (0, utils_1.logTmsLoadTestRun)("POST setAutoTestResults (InProgress stub)", {
146
+ testRunId,
147
+ autoTestExternalId: model.autoTestExternalId,
148
+ statusType: model.statusType,
149
+ statusCode: model.statusCode,
150
+ hasStartedOn: Boolean(model.startedOn),
151
+ });
152
+ yield this._client.setAutoTestResultsForTestRun(testRunId, { autoTestResultsForTestRunModel: [model] });
153
+ (0, utils_1.logTmsLoadTestRun)("POST setAutoTestResults (InProgress stub) done", {
154
+ autoTestExternalId: model.autoTestExternalId,
155
+ });
156
+ });
157
+ }
141
158
  loadAutotests(testRunId, results) {
142
159
  return __awaiter(this, void 0, void 0, function* () {
160
+ var _a, _b;
143
161
  const autotestResultsForTestRun = results.map((result) => this._converter.toOriginAutotestResult(result));
144
162
  (0, utils_1.escapeHtmlInObjectArray)(autotestResultsForTestRun);
145
163
  for (const autotestResult of autotestResultsForTestRun) {
146
- yield this._client.setAutoTestResultsForTestRun(testRunId, { autoTestResultsForTestRunModel: [autotestResult] });
164
+ (0, utils_1.logTmsLoadTestRun)("POST setAutoTestResults (final)", {
165
+ testRunId,
166
+ autoTestExternalId: autotestResult.autoTestExternalId,
167
+ statusType: autotestResult.statusType,
168
+ statusCode: autotestResult.statusCode,
169
+ stepCount: (_b = (_a = autotestResult.stepResults) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0,
170
+ });
171
+ yield this.sendAutotestResultWithRetry(testRunId, autotestResult).catch((err) => {
172
+ var _a, _b;
173
+ const normalized = (_b = (_a = err === null || err === void 0 ? void 0 : err.body) !== null && _a !== void 0 ? _a : err === null || err === void 0 ? void 0 : err.error) !== null && _b !== void 0 ? _b : err;
174
+ console.error("[testit-js-commons:loadTestRun] FAILED to post final result", {
175
+ testRunId,
176
+ autoTestExternalId: autotestResult.autoTestExternalId,
177
+ error: normalized,
178
+ });
179
+ });
180
+ }
181
+ });
182
+ }
183
+ sendAutotestResultWithRetry(testRunId, autotestResult) {
184
+ return __awaiter(this, void 0, void 0, function* () {
185
+ var _a, _b, _c;
186
+ const maxAttempts = 3;
187
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
188
+ try {
189
+ yield this._client.setAutoTestResultsForTestRun(testRunId, { autoTestResultsForTestRunModel: [autotestResult] });
190
+ return;
191
+ }
192
+ catch (err) {
193
+ const code = err === null || err === void 0 ? void 0 : err.code;
194
+ const status = (_a = err === null || err === void 0 ? void 0 : err.status) !== null && _a !== void 0 ? _a : err === null || err === void 0 ? void 0 : err.statusCode;
195
+ const msg = String((_c = (_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err) !== null && _c !== void 0 ? _c : "");
196
+ const transient = code === "ECONNRESET" ||
197
+ code === "ETIMEDOUT" ||
198
+ code === "EPIPE" ||
199
+ code === "ECONNABORTED" ||
200
+ msg.includes("socket hang up") ||
201
+ msg.includes("read ECONNRESET") ||
202
+ (typeof status === "number" && status >= 500);
203
+ if (!transient || attempt === maxAttempts) {
204
+ throw err;
205
+ }
206
+ yield new Promise((resolve) => setTimeout(resolve, 300 * attempt));
207
+ }
147
208
  }
148
209
  });
149
210
  }
@@ -58,6 +58,8 @@ export interface ITestRunsService {
58
58
  updateTestRun(testRun: TestRunGet): Promise<void>;
59
59
  startTestRun(testRunId: TestRunId): Promise<void>;
60
60
  completeTestRun(testRunId: TestRunId): Promise<void>;
61
+ /** First TMS write as InProgress (mirrors Python realtime in-progress before final load). */
62
+ postInProgressAutotestResult(testRunId: string, result: AutotestResult): Promise<void>;
61
63
  loadAutotests(testRunId: string, autotests: Array<AutotestResult>): Promise<void>;
62
64
  }
63
65
  export {};
@@ -4,13 +4,17 @@ import { AutotestPost, AutotestResult, TestRunId } from "../services";
4
4
  import { IStrategy } from "./strategy.type";
5
5
  export declare class BaseStrategy implements IStrategy {
6
6
  protected config: AdapterConfig;
7
+ private static readonly INPROGRESS_FIRST_GRACE_MS;
7
8
  client: IClient;
8
9
  testRunId: Promise<TestRunId>;
10
+ private syncStorageRunner?;
9
11
  protected constructor(config: AdapterConfig);
10
12
  setup(): Promise<void>;
11
13
  teardown(): Promise<void>;
12
14
  loadAutotest(autotest: AutotestPost, status: string): Promise<void>;
13
15
  private updateTestLinkToWorkItems;
14
16
  loadTestRun(autotests: AutotestResult[]): Promise<void>;
17
+ private tryStartSyncStorage;
15
18
  protected updateTestRun(config: AdapterConfig): Promise<void>;
19
+ private getInProgressFirstGraceMs;
16
20
  }