@desplega.ai/qa-use 2.15.0 → 2.15.2
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/lib/api/index.d.ts +6 -5
- package/dist/lib/api/index.d.ts.map +1 -1
- package/dist/lib/api/index.js +112 -5
- package/dist/lib/api/index.js.map +1 -1
- package/dist/lib/api/sse.d.ts +22 -2
- package/dist/lib/api/sse.d.ts.map +1 -1
- package/dist/lib/api/sse.js +77 -5
- package/dist/lib/api/sse.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/src/cli/commands/test/create.d.ts +10 -0
- package/dist/src/cli/commands/test/create.d.ts.map +1 -0
- package/dist/src/cli/commands/test/create.js +82 -0
- package/dist/src/cli/commands/test/create.js.map +1 -0
- package/dist/src/cli/commands/test/index.d.ts.map +1 -1
- package/dist/src/cli/commands/test/index.js +2 -0
- package/dist/src/cli/commands/test/index.js.map +1 -1
- package/dist/src/cli/commands/test/run.d.ts.map +1 -1
- package/dist/src/cli/commands/test/run.js +4 -1
- package/dist/src/cli/commands/test/run.js.map +1 -1
- package/dist/src/cli/commands/test/schema.d.ts.map +1 -1
- package/dist/src/cli/commands/test/schema.js +51 -0
- package/dist/src/cli/commands/test/schema.js.map +1 -1
- package/dist/src/cli/commands/test/sync.d.ts +19 -0
- package/dist/src/cli/commands/test/sync.d.ts.map +1 -1
- package/dist/src/cli/commands/test/sync.js +92 -58
- package/dist/src/cli/commands/test/sync.js.map +1 -1
- package/dist/src/cli/commands/test/validate.d.ts.map +1 -1
- package/dist/src/cli/commands/test/validate.js +13 -0
- package/dist/src/cli/commands/test/validate.js.map +1 -1
- package/dist/src/cli/lib/runner.d.ts +6 -0
- package/dist/src/cli/lib/runner.d.ts.map +1 -1
- package/dist/src/cli/lib/runner.js +2 -2
- package/dist/src/cli/lib/runner.js.map +1 -1
- package/lib/api/index.ts +137 -10
- package/lib/api/sse.test.ts +530 -0
- package/lib/api/sse.ts +105 -5
- package/package.json +1 -1
package/lib/api/index.ts
CHANGED
|
@@ -338,10 +338,7 @@ export interface ImportedTest {
|
|
|
338
338
|
export interface ImportResult {
|
|
339
339
|
success: boolean;
|
|
340
340
|
imported: ImportedTest[];
|
|
341
|
-
errors:
|
|
342
|
-
name: string;
|
|
343
|
-
error: string;
|
|
344
|
-
}>;
|
|
341
|
+
errors: ValidationError[];
|
|
345
342
|
}
|
|
346
343
|
|
|
347
344
|
// Re-export TestDefinition for convenience
|
|
@@ -840,9 +837,89 @@ export class ApiClient {
|
|
|
840
837
|
* Run test definitions with SSE streaming progress
|
|
841
838
|
* @param options - Test execution options
|
|
842
839
|
* @param onEvent - Optional callback for SSE events
|
|
840
|
+
* @param runtimeOptions - Optional runtime controls (idle timeout, external signal)
|
|
843
841
|
* @returns Promise resolving to test result
|
|
844
842
|
*/
|
|
845
|
-
async runCliTest(
|
|
843
|
+
async runCliTest(
|
|
844
|
+
options: RunCliTestOptions,
|
|
845
|
+
onEvent?: SSECallback,
|
|
846
|
+
runtimeOptions?: { idleTimeoutSec?: number; signal?: AbortSignal }
|
|
847
|
+
): Promise<RunCliTestResult> {
|
|
848
|
+
// Internal AbortController is wired into both `fetch` and `streamSSE` so
|
|
849
|
+
// we can deterministically tear down the underlying TCP socket on every
|
|
850
|
+
// exit path. Phase 2 actively aborts on terminal SSE events (`complete` /
|
|
851
|
+
// `error`) so we don't wait for the backend to close the stream.
|
|
852
|
+
// Phase 3 widens the public signature to accept a caller signal and an
|
|
853
|
+
// idle-timeout watchdog (`--timeout` end-to-end).
|
|
854
|
+
const controller = new AbortController();
|
|
855
|
+
|
|
856
|
+
// Tracks why the loop terminated so the catch block can distinguish
|
|
857
|
+
// "we aborted on purpose because of `complete`/`error`" (swallow the
|
|
858
|
+
// AbortError, return result) from "real network error or external abort"
|
|
859
|
+
// (re-throw), or from "idle timeout fired" (throw a typed timeout error).
|
|
860
|
+
//
|
|
861
|
+
// The reason is mutated from inside async callbacks (idle watchdog
|
|
862
|
+
// setTimeout, caller signal abort listener) which TS can't see during
|
|
863
|
+
// control-flow analysis. We use a small wrapper object so TS doesn't
|
|
864
|
+
// narrow the type to the literal assigned in the synchronous flow.
|
|
865
|
+
type TerminationReason = 'complete' | 'idle-timeout' | 'external' | null;
|
|
866
|
+
const reasonRef: { current: TerminationReason } = { current: null };
|
|
867
|
+
let result: RunCliTestResult | null = null;
|
|
868
|
+
|
|
869
|
+
const idleTimeoutSec = runtimeOptions?.idleTimeoutSec;
|
|
870
|
+
const idleTimeoutMs =
|
|
871
|
+
idleTimeoutSec && idleTimeoutSec > 0 ? Math.round(idleTimeoutSec * 1000) : 0;
|
|
872
|
+
let idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
873
|
+
|
|
874
|
+
const clearIdleTimer = () => {
|
|
875
|
+
if (idleTimer !== null) {
|
|
876
|
+
clearTimeout(idleTimer);
|
|
877
|
+
idleTimer = null;
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
const armIdleTimer = () => {
|
|
882
|
+
if (idleTimeoutMs <= 0) return;
|
|
883
|
+
clearIdleTimer();
|
|
884
|
+
idleTimer = setTimeout(() => {
|
|
885
|
+
// Idle watchdog fired — no SSE bytes for `idleTimeoutSec` seconds.
|
|
886
|
+
// Mark the reason BEFORE aborting so the catch block can throw a
|
|
887
|
+
// typed timeout error instead of swallowing the AbortError.
|
|
888
|
+
reasonRef.current = 'idle-timeout';
|
|
889
|
+
controller.abort();
|
|
890
|
+
}, idleTimeoutMs);
|
|
891
|
+
// Don't keep the event loop alive just for the watchdog.
|
|
892
|
+
if (typeof idleTimer === 'object' && idleTimer && 'unref' in idleTimer) {
|
|
893
|
+
(idleTimer as { unref: () => void }).unref();
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
// Wire caller-provided signal: if it aborts, mark `terminationReason` so
|
|
898
|
+
// the catch block re-throws (instead of swallowing) with the abort error.
|
|
899
|
+
const callerSignal = runtimeOptions?.signal;
|
|
900
|
+
let callerAbortListener: (() => void) | null = null;
|
|
901
|
+
if (callerSignal) {
|
|
902
|
+
if (callerSignal.aborted) {
|
|
903
|
+
reasonRef.current = 'external';
|
|
904
|
+
controller.abort();
|
|
905
|
+
} else {
|
|
906
|
+
callerAbortListener = () => {
|
|
907
|
+
if (reasonRef.current === null) {
|
|
908
|
+
reasonRef.current = 'external';
|
|
909
|
+
}
|
|
910
|
+
controller.abort();
|
|
911
|
+
};
|
|
912
|
+
callerSignal.addEventListener('abort', callerAbortListener, { once: true });
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Merge caller signal with internal controller.signal so `fetch` and
|
|
917
|
+
// `streamSSE` see whichever fires first. `AbortSignal.any` is Node 20.3+
|
|
918
|
+
// and the project requires Node 20+.
|
|
919
|
+
const mergedSignal: AbortSignal = callerSignal
|
|
920
|
+
? AbortSignal.any([callerSignal, controller.signal])
|
|
921
|
+
: controller.signal;
|
|
922
|
+
|
|
846
923
|
try {
|
|
847
924
|
const response = await fetch(`${this.getApiUrl()}/vibe-qa/cli/run`, {
|
|
848
925
|
method: 'POST',
|
|
@@ -853,6 +930,7 @@ export class ApiClient {
|
|
|
853
930
|
Accept: 'text/event-stream',
|
|
854
931
|
},
|
|
855
932
|
body: JSON.stringify(options),
|
|
933
|
+
signal: mergedSignal,
|
|
856
934
|
});
|
|
857
935
|
|
|
858
936
|
if (!response.ok) {
|
|
@@ -863,28 +941,77 @@ export class ApiClient {
|
|
|
863
941
|
throw new Error(formatApiError(errorData, `HTTP ${response.status}: Failed to run test`));
|
|
864
942
|
}
|
|
865
943
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
//
|
|
869
|
-
|
|
944
|
+
// Arm the idle watchdog AFTER headers arrive — first event may take
|
|
945
|
+
// a while on a cold backend / large test. We don't punish slow first
|
|
946
|
+
// bytes; we punish silence between bytes.
|
|
947
|
+
armIdleTimer();
|
|
948
|
+
|
|
949
|
+
// Stream SSE events. `onChunk` resets the idle watchdog on every
|
|
950
|
+
// byte chunk (so SSE comment pings — which yield zero parsed events
|
|
951
|
+
// — still keep the connection alive).
|
|
952
|
+
for await (const event of streamSSE(response, {
|
|
953
|
+
signal: mergedSignal,
|
|
954
|
+
onChunk: () => armIdleTimer(),
|
|
955
|
+
})) {
|
|
870
956
|
if (onEvent) onEvent(event);
|
|
871
957
|
|
|
872
|
-
// Capture final result
|
|
958
|
+
// Capture final result and tear down the socket immediately on a
|
|
959
|
+
// terminal event. Trailing events (e.g. `test_fixed`, `persisted`)
|
|
960
|
+
// that may arrive after `complete` are intentionally dropped per the
|
|
961
|
+
// plan — the backend keeps the SSE stream open, so waiting for it to
|
|
962
|
+
// close on its own is what causes the ~80s hang we're fixing.
|
|
873
963
|
if (event.event === 'complete' || event.event === 'error') {
|
|
874
964
|
result = event.data;
|
|
965
|
+
reasonRef.current = 'complete';
|
|
966
|
+
controller.abort();
|
|
967
|
+
break;
|
|
875
968
|
}
|
|
876
969
|
}
|
|
877
970
|
|
|
878
971
|
if (!result) {
|
|
972
|
+
// No terminal event arrived AND the stream closed (or was aborted).
|
|
973
|
+
// If we got here via idle timeout, surface that explicitly.
|
|
974
|
+
if (reasonRef.current === 'idle-timeout') {
|
|
975
|
+
throw new Error(`Test run timed out: no SSE events for ${idleTimeoutSec}s`);
|
|
976
|
+
}
|
|
879
977
|
throw new Error('No result received from test execution');
|
|
880
978
|
}
|
|
881
979
|
|
|
882
980
|
return result;
|
|
883
981
|
} catch (error) {
|
|
982
|
+
// If we triggered the abort ourselves on a terminal event, the for-await
|
|
983
|
+
// loop may surface the abort as an AbortError after we've already
|
|
984
|
+
// captured `result`. Swallow it and return the captured result.
|
|
985
|
+
if (
|
|
986
|
+
reasonRef.current === 'complete' &&
|
|
987
|
+
error instanceof Error &&
|
|
988
|
+
error.name === 'AbortError' &&
|
|
989
|
+
result
|
|
990
|
+
) {
|
|
991
|
+
return result;
|
|
992
|
+
}
|
|
993
|
+
// Idle-timeout: the abort came from our watchdog, not from a real
|
|
994
|
+
// network failure. Surface a typed message that tools (and humans)
|
|
995
|
+
// can grep for.
|
|
996
|
+
if (
|
|
997
|
+
reasonRef.current === 'idle-timeout' &&
|
|
998
|
+
error instanceof Error &&
|
|
999
|
+
error.name === 'AbortError'
|
|
1000
|
+
) {
|
|
1001
|
+
throw new Error(`Test run timed out: no SSE events for ${idleTimeoutSec}s`);
|
|
1002
|
+
}
|
|
884
1003
|
if (error instanceof Error) {
|
|
885
1004
|
throw error;
|
|
886
1005
|
}
|
|
887
1006
|
throw new Error('Unknown error running test');
|
|
1007
|
+
} finally {
|
|
1008
|
+
// Defensive teardown: abort is idempotent. Guarantees the socket is
|
|
1009
|
+
// released even if the consumer above threw mid-iteration.
|
|
1010
|
+
controller.abort();
|
|
1011
|
+
clearIdleTimer();
|
|
1012
|
+
if (callerSignal && callerAbortListener) {
|
|
1013
|
+
callerSignal.removeEventListener('abort', callerAbortListener);
|
|
1014
|
+
}
|
|
888
1015
|
}
|
|
889
1016
|
}
|
|
890
1017
|
|