@openclaw/plugin-inspector 0.3.24 → 0.3.26
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/CHANGELOG.md +31 -0
- package/README.md +141 -0
- package/package.json +4 -2
- package/src/api.js +7 -0
- package/src/artifacts.js +22 -1
- package/src/batch.js +7 -12
- package/src/capture-api.js +15 -0
- package/src/capture-cli.js +1 -1
- package/src/capture-config.js +10 -7
- package/src/cli.js +6 -2
- package/src/cold-import-readiness.js +3 -3
- package/src/fixture-summary.js +43 -6
- package/src/import-loop-profile.js +56 -11
- package/src/inspector.js +120 -99
- package/src/mock-sdk-capture-runner.js +61 -22
- package/src/openclaw-target.js +110 -5
- package/src/openclaw-version.js +121 -9
- package/src/process-profile.js +255 -107
- package/src/runtime-capture-report.js +6 -0
- package/src/runtime-imports.js +191 -0
- package/src/runtime-profile.js +4 -0
- package/src/runtime-reconciliation.js +0 -6
- package/src/sdk-mock.js +128 -13
- package/src/synthetic-entrypoint.js +19 -19
- package/src/synthetic-probes-cli.js +79 -2
- package/src/synthetic-probes.js +212 -34
package/src/synthetic-probes.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
|
2
|
+
import { resolveProcessLimits } from "./process-profile.js";
|
|
2
3
|
|
|
3
4
|
export const syntheticRegistrationExecutionProfiles = {
|
|
4
5
|
createChatChannelPlugin: {
|
|
@@ -46,6 +47,11 @@ export const syntheticRegistrationExecutionProfiles = {
|
|
|
46
47
|
callableProperties: [],
|
|
47
48
|
reason: "auto-enable probes are captured as registration metadata before runtime activation checks",
|
|
48
49
|
},
|
|
50
|
+
registerBoardWidgetContentKind: {
|
|
51
|
+
mode: "metadata-only",
|
|
52
|
+
callableProperties: [],
|
|
53
|
+
reason: "board widget content kinds are captured as registration metadata before source validation or document composition",
|
|
54
|
+
},
|
|
49
55
|
registerCli: {
|
|
50
56
|
mode: "direct",
|
|
51
57
|
callableProperties: ["handler", "run", "execute"],
|
|
@@ -126,6 +132,16 @@ export const syntheticRegistrationExecutionProfiles = {
|
|
|
126
132
|
callableProperties: [],
|
|
127
133
|
reason: "hosted media resolvers are captured as registration metadata before media URL resolution",
|
|
128
134
|
},
|
|
135
|
+
registerMcpServerConnectionResolver: {
|
|
136
|
+
mode: "metadata-only",
|
|
137
|
+
callableProperties: [],
|
|
138
|
+
reason: "MCP server connection resolvers are captured as registration metadata before requester-bound transport resolution",
|
|
139
|
+
},
|
|
140
|
+
registerMemoryPromptPreparation: {
|
|
141
|
+
mode: "metadata-only",
|
|
142
|
+
callableProperties: [],
|
|
143
|
+
reason: "memory prompt preparation callbacks are captured as registration metadata before prompt-runtime execution",
|
|
144
|
+
},
|
|
129
145
|
registerMemoryPromptSection: {
|
|
130
146
|
mode: "metadata-only",
|
|
131
147
|
callableProperties: [],
|
|
@@ -275,6 +291,11 @@ export const syntheticRegistrationExecutionProfiles = {
|
|
|
275
291
|
callableProperties: [],
|
|
276
292
|
reason: "text transforms are captured as registration metadata before content mutation execution",
|
|
277
293
|
},
|
|
294
|
+
registerTranscriptSourceProvider: {
|
|
295
|
+
mode: "metadata-only",
|
|
296
|
+
callableProperties: [],
|
|
297
|
+
reason: "transcript source providers are captured as registration metadata before live capture or transcript import",
|
|
298
|
+
},
|
|
278
299
|
registerVideoGenerationProvider: {
|
|
279
300
|
mode: "metadata-only",
|
|
280
301
|
callableProperties: [],
|
|
@@ -300,6 +321,11 @@ export const syntheticRegistrationExecutionProfiles = {
|
|
|
300
321
|
callableProperties: [],
|
|
301
322
|
reason: "widget presenters are captured as registration metadata before presentation runtime execution",
|
|
302
323
|
},
|
|
324
|
+
registerWorkerProvider: {
|
|
325
|
+
mode: "metadata-only",
|
|
326
|
+
callableProperties: [],
|
|
327
|
+
reason: "worker providers are captured as registration metadata before cloud-worker lifecycle execution",
|
|
328
|
+
},
|
|
303
329
|
};
|
|
304
330
|
|
|
305
331
|
export const defaultSyntheticHookEvents = {
|
|
@@ -483,6 +509,7 @@ export const defaultSyntheticRegistrationProbeInputs = {
|
|
|
483
509
|
registerGatewayMethod: {
|
|
484
510
|
execute: gatewayProbeArgs,
|
|
485
511
|
handler: gatewayProbeArgs,
|
|
512
|
+
invoke: gatewayProbeArgs,
|
|
486
513
|
run: gatewayProbeArgs,
|
|
487
514
|
},
|
|
488
515
|
registerHttpRoute: {
|
|
@@ -597,8 +624,28 @@ export async function writeSyntheticProbePlan(plan, options = {}) {
|
|
|
597
624
|
}
|
|
598
625
|
|
|
599
626
|
export async function runCapturedSyntheticProbes(capture, options = {}) {
|
|
627
|
+
options.signal?.throwIfAborted();
|
|
600
628
|
const hookEvents = options.hookEvents ?? defaultSyntheticHookEvents;
|
|
601
629
|
const hookContexts = options.hookContexts ?? defaultSyntheticHookContexts;
|
|
630
|
+
const { timeoutMs } = resolveProcessLimits(options, "PROBE");
|
|
631
|
+
const controller = new AbortController();
|
|
632
|
+
const onCancel = () => controller.abort(new Error("Synthetic probes cancelled"));
|
|
633
|
+
options.signal?.addEventListener("abort", onCancel, { once: true });
|
|
634
|
+
if (options.signal?.aborted) onCancel();
|
|
635
|
+
try {
|
|
636
|
+
const result = await runCapturedProbes(capture, {
|
|
637
|
+
...options, hookEvents, hookContexts, timeoutMs, controller, signal: controller.signal,
|
|
638
|
+
gatewayConfig: options.apiOptions?.config ?? {},
|
|
639
|
+
});
|
|
640
|
+
if (options.signal?.aborted) throw controller.signal.reason;
|
|
641
|
+
return result;
|
|
642
|
+
} finally {
|
|
643
|
+
options.signal?.removeEventListener("abort", onCancel);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function runCapturedProbes(capture, options) {
|
|
648
|
+
const { hookEvents, hookContexts, timeoutMs, controller, signal } = options;
|
|
602
649
|
const captured = capture.captured ?? [];
|
|
603
650
|
const retained = new Map((capture.retained ?? []).map((item) => [item.captureIndex, item]));
|
|
604
651
|
const resultsByCaptureIndex = new Map();
|
|
@@ -611,6 +658,10 @@ export async function runCapturedSyntheticProbes(capture, options = {}) {
|
|
|
611
658
|
);
|
|
612
659
|
|
|
613
660
|
for (const { entry, captureIndex } of executionEntries) {
|
|
661
|
+
if (signal.aborted) {
|
|
662
|
+
resultsByCaptureIndex.set(captureIndex, [blockedResult(entry, captureIndex, signal.reason.message)]);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
614
665
|
const retainedEntry = retained.get(captureIndex);
|
|
615
666
|
if (!retainedEntry) {
|
|
616
667
|
resultsByCaptureIndex.set(captureIndex, [blockedResult(entry, captureIndex, "handler retention was not enabled")]);
|
|
@@ -618,7 +669,7 @@ export async function runCapturedSyntheticProbes(capture, options = {}) {
|
|
|
618
669
|
}
|
|
619
670
|
if (entry.kind === "hook") {
|
|
620
671
|
resultsByCaptureIndex.set(captureIndex, [
|
|
621
|
-
await runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, hookContexts }),
|
|
672
|
+
await runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, hookContexts, timeoutMs, controller }),
|
|
622
673
|
]);
|
|
623
674
|
continue;
|
|
624
675
|
}
|
|
@@ -719,7 +770,7 @@ function probeBlocker({ hasSyntheticArguments, execution }) {
|
|
|
719
770
|
return null;
|
|
720
771
|
}
|
|
721
772
|
|
|
722
|
-
async function runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, hookContexts }) {
|
|
773
|
+
async function runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, hookContexts, timeoutMs, controller }) {
|
|
723
774
|
if (typeof retainedEntry.handler !== "function") {
|
|
724
775
|
return blockedResult(entry, captureIndex, "captured hook has no callable handler");
|
|
725
776
|
}
|
|
@@ -728,6 +779,8 @@ async function runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, ho
|
|
|
728
779
|
kind: "hook",
|
|
729
780
|
seam: entry.name,
|
|
730
781
|
label: entry.name,
|
|
782
|
+
timeoutMs,
|
|
783
|
+
controller,
|
|
731
784
|
invoke: () =>
|
|
732
785
|
retainedEntry.handler(
|
|
733
786
|
hookEvents[entry.name] ?? { hook: entry.name },
|
|
@@ -746,10 +799,22 @@ async function runRegistrationProbes(entry, retainedEntry, captureIndex, options
|
|
|
746
799
|
}
|
|
747
800
|
|
|
748
801
|
const descriptor =
|
|
749
|
-
|
|
802
|
+
entry.name === "registerGatewayMethod" && typeof retainedEntry.arguments?.[0] === "string"
|
|
803
|
+
? retainedEntry.returnValue
|
|
804
|
+
: retainedEntry.arguments?.find((value) => value && typeof value === "object") ?? retainedEntry.returnValue;
|
|
750
805
|
if (!descriptor || typeof descriptor !== "object") {
|
|
751
806
|
return [blockedResult(entry, captureIndex, "captured registration has no object descriptor")];
|
|
752
807
|
}
|
|
808
|
+
const method = descriptor.method ?? descriptor.name;
|
|
809
|
+
if (entry.name === "registerGatewayMethod" && Object.hasOwn(options.gatewayMethodPrerequisites ?? {}, method)) {
|
|
810
|
+
const reason = options.gatewayMethodPrerequisites[method];
|
|
811
|
+
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
812
|
+
throw new TypeError(`Gateway probe prerequisite for ${method} must be a non-empty string`);
|
|
813
|
+
}
|
|
814
|
+
// A method needing host state or live credentials cannot be exercised with
|
|
815
|
+
// the default empty request. Record the missing prerequisite before calling it.
|
|
816
|
+
return [{ ...blockedResult(entry, captureIndex, reason), method }];
|
|
817
|
+
}
|
|
753
818
|
if (profile.option && options[profile.option] !== true) {
|
|
754
819
|
return [blockedResult(entry, captureIndex, `captured registration requires ${profile.option}=true`)];
|
|
755
820
|
}
|
|
@@ -759,17 +824,29 @@ async function runRegistrationProbes(entry, retainedEntry, captureIndex, options
|
|
|
759
824
|
return [blockedResult(entry, captureIndex, "captured registration has no supported callable probe")];
|
|
760
825
|
}
|
|
761
826
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
827
|
+
// The profile owns lifecycle order; finish each callback before starting the next.
|
|
828
|
+
const results = [];
|
|
829
|
+
for (const invocation of invocations) {
|
|
830
|
+
if (options.signal.aborted) {
|
|
831
|
+
results.push({
|
|
832
|
+
...blockedResult(entry, captureIndex, options.signal.reason.message),
|
|
833
|
+
label: invocation.label,
|
|
834
|
+
});
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
results.push(
|
|
838
|
+
await runProbe({
|
|
765
839
|
captureIndex,
|
|
766
840
|
kind: "registration",
|
|
767
841
|
seam: entry.name,
|
|
768
842
|
label: invocation.label,
|
|
843
|
+
timeoutMs: options.timeoutMs,
|
|
844
|
+
controller: options.controller,
|
|
769
845
|
invoke: invocation.invoke,
|
|
770
846
|
}),
|
|
771
|
-
)
|
|
772
|
-
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
return results;
|
|
773
850
|
}
|
|
774
851
|
|
|
775
852
|
function registrationInvocations(registrar, descriptor, returnValue, profile, options) {
|
|
@@ -786,20 +863,100 @@ function registrationInvocations(registrar, descriptor, returnValue, profile, op
|
|
|
786
863
|
if (typeof callable === "function") {
|
|
787
864
|
invocations.push({
|
|
788
865
|
label: `${registrar}.${property}`,
|
|
789
|
-
invoke: () =>
|
|
866
|
+
invoke: () => registrar === "registerGatewayMethod"
|
|
867
|
+
? invokeGatewayCallable(callable, property, descriptor, options)
|
|
868
|
+
: invokeRegistrationCallable(callable, registrar, property, options),
|
|
790
869
|
});
|
|
870
|
+
// Gateway aliases describe one method, not independent lifecycle callbacks.
|
|
871
|
+
if (registrar === "registerGatewayMethod") break;
|
|
791
872
|
}
|
|
792
873
|
}
|
|
793
874
|
return invocations;
|
|
794
875
|
}
|
|
795
876
|
|
|
796
|
-
function invokeRegistrationCallable(
|
|
797
|
-
|
|
877
|
+
function invokeRegistrationCallable(
|
|
878
|
+
callable, registrar, property, options,
|
|
879
|
+
event = syntheticRegistrationEvent(registrar, property, options),
|
|
880
|
+
) {
|
|
798
881
|
const inputFactory = options.registrationProbeInputs?.[registrar]?.[property] ?? defaultSyntheticRegistrationProbeInputs[registrar]?.[property];
|
|
799
882
|
const args = inputFactory ? inputFactory(event, options) : [event];
|
|
800
883
|
return callable(...args);
|
|
801
884
|
}
|
|
802
885
|
|
|
886
|
+
async function invokeGatewayCallable(callable, property, descriptor, options) {
|
|
887
|
+
let responded = false;
|
|
888
|
+
let closed = false;
|
|
889
|
+
let resolveResponse;
|
|
890
|
+
const response = new Promise((resolve) => { resolveResponse = resolve; });
|
|
891
|
+
const onAbort = () => {
|
|
892
|
+
closed = true;
|
|
893
|
+
resolveResponse({ error: options.signal.reason });
|
|
894
|
+
};
|
|
895
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
896
|
+
const respond = (ok, payload, error) => {
|
|
897
|
+
if (closed || responded) return;
|
|
898
|
+
// Snapshot the first emission, including invalid frames. Later responses
|
|
899
|
+
// cannot recover it; logging metadata is not part of the wire envelope.
|
|
900
|
+
responded = true;
|
|
901
|
+
try {
|
|
902
|
+
resolveResponse({ frame: gatewayResponseFrame(ok, payload, error) });
|
|
903
|
+
} catch (error) {
|
|
904
|
+
resolveResponse({ error });
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
try {
|
|
908
|
+
options.signal.throwIfAborted();
|
|
909
|
+
const event = {
|
|
910
|
+
...syntheticRegistrationEvent("registerGatewayMethod", property, options),
|
|
911
|
+
method: descriptor.method ?? descriptor.name,
|
|
912
|
+
respond,
|
|
913
|
+
};
|
|
914
|
+
const result = await invokeRegistrationCallable(callable, "registerGatewayMethod", property, options, event);
|
|
915
|
+
// OpenClaw's plugin registrar adapts returns only without an explicit reply.
|
|
916
|
+
if (!responded && result !== undefined) respond(true, result);
|
|
917
|
+
// A void handler may respond later. The existing probe deadline bounds this
|
|
918
|
+
// observation as well as handler settlement; it owns timeout/cancellation.
|
|
919
|
+
const outcome = await response;
|
|
920
|
+
if (outcome.error) throw outcome.error;
|
|
921
|
+
if (!outcome.frame.ok) {
|
|
922
|
+
throw new Error(`Gateway response error: ${outcome.frame.error?.message ?? "request failed"}`);
|
|
923
|
+
}
|
|
924
|
+
return outcome.frame;
|
|
925
|
+
} finally {
|
|
926
|
+
closed = true;
|
|
927
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function gatewayResponseFrame(ok, payload, error) {
|
|
932
|
+
let frame;
|
|
933
|
+
try {
|
|
934
|
+
frame = JSON.parse(JSON.stringify({ type: "res", id: "fixture-request", ok, payload, error }));
|
|
935
|
+
} catch {
|
|
936
|
+
throw new Error("Gateway response is not JSON serializable");
|
|
937
|
+
}
|
|
938
|
+
// ResponseFrameSchema / ErrorShapeSchema in OpenClaw v2026.9.3. Payload and
|
|
939
|
+
// error are optional independently of ok; error codes are nonempty strings.
|
|
940
|
+
const responseError = frame.error;
|
|
941
|
+
if (
|
|
942
|
+
typeof frame.ok !== "boolean" ||
|
|
943
|
+
(responseError !== undefined && (
|
|
944
|
+
!responseError ||
|
|
945
|
+
typeof responseError !== "object" ||
|
|
946
|
+
Array.isArray(responseError) ||
|
|
947
|
+
typeof responseError.code !== "string" || responseError.code.length === 0 ||
|
|
948
|
+
typeof responseError.message !== "string" || responseError.message.length === 0 ||
|
|
949
|
+
(responseError.retryable !== undefined && typeof responseError.retryable !== "boolean") ||
|
|
950
|
+
(responseError.retryAfterMs !== undefined &&
|
|
951
|
+
(!Number.isInteger(responseError.retryAfterMs) || responseError.retryAfterMs < 0)) ||
|
|
952
|
+
Object.keys(responseError).some((key) => !["code", "message", "details", "retryable", "retryAfterMs"].includes(key))
|
|
953
|
+
))
|
|
954
|
+
) {
|
|
955
|
+
throw new Error("Gateway response is malformed");
|
|
956
|
+
}
|
|
957
|
+
return frame;
|
|
958
|
+
}
|
|
959
|
+
|
|
803
960
|
function syntheticRegistrationEvent(registrar, property, options) {
|
|
804
961
|
const hookEvents = options.hookEvents ?? defaultSyntheticHookEvents;
|
|
805
962
|
const beforeToolCall = hookEvents.before_tool_call ?? defaultSyntheticHookEvents.before_tool_call;
|
|
@@ -822,21 +979,21 @@ function syntheticRegistrationEvent(registrar, property, options) {
|
|
|
822
979
|
};
|
|
823
980
|
}
|
|
824
981
|
|
|
825
|
-
function toolRunProbeArgs(event) {
|
|
982
|
+
function toolRunProbeArgs(event, options = {}) {
|
|
826
983
|
return [
|
|
827
984
|
event.params,
|
|
828
985
|
{
|
|
829
986
|
source: event.source,
|
|
830
987
|
toolName: event.toolName,
|
|
831
988
|
toolCallId: event.toolCall.id,
|
|
832
|
-
signal: new AbortController().signal,
|
|
989
|
+
signal: options.signal ?? new AbortController().signal,
|
|
833
990
|
logger: console,
|
|
834
991
|
},
|
|
835
992
|
];
|
|
836
993
|
}
|
|
837
994
|
|
|
838
|
-
function toolExecuteProbeArgs(event) {
|
|
839
|
-
return [event.toolCall.id, event.params, new AbortController().signal, () => undefined];
|
|
995
|
+
function toolExecuteProbeArgs(event, options = {}) {
|
|
996
|
+
return [event.toolCall.id, event.params, options.signal ?? new AbortController().signal, () => undefined];
|
|
840
997
|
}
|
|
841
998
|
|
|
842
999
|
function httpRouteProbeArgs(event) {
|
|
@@ -858,34 +1015,31 @@ function httpRouteProbeArgs(event) {
|
|
|
858
1015
|
];
|
|
859
1016
|
}
|
|
860
1017
|
|
|
861
|
-
function commandProbeArgs(event) {
|
|
1018
|
+
function commandProbeArgs(event, options = {}) {
|
|
862
1019
|
return [
|
|
863
1020
|
event.input,
|
|
864
1021
|
{
|
|
865
1022
|
source: event.source,
|
|
866
|
-
signal: new AbortController().signal,
|
|
1023
|
+
signal: options.signal ?? new AbortController().signal,
|
|
867
1024
|
logger: console,
|
|
868
1025
|
},
|
|
869
1026
|
];
|
|
870
1027
|
}
|
|
871
1028
|
|
|
872
|
-
function gatewayProbeArgs(event) {
|
|
1029
|
+
function gatewayProbeArgs(event, options = {}) {
|
|
873
1030
|
return [
|
|
874
1031
|
{
|
|
875
1032
|
...event,
|
|
876
|
-
params: event.params,
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
{
|
|
882
|
-
source: event.source,
|
|
883
|
-
logger: console,
|
|
1033
|
+
req: { type: "req", id: "fixture-request", method: event.method ?? "fixture.gateway.method", params: event.params },
|
|
1034
|
+
client: null,
|
|
1035
|
+
isWebchatConnect: () => false,
|
|
1036
|
+
context: { source: event.source, logger: console, getRuntimeConfig: () => options.gatewayConfig },
|
|
1037
|
+
signal: options.signal,
|
|
884
1038
|
},
|
|
885
1039
|
];
|
|
886
1040
|
}
|
|
887
1041
|
|
|
888
|
-
function channelSendProbeArgs(event) {
|
|
1042
|
+
function channelSendProbeArgs(event, options = {}) {
|
|
889
1043
|
return [
|
|
890
1044
|
{
|
|
891
1045
|
source: event.source,
|
|
@@ -896,12 +1050,12 @@ function channelSendProbeArgs(event) {
|
|
|
896
1050
|
replyToId: "fixture-reply",
|
|
897
1051
|
threadId: "fixture-thread",
|
|
898
1052
|
logger: console,
|
|
899
|
-
signal: new AbortController().signal,
|
|
1053
|
+
signal: options.signal ?? new AbortController().signal,
|
|
900
1054
|
},
|
|
901
1055
|
];
|
|
902
1056
|
}
|
|
903
1057
|
|
|
904
|
-
function channelReceiveProbeArgs(event) {
|
|
1058
|
+
function channelReceiveProbeArgs(event, options = {}) {
|
|
905
1059
|
return [
|
|
906
1060
|
{
|
|
907
1061
|
source: event.source,
|
|
@@ -921,7 +1075,7 @@ function channelReceiveProbeArgs(event) {
|
|
|
921
1075
|
to: "fixture-channel",
|
|
922
1076
|
},
|
|
923
1077
|
logger: console,
|
|
924
|
-
signal: new AbortController().signal,
|
|
1078
|
+
signal: options.signal ?? new AbortController().signal,
|
|
925
1079
|
},
|
|
926
1080
|
];
|
|
927
1081
|
}
|
|
@@ -939,7 +1093,7 @@ function interactiveProbeArgs(event) {
|
|
|
939
1093
|
];
|
|
940
1094
|
}
|
|
941
1095
|
|
|
942
|
-
function lifecycleProbeArgs(event) {
|
|
1096
|
+
function lifecycleProbeArgs(event, options = {}) {
|
|
943
1097
|
return [
|
|
944
1098
|
{
|
|
945
1099
|
source: event.source,
|
|
@@ -947,7 +1101,7 @@ function lifecycleProbeArgs(event) {
|
|
|
947
1101
|
logger: console,
|
|
948
1102
|
runtime: { env: {}, logger: console },
|
|
949
1103
|
secrets: { get: async () => null, has: async () => false },
|
|
950
|
-
signal: new AbortController().signal,
|
|
1104
|
+
signal: options.signal ?? new AbortController().signal,
|
|
951
1105
|
},
|
|
952
1106
|
];
|
|
953
1107
|
}
|
|
@@ -965,9 +1119,9 @@ function speechProbeArgs(event) {
|
|
|
965
1119
|
];
|
|
966
1120
|
}
|
|
967
1121
|
|
|
968
|
-
async function runProbe({ captureIndex, kind, seam, label, invoke }) {
|
|
1122
|
+
async function runProbe({ captureIndex, kind, seam, label, invoke, timeoutMs, controller }) {
|
|
969
1123
|
try {
|
|
970
|
-
const output = await invoke
|
|
1124
|
+
const output = await invokeWithTimeout(invoke, timeoutMs, controller);
|
|
971
1125
|
return {
|
|
972
1126
|
captureIndex,
|
|
973
1127
|
kind,
|
|
@@ -988,6 +1142,30 @@ async function runProbe({ captureIndex, kind, seam, label, invoke }) {
|
|
|
988
1142
|
}
|
|
989
1143
|
}
|
|
990
1144
|
|
|
1145
|
+
function invokeWithTimeout(invoke, timeoutMs, controller) {
|
|
1146
|
+
const { signal } = controller;
|
|
1147
|
+
let rejectAborted;
|
|
1148
|
+
const aborted = new Promise((_, reject) => { rejectAborted = reject; });
|
|
1149
|
+
const onAbort = () => rejectAborted(signal.reason);
|
|
1150
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1151
|
+
const timeoutId = setTimeout(() => {
|
|
1152
|
+
controller.abort(new Error(`Synthetic probe timed out after ${timeoutMs}ms`));
|
|
1153
|
+
}, timeoutMs);
|
|
1154
|
+
// Keep observing settlement after abort without starting dependent callbacks.
|
|
1155
|
+
// In-process JavaScript itself is not preempted by this deadline.
|
|
1156
|
+
const result = Promise.resolve().then(() => {
|
|
1157
|
+
signal.throwIfAborted();
|
|
1158
|
+
return invoke();
|
|
1159
|
+
}).then((output) => {
|
|
1160
|
+
signal.throwIfAborted();
|
|
1161
|
+
return output;
|
|
1162
|
+
});
|
|
1163
|
+
return Promise.race([result, aborted]).finally(() => {
|
|
1164
|
+
clearTimeout(timeoutId);
|
|
1165
|
+
signal.removeEventListener("abort", onAbort);
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
|
|
991
1169
|
function blockedResult(entry, captureIndex, reason) {
|
|
992
1170
|
return {
|
|
993
1171
|
captureIndex,
|