@midscene/core 1.15.2-beta-20261012123219.0 → 1.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/es/agent/action-readiness.mjs +6 -3
- package/dist/es/agent/action-readiness.mjs.map +1 -1
- package/dist/es/agent/agent.mjs +4 -2
- package/dist/es/agent/agent.mjs.map +1 -1
- package/dist/es/agent/execution-session.mjs.map +1 -1
- package/dist/es/agent/task-builder.mjs +35 -16
- package/dist/es/agent/task-builder.mjs.map +1 -1
- package/dist/es/agent/tasks.mjs +35 -12
- package/dist/es/agent/tasks.mjs.map +1 -1
- package/dist/es/agent/utils.mjs +1 -1
- package/dist/es/device/index.mjs +7 -6
- package/dist/es/device/index.mjs.map +1 -1
- package/dist/es/report-generator.mjs +30 -14
- package/dist/es/report-generator.mjs.map +1 -1
- package/dist/es/report-html-template.mjs +1 -1
- package/dist/es/report.mjs +121 -44
- package/dist/es/report.mjs.map +1 -1
- package/dist/es/service/index.mjs.map +1 -1
- package/dist/es/task-runner.mjs +22 -12
- package/dist/es/task-runner.mjs.map +1 -1
- package/dist/es/test-run-report.mjs.map +1 -1
- package/dist/es/test-runner/engine/case-runner.mjs +1 -1
- package/dist/es/test-runner/engine/case-runner.mjs.map +1 -1
- package/dist/es/test-runner/engine/document-runtime.mjs +42 -12
- package/dist/es/test-runner/engine/document-runtime.mjs.map +1 -1
- package/dist/es/test-runner/engine/execute-step.mjs +130 -12
- package/dist/es/test-runner/engine/execute-step.mjs.map +1 -1
- package/dist/es/test-runner/engine/index.mjs +1 -0
- package/dist/es/test-runner/engine/project-runtime.mjs +14 -5
- package/dist/es/test-runner/engine/project-runtime.mjs.map +1 -1
- package/dist/es/test-runner/engine/resource-operations.mjs +286 -35
- package/dist/es/test-runner/engine/resource-operations.mjs.map +1 -1
- package/dist/es/test-runner/engine/result-metadata.mjs +21 -0
- package/dist/es/test-runner/engine/result-metadata.mjs.map +1 -0
- package/dist/es/test-runner/engine/run-collected-case.mjs +30 -9
- package/dist/es/test-runner/engine/run-collected-case.mjs.map +1 -1
- package/dist/es/test-runner/engine/run-workflow-document.mjs +57 -10
- package/dist/es/test-runner/engine/run-workflow-document.mjs.map +1 -1
- package/dist/es/test-runner/errors.mjs +26 -13
- package/dist/es/test-runner/errors.mjs.map +1 -1
- package/dist/es/test-runner/execution-record.mjs +2 -0
- package/dist/es/test-runner/execution-record.mjs.map +1 -1
- package/dist/es/test-runner/index.mjs +3 -0
- package/dist/es/test-runner/reporting/agent-report.mjs +2 -7
- package/dist/es/test-runner/reporting/agent-report.mjs.map +1 -1
- package/dist/es/test-runner/reporting/merge-report.mjs +207 -6
- package/dist/es/test-runner/reporting/merge-report.mjs.map +1 -1
- package/dist/es/test-runner/reporting/metrics.mjs +77 -0
- package/dist/es/test-runner/reporting/metrics.mjs.map +1 -0
- package/dist/es/test-runner/reporting/sanitize.mjs +92 -16
- package/dist/es/test-runner/reporting/sanitize.mjs.map +1 -1
- package/dist/es/test-runner/reporting/test-plan.mjs +56 -0
- package/dist/es/test-runner/reporting/test-plan.mjs.map +1 -0
- package/dist/es/test-runner/reporting/test-run-report.mjs +208 -21
- package/dist/es/test-runner/reporting/test-run-report.mjs.map +1 -1
- package/dist/es/types.mjs.map +1 -1
- package/dist/es/utils.mjs +1 -1
- package/dist/es/yaml/legacy-yaml-runtime.mjs +3 -3
- package/dist/es/yaml/legacy-yaml-runtime.mjs.map +1 -1
- package/dist/lib/agent/action-readiness.js +6 -3
- package/dist/lib/agent/action-readiness.js.map +1 -1
- package/dist/lib/agent/agent.js +4 -2
- package/dist/lib/agent/agent.js.map +1 -1
- package/dist/lib/agent/execution-session.js.map +1 -1
- package/dist/lib/agent/task-builder.js +42 -23
- package/dist/lib/agent/task-builder.js.map +1 -1
- package/dist/lib/agent/tasks.js +35 -12
- package/dist/lib/agent/tasks.js.map +1 -1
- package/dist/lib/agent/utils.js +1 -1
- package/dist/lib/device/index.js +7 -6
- package/dist/lib/device/index.js.map +1 -1
- package/dist/lib/report-generator.js +30 -14
- package/dist/lib/report-generator.js.map +1 -1
- package/dist/lib/report-html-template.js +1 -1
- package/dist/lib/report.js +121 -44
- package/dist/lib/report.js.map +1 -1
- package/dist/lib/service/index.js.map +1 -1
- package/dist/lib/task-runner.js +22 -12
- package/dist/lib/task-runner.js.map +1 -1
- package/dist/lib/test-run-report.js.map +1 -1
- package/dist/lib/test-runner/engine/case-runner.js +1 -1
- package/dist/lib/test-runner/engine/case-runner.js.map +1 -1
- package/dist/lib/test-runner/engine/document-runtime.js +41 -11
- package/dist/lib/test-runner/engine/document-runtime.js.map +1 -1
- package/dist/lib/test-runner/engine/execute-step.js +131 -10
- package/dist/lib/test-runner/engine/execute-step.js.map +1 -1
- package/dist/lib/test-runner/engine/index.js +9 -2
- package/dist/lib/test-runner/engine/index.js.map +1 -1
- package/dist/lib/test-runner/engine/project-runtime.js +13 -4
- package/dist/lib/test-runner/engine/project-runtime.js.map +1 -1
- package/dist/lib/test-runner/engine/resource-operations.js +319 -38
- package/dist/lib/test-runner/engine/resource-operations.js.map +1 -1
- package/dist/lib/test-runner/engine/result-metadata.js +58 -0
- package/dist/lib/test-runner/engine/result-metadata.js.map +1 -0
- package/dist/lib/test-runner/engine/run-collected-case.js +29 -8
- package/dist/lib/test-runner/engine/run-collected-case.js.map +1 -1
- package/dist/lib/test-runner/engine/run-workflow-document.js +57 -10
- package/dist/lib/test-runner/engine/run-workflow-document.js.map +1 -1
- package/dist/lib/test-runner/errors.js +42 -23
- package/dist/lib/test-runner/errors.js.map +1 -1
- package/dist/lib/test-runner/execution-record.js +2 -0
- package/dist/lib/test-runner/execution-record.js.map +1 -1
- package/dist/lib/test-runner/index.js +88 -16
- package/dist/lib/test-runner/index.js.map +1 -1
- package/dist/lib/test-runner/node/index.js +2 -2
- package/dist/lib/test-runner/reporting/agent-report.js +2 -7
- package/dist/lib/test-runner/reporting/agent-report.js.map +1 -1
- package/dist/lib/test-runner/reporting/merge-report.js +207 -6
- package/dist/lib/test-runner/reporting/merge-report.js.map +1 -1
- package/dist/lib/test-runner/reporting/metrics.js +114 -0
- package/dist/lib/test-runner/reporting/metrics.js.map +1 -0
- package/dist/lib/test-runner/reporting/sanitize.js +94 -15
- package/dist/lib/test-runner/reporting/sanitize.js.map +1 -1
- package/dist/lib/test-runner/reporting/test-plan.js +90 -0
- package/dist/lib/test-runner/reporting/test-plan.js.map +1 -0
- package/dist/lib/test-runner/reporting/test-run-report.js +208 -21
- package/dist/lib/test-runner/reporting/test-run-report.js.map +1 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/lib/yaml/legacy-yaml-runtime.js +2 -2
- package/dist/lib/yaml/legacy-yaml-runtime.js.map +1 -1
- package/dist/types/agent/action-readiness.d.ts +1 -1
- package/dist/types/agent/action-readiness.d.ts.map +1 -1
- package/dist/types/agent/agent.d.ts +1 -1
- package/dist/types/agent/agent.d.ts.map +1 -1
- package/dist/types/agent/execution-session.d.ts +1 -1
- package/dist/types/agent/execution-session.d.ts.map +1 -1
- package/dist/types/agent/task-builder.d.ts +4 -2
- package/dist/types/agent/task-builder.d.ts.map +1 -1
- package/dist/types/agent/tasks.d.ts +3 -0
- package/dist/types/agent/tasks.d.ts.map +1 -1
- package/dist/types/device/index.d.ts +3 -3
- package/dist/types/device/index.d.ts.map +1 -1
- package/dist/types/report-generator.d.ts +4 -2
- package/dist/types/report-generator.d.ts.map +1 -1
- package/dist/types/report.d.ts.map +1 -1
- package/dist/types/service/index.d.ts +2 -2
- package/dist/types/service/index.d.ts.map +1 -1
- package/dist/types/task-runner.d.ts +1 -1
- package/dist/types/task-runner.d.ts.map +1 -1
- package/dist/types/test-run-report.d.ts +157 -8
- package/dist/types/test-run-report.d.ts.map +1 -1
- package/dist/types/test-runner/engine/document-runtime.d.ts.map +1 -1
- package/dist/types/test-runner/engine/execute-step.d.ts +1 -0
- package/dist/types/test-runner/engine/execute-step.d.ts.map +1 -1
- package/dist/types/test-runner/engine/index.d.ts +1 -0
- package/dist/types/test-runner/engine/index.d.ts.map +1 -1
- package/dist/types/test-runner/engine/project-runtime.d.ts.map +1 -1
- package/dist/types/test-runner/engine/resource-operations.d.ts +41 -3
- package/dist/types/test-runner/engine/resource-operations.d.ts.map +1 -1
- package/dist/types/test-runner/engine/result-metadata.d.ts +18 -0
- package/dist/types/test-runner/engine/result-metadata.d.ts.map +1 -0
- package/dist/types/test-runner/engine/run-collected-case.d.ts.map +1 -1
- package/dist/types/test-runner/engine/run-workflow-document.d.ts.map +1 -1
- package/dist/types/test-runner/engine/types.d.ts +47 -8
- package/dist/types/test-runner/engine/types.d.ts.map +1 -1
- package/dist/types/test-runner/errors.d.ts +7 -0
- package/dist/types/test-runner/errors.d.ts.map +1 -1
- package/dist/types/test-runner/execution-record.d.ts.map +1 -1
- package/dist/types/test-runner/index.d.ts +2 -0
- package/dist/types/test-runner/index.d.ts.map +1 -1
- package/dist/types/test-runner/node/types.d.ts +9 -1
- package/dist/types/test-runner/node/types.d.ts.map +1 -1
- package/dist/types/test-runner/parser/types.d.ts +5 -0
- package/dist/types/test-runner/parser/types.d.ts.map +1 -1
- package/dist/types/test-runner/reporting/agent-report.d.ts.map +1 -1
- package/dist/types/test-runner/reporting/merge-report.d.ts +1 -1
- package/dist/types/test-runner/reporting/merge-report.d.ts.map +1 -1
- package/dist/types/test-runner/reporting/metrics.d.ts +7 -0
- package/dist/types/test-runner/reporting/metrics.d.ts.map +1 -0
- package/dist/types/test-runner/reporting/sanitize.d.ts +1 -0
- package/dist/types/test-runner/reporting/sanitize.d.ts.map +1 -1
- package/dist/types/test-runner/reporting/test-plan.d.ts +8 -0
- package/dist/types/test-runner/reporting/test-plan.d.ts.map +1 -0
- package/dist/types/test-runner/reporting/test-run-report.d.ts.map +1 -1
- package/dist/types/test-runner/reporting/types.d.ts +6 -1
- package/dist/types/test-runner/reporting/types.d.ts.map +1 -1
- package/dist/types/types.d.ts +3 -1
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -1,13 +1,29 @@
|
|
|
1
|
+
import { getDebug } from "@midscene/shared/logger";
|
|
2
|
+
import { uuid } from "@midscene/shared/utils";
|
|
1
3
|
import { WorkflowError } from "../errors.mjs";
|
|
2
|
-
|
|
4
|
+
import { sanitizeReportError } from "../reporting/sanitize.mjs";
|
|
5
|
+
function _define_property(obj, key, value) {
|
|
6
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
7
|
+
value: value,
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
writable: true
|
|
11
|
+
});
|
|
12
|
+
else obj[key] = value;
|
|
13
|
+
return obj;
|
|
14
|
+
}
|
|
15
|
+
const admissionErrorKey = Symbol.for('@midscene/core/resource-admission-error');
|
|
16
|
+
const stateKey = Symbol.for('@midscene/core/resource-operations/v4');
|
|
3
17
|
const host = globalThis;
|
|
4
18
|
host[stateKey] ??= {
|
|
5
19
|
resources: new WeakMap(),
|
|
6
20
|
scopes: new WeakMap(),
|
|
7
21
|
parents: new WeakMap(),
|
|
8
|
-
cleanupOwners: new WeakMap()
|
|
22
|
+
cleanupOwners: new WeakMap(),
|
|
23
|
+
identities: new WeakMap(),
|
|
24
|
+
listeners: new WeakMap()
|
|
9
25
|
};
|
|
10
|
-
const { resources: resource_operations_resources, scopes, parents, cleanupOwners } = host[stateKey];
|
|
26
|
+
const { resources: resource_operations_resources, scopes, parents, cleanupOwners, identities, listeners } = host[stateKey];
|
|
11
27
|
const resourceHandle = (resource)=>{
|
|
12
28
|
let handle = resource_operations_resources.get(resource);
|
|
13
29
|
if (!handle) {
|
|
@@ -20,8 +36,50 @@ const resourceHandle = (resource)=>{
|
|
|
20
36
|
return handle;
|
|
21
37
|
};
|
|
22
38
|
const getResourceCleanupCompletion = (owner)=>resource_operations_resources.get(owner)?.cleanup;
|
|
39
|
+
function blockingReferences(blockers) {
|
|
40
|
+
const references = new Map();
|
|
41
|
+
const visited = new Set();
|
|
42
|
+
const visit = (record)=>{
|
|
43
|
+
if (visited.has(record)) return;
|
|
44
|
+
visited.add(record);
|
|
45
|
+
if (!record.settled) references.set(record.operationId, {
|
|
46
|
+
operationId: record.operationId,
|
|
47
|
+
...record.identity
|
|
48
|
+
});
|
|
49
|
+
for (const blocker of record.blockers ?? [])visit(blocker);
|
|
50
|
+
};
|
|
51
|
+
for (const blocker of blockers)visit(blocker);
|
|
52
|
+
return [
|
|
53
|
+
...references.values()
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
function getResourceCleanupSnapshot(owner) {
|
|
57
|
+
const state = resource_operations_resources.get(owner)?.cleanupState;
|
|
58
|
+
return freezeEvent({
|
|
59
|
+
capturedAt: new Date().toISOString(),
|
|
60
|
+
status: state?.status ?? 'not-required',
|
|
61
|
+
...state?.operationId ? {
|
|
62
|
+
operationId: state.operationId
|
|
63
|
+
} : {},
|
|
64
|
+
...state?.status === 'pending' ? {
|
|
65
|
+
blockingOperations: blockingReferences(state.blockers)
|
|
66
|
+
} : {},
|
|
67
|
+
...state?.error === void 0 ? {} : {
|
|
68
|
+
error: sanitizeReportError(state.error)
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
23
72
|
const RESOURCE_CLEANUP_GRACE_MS = 1000;
|
|
24
73
|
const RESOURCE_RECOVERY_GRACE_MS = 30000;
|
|
74
|
+
const RESOURCE_TEARDOWN_GRACE_MS = 10000;
|
|
75
|
+
function getPendingScopeOperations(signal) {
|
|
76
|
+
return freezeEvent(blockingReferences([
|
|
77
|
+
...scopes.get(signal)?.operations.keys() ?? []
|
|
78
|
+
].flatMap((resource)=>[
|
|
79
|
+
...resource_operations_resources.get(resource)?.operations.values() ?? []
|
|
80
|
+
])));
|
|
81
|
+
}
|
|
82
|
+
const hasPendingResourceOperation = (resource)=>(resource_operations_resources.get(resource)?.operations.size ?? 0) > 0;
|
|
25
83
|
function linkResourceSignal(signal, parent) {
|
|
26
84
|
if (parent && parent !== signal) parents.set(signal, parent);
|
|
27
85
|
}
|
|
@@ -66,23 +124,167 @@ function assertResourceAvailable(resource, exclusive = false, signal) {
|
|
|
66
124
|
code: 'RESOURCE_CLEANUP_PENDING'
|
|
67
125
|
});
|
|
68
126
|
const pending = resource_operations_resources.get(resource)?.operations;
|
|
69
|
-
if (exclusive && pending?.size
|
|
127
|
+
if (exclusive && pending?.size) throw new WorkflowError('The same Agent instance cannot execute overlapping Runner operations.', {
|
|
70
128
|
code: 'RESOURCE_OPERATION_PENDING'
|
|
71
129
|
});
|
|
72
|
-
if (pending &&
|
|
130
|
+
if (pending && [
|
|
73
131
|
...pending.values()
|
|
74
|
-
].some((
|
|
132
|
+
].some((operation)=>operation.signal.aborted)) throw new WorkflowError('Cannot reuse an Agent while an aborted YAML action or Runner operation is still running.', {
|
|
75
133
|
code: 'RESOURCE_OPERATION_PENDING'
|
|
76
134
|
});
|
|
77
135
|
return isOwnerCleanup;
|
|
78
136
|
}
|
|
137
|
+
function observeResourceOperations(signal, listener) {
|
|
138
|
+
const subscribers = listeners.get(signal) ?? new Set();
|
|
139
|
+
listeners.set(signal, subscribers);
|
|
140
|
+
subscribers.add(listener);
|
|
141
|
+
return ()=>{
|
|
142
|
+
subscribers.delete(listener);
|
|
143
|
+
if (!subscribers.size) listeners.delete(signal);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function setResourceOperationIdentity(signal, identity) {
|
|
147
|
+
identities.set(signal, Object.freeze({
|
|
148
|
+
...identity
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
function freezeEvent(value) {
|
|
152
|
+
if ('object' == typeof value && null !== value && !Object.isFrozen(value)) {
|
|
153
|
+
for (const child of Object.values(value))freezeEvent(child);
|
|
154
|
+
Object.freeze(value);
|
|
155
|
+
}
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
function recordOperation(operation, signal, operationKind) {
|
|
159
|
+
const owners = [
|
|
160
|
+
...ownerSignals(signal)
|
|
161
|
+
];
|
|
162
|
+
const identity = owners.map((owner)=>identities.get(owner)).find(Boolean);
|
|
163
|
+
const record = {
|
|
164
|
+
signal,
|
|
165
|
+
operationId: uuid(),
|
|
166
|
+
identity
|
|
167
|
+
};
|
|
168
|
+
const subscribers = new Set(owners.flatMap((owner)=>[
|
|
169
|
+
...listeners.get(owner) ?? []
|
|
170
|
+
]));
|
|
171
|
+
let settled = false;
|
|
172
|
+
const emit = (event)=>{
|
|
173
|
+
const frozen = freezeEvent({
|
|
174
|
+
...identity,
|
|
175
|
+
operationId: record.operationId,
|
|
176
|
+
operationKind,
|
|
177
|
+
at: new Date().toISOString(),
|
|
178
|
+
...event
|
|
179
|
+
});
|
|
180
|
+
for (const subscriber of subscribers)try {
|
|
181
|
+
subscriber(frozen);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
getDebug('runner:operations', {
|
|
184
|
+
console: true
|
|
185
|
+
})(`Operation observer failed: ${String(error)}`);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const abort = ()=>emit({
|
|
189
|
+
kind: 'cancel-requested',
|
|
190
|
+
error: sanitizeReportError(signal.reason)
|
|
191
|
+
});
|
|
192
|
+
emit({
|
|
193
|
+
kind: 'started'
|
|
194
|
+
});
|
|
195
|
+
if (signal.aborted) abort();
|
|
196
|
+
else signal.addEventListener('abort', abort, {
|
|
197
|
+
once: true
|
|
198
|
+
});
|
|
199
|
+
const finish = (status, error)=>{
|
|
200
|
+
settled = true;
|
|
201
|
+
record.settled = true;
|
|
202
|
+
signal.removeEventListener('abort', abort);
|
|
203
|
+
emit({
|
|
204
|
+
kind: 'settled',
|
|
205
|
+
status,
|
|
206
|
+
...'success' !== status ? {
|
|
207
|
+
error: sanitizeReportError(error)
|
|
208
|
+
} : {}
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
operation.then(()=>finish('success'), (error)=>finish(isResourceAdmissionError(error) ? 'not-run' : 'failed', error));
|
|
212
|
+
return {
|
|
213
|
+
record,
|
|
214
|
+
runnerReturned (status) {
|
|
215
|
+
emit({
|
|
216
|
+
kind: 'runner-returned',
|
|
217
|
+
status,
|
|
218
|
+
pending: !settled
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
cleanupDeferred (blockers) {
|
|
222
|
+
emit({
|
|
223
|
+
kind: 'cleanup-deferred',
|
|
224
|
+
blockingOperations: blockingReferences(blockers)
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
let _admissionErrorKey = admissionErrorKey;
|
|
230
|
+
class ResourceAdmissionError extends WorkflowError {
|
|
231
|
+
constructor(code, blockers){
|
|
232
|
+
super('The Agent is occupied by an unfinished operation or pending cleanup; this action was not started.', {
|
|
233
|
+
code,
|
|
234
|
+
details: {
|
|
235
|
+
blockingOperations: blockers.map(({ operationId, identity })=>({
|
|
236
|
+
operationId,
|
|
237
|
+
...identity
|
|
238
|
+
}))
|
|
239
|
+
}
|
|
240
|
+
}), _define_property(this, "blockedBy", void 0), _define_property(this, _admissionErrorKey, true);
|
|
241
|
+
this.blockedBy = blockers.find((blocker)=>blocker.identity?.stepId)?.identity?.stepId;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function isResourceAdmissionError(error) {
|
|
245
|
+
return 'object' == typeof error && null !== error && true === error[admissionErrorKey];
|
|
246
|
+
}
|
|
247
|
+
function admitResourceOperation(resource, signal, exclusive = true) {
|
|
248
|
+
let isCleanupOwner;
|
|
249
|
+
try {
|
|
250
|
+
isCleanupOwner = assertResourceAvailable(resource, exclusive, signal);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error instanceof WorkflowError && [
|
|
253
|
+
'RESOURCE_OPERATION_PENDING',
|
|
254
|
+
'RESOURCE_CLEANUP_PENDING'
|
|
255
|
+
].includes(error.code)) throw new ResourceAdmissionError(error.code, [
|
|
256
|
+
...resource_operations_resources.get(resource)?.operations.values() ?? [],
|
|
257
|
+
...[
|
|
258
|
+
...resource_operations_resources.get(resource)?.retiring ?? []
|
|
259
|
+
].flatMap((owner)=>[
|
|
260
|
+
...owner.blockers
|
|
261
|
+
])
|
|
262
|
+
]);
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
return isCleanupOwner;
|
|
266
|
+
}
|
|
79
267
|
const retainForCleanup = (owner, resource)=>{
|
|
80
268
|
owner.resources.add(resource);
|
|
81
|
-
resourceHandle(resource)
|
|
269
|
+
const handle = resourceHandle(resource);
|
|
270
|
+
for (const operation of handle.operations.values())owner.blockers.add(operation);
|
|
271
|
+
handle.retiring.add(owner);
|
|
82
272
|
};
|
|
83
|
-
function
|
|
273
|
+
function retainResourceForScope(resource, signal) {
|
|
274
|
+
let retained = false;
|
|
275
|
+
for (const ownerSignal of ownerSignals(signal)){
|
|
276
|
+
const scope = scopes.get(ownerSignal);
|
|
277
|
+
if (!retained && scope?.resources) {
|
|
278
|
+
scope.resources.add(resource);
|
|
279
|
+
retained = true;
|
|
280
|
+
}
|
|
281
|
+
if (scope?.cleanupOwner) retainForCleanup(scope.cleanupOwner, resource);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function trackResourceOperation(resource, operation, signal, kind = 'resource', existingObservation) {
|
|
285
|
+
const observation = existingObservation ?? recordOperation(operation, signal, kind);
|
|
84
286
|
const active = resourceHandle(resource).operations;
|
|
85
|
-
active.set(operation,
|
|
287
|
+
active.set(operation, observation.record);
|
|
86
288
|
const ownerScopes = [];
|
|
87
289
|
let retainedByScope = false;
|
|
88
290
|
for (const ownerSignal of ownerSignals(signal)){
|
|
@@ -100,7 +302,10 @@ function trackResourceOperation(resource, operation, signal) {
|
|
|
100
302
|
retainedByScope = true;
|
|
101
303
|
}
|
|
102
304
|
const cleanupOwner = cleanupOwners.get(ownerSignal);
|
|
103
|
-
if (cleanupOwner)
|
|
305
|
+
if (cleanupOwner) {
|
|
306
|
+
cleanupOwner.blockers.add(observation.record);
|
|
307
|
+
retainForCleanup(cleanupOwner, resource);
|
|
308
|
+
}
|
|
104
309
|
}
|
|
105
310
|
const finish = ()=>{
|
|
106
311
|
active.delete(operation);
|
|
@@ -121,19 +326,24 @@ async function waitForResourceIdle(resource) {
|
|
|
121
326
|
}
|
|
122
327
|
async function recoverCancelledScopeResources(signal, graceMs = RESOURCE_RECOVERY_GRACE_MS) {
|
|
123
328
|
signal.throwIfAborted();
|
|
329
|
+
const startedAt = Date.now();
|
|
124
330
|
const pendingOperations = ()=>{
|
|
125
331
|
const pending = [];
|
|
126
|
-
for (const resource of scopes.get(signal)?.operations.keys() ?? [])for (const [operation,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
signal
|
|
131
|
-
|
|
132
|
-
|
|
332
|
+
for (const resource of scopes.get(signal)?.operations.keys() ?? [])for (const [operation, tracked] of resource_operations_resources.get(resource)?.operations ?? []){
|
|
333
|
+
const ownerSignal = tracked.signal;
|
|
334
|
+
if (ownerSignal.aborted && [
|
|
335
|
+
...ownerSignals(ownerSignal)
|
|
336
|
+
].includes(signal)) pending.push({
|
|
337
|
+
operation,
|
|
338
|
+
signal: ownerSignal,
|
|
339
|
+
resource,
|
|
340
|
+
record: tracked
|
|
341
|
+
});
|
|
342
|
+
}
|
|
133
343
|
return pending;
|
|
134
344
|
};
|
|
135
345
|
let pending = pendingOperations();
|
|
136
|
-
if (!pending.length) return;
|
|
346
|
+
if (!pending.length) return 0;
|
|
137
347
|
let timer;
|
|
138
348
|
let onAbort;
|
|
139
349
|
const stopped = new Promise((_resolve, reject)=>{
|
|
@@ -141,14 +351,21 @@ async function recoverCancelledScopeResources(signal, graceMs = RESOURCE_RECOVER
|
|
|
141
351
|
signal.addEventListener('abort', onAbort, {
|
|
142
352
|
once: true
|
|
143
353
|
});
|
|
144
|
-
timer = setTimeout(()=>
|
|
354
|
+
timer = setTimeout(()=>{
|
|
355
|
+
const blockingOperations = blockingReferences(pending.map(({ record })=>record));
|
|
356
|
+
const blockedByStepId = blockingOperations.every(({ stepId })=>stepId === blockingOperations[0]?.stepId) ? blockingOperations[0]?.stepId : void 0;
|
|
357
|
+
reject(new WorkflowError(`The previous operation was still running after ${graceMs / 1000} s. Later steps were not started.`, {
|
|
145
358
|
code: 'RESOURCE_RECOVERY_TIMEOUT',
|
|
146
359
|
details: {
|
|
147
360
|
graceMs,
|
|
148
|
-
|
|
361
|
+
waitedMs: Date.now() - startedAt,
|
|
362
|
+
resourceCount: new Set(pending.map(({ resource })=>resource)).size,
|
|
363
|
+
blockingOperations,
|
|
364
|
+
blockedByStepId
|
|
149
365
|
},
|
|
150
366
|
cause: pending[0]?.signal.reason
|
|
151
|
-
}))
|
|
367
|
+
}));
|
|
368
|
+
}, graceMs);
|
|
152
369
|
});
|
|
153
370
|
try {
|
|
154
371
|
while(pending.length){
|
|
@@ -159,18 +376,23 @@ async function recoverCancelledScopeResources(signal, graceMs = RESOURCE_RECOVER
|
|
|
159
376
|
signal.throwIfAborted();
|
|
160
377
|
pending = pendingOperations();
|
|
161
378
|
}
|
|
379
|
+
return Date.now() - startedAt;
|
|
162
380
|
} finally{
|
|
163
381
|
clearTimeout(timer);
|
|
164
382
|
signal.removeEventListener('abort', onAbort);
|
|
165
383
|
}
|
|
166
384
|
}
|
|
167
385
|
class ResourceCleanupDeferredError extends WorkflowError {
|
|
168
|
-
constructor(graceMs, resourceCount){
|
|
169
|
-
super(
|
|
386
|
+
constructor(graceMs, resourceCount, blockers = []){
|
|
387
|
+
super(`Resource cleanup did not finish within ${graceMs}ms. Resources remain quarantined until cleanup settles.`, {
|
|
170
388
|
code: 'RESOURCE_CLEANUP_DEFERRED',
|
|
171
389
|
details: {
|
|
172
390
|
graceMs,
|
|
173
|
-
resourceCount
|
|
391
|
+
resourceCount,
|
|
392
|
+
blockingOperations: blockers.map(({ operationId, identity })=>({
|
|
393
|
+
operationId,
|
|
394
|
+
...identity
|
|
395
|
+
}))
|
|
174
396
|
}
|
|
175
397
|
});
|
|
176
398
|
}
|
|
@@ -178,25 +400,31 @@ class ResourceCleanupDeferredError extends WorkflowError {
|
|
|
178
400
|
async function runResourceCleanup(resources, cleanup, options, beforeCleanup) {
|
|
179
401
|
const owner = {
|
|
180
402
|
resources: new Set(),
|
|
403
|
+
blockers: new Set(),
|
|
181
404
|
signal: beforeCleanup?.signal
|
|
182
405
|
};
|
|
183
406
|
for (const resource of resources)retainForCleanup(owner, resource);
|
|
184
407
|
if (owner.signal) cleanupOwners.set(owner.signal, owner);
|
|
408
|
+
const ownerScope = options.signal ? scopes.get(options.signal) : void 0;
|
|
409
|
+
if (ownerScope) ownerScope.cleanupOwner = owner;
|
|
185
410
|
let defer;
|
|
186
411
|
const deferred = new Promise((resolve)=>{
|
|
187
412
|
defer = ()=>resolve({
|
|
188
413
|
deferred: true
|
|
189
414
|
});
|
|
190
415
|
});
|
|
191
|
-
|
|
416
|
+
let graceMs = options.graceMs ?? RESOURCE_CLEANUP_GRACE_MS;
|
|
417
|
+
let timer;
|
|
192
418
|
const drain = async ()=>{
|
|
193
|
-
|
|
194
|
-
...owner.resources
|
|
195
|
-
].filter((resource)=>resourceHandle(resource).operations.size);
|
|
196
|
-
if (!pending.length) return;
|
|
197
|
-
const timer = setTimeout(defer, graceMs);
|
|
419
|
+
timer = setTimeout(defer, graceMs);
|
|
198
420
|
try {
|
|
199
|
-
|
|
421
|
+
while(true){
|
|
422
|
+
const pending = [
|
|
423
|
+
...owner.resources
|
|
424
|
+
].filter((resource)=>resourceHandle(resource).operations.size);
|
|
425
|
+
if (!pending.length) return;
|
|
426
|
+
await Promise.all(pending.map(waitForResourceIdle));
|
|
427
|
+
}
|
|
200
428
|
} finally{
|
|
201
429
|
clearTimeout(timer);
|
|
202
430
|
}
|
|
@@ -205,17 +433,37 @@ async function runResourceCleanup(resources, cleanup, options, beforeCleanup) {
|
|
|
205
433
|
try {
|
|
206
434
|
if (beforeCleanup) await beforeCleanup.run(beforeCleanup.signal);
|
|
207
435
|
await drain();
|
|
436
|
+
graceMs = options.graceMs ?? RESOURCE_TEARDOWN_GRACE_MS;
|
|
437
|
+
timer = setTimeout(defer, graceMs);
|
|
208
438
|
return await cleanup();
|
|
209
439
|
} finally{
|
|
440
|
+
clearTimeout(timer);
|
|
210
441
|
if (owner.signal) cleanupOwners.delete(owner.signal);
|
|
442
|
+
if (ownerScope?.cleanupOwner === owner) ownerScope.cleanupOwner = void 0;
|
|
211
443
|
for (const resource of owner.resources){
|
|
212
444
|
const owners = resourceHandle(resource).retiring;
|
|
213
445
|
owners.delete(owner);
|
|
214
446
|
}
|
|
215
447
|
}
|
|
216
448
|
})();
|
|
449
|
+
const cleanupObservation = options.signal ? recordOperation(completion, options.signal, 'cleanup') : void 0;
|
|
217
450
|
const ownerKey = options.owner ?? beforeCleanup?.signal ?? resources[0];
|
|
218
|
-
|
|
451
|
+
const cleanupState = {
|
|
452
|
+
status: 'pending',
|
|
453
|
+
operationId: cleanupObservation?.record.operationId,
|
|
454
|
+
blockers: owner.blockers
|
|
455
|
+
};
|
|
456
|
+
if (cleanupObservation) cleanupObservation.record.blockers = owner.blockers;
|
|
457
|
+
if (ownerKey) {
|
|
458
|
+
resourceHandle(ownerKey).cleanup = completion;
|
|
459
|
+
resourceHandle(ownerKey).cleanupState = cleanupState;
|
|
460
|
+
}
|
|
461
|
+
completion.then(()=>{
|
|
462
|
+
cleanupState.status = 'completed';
|
|
463
|
+
}, (error)=>{
|
|
464
|
+
cleanupState.status = 'failed';
|
|
465
|
+
cleanupState.error = error;
|
|
466
|
+
});
|
|
219
467
|
const outcome = await Promise.race([
|
|
220
468
|
completion.then((value)=>({
|
|
221
469
|
value
|
|
@@ -223,13 +471,16 @@ async function runResourceCleanup(resources, cleanup, options, beforeCleanup) {
|
|
|
223
471
|
deferred
|
|
224
472
|
]);
|
|
225
473
|
if ('deferred' in outcome) {
|
|
474
|
+
cleanupObservation?.cleanupDeferred(owner.blockers);
|
|
226
475
|
completion.catch((error)=>{
|
|
227
476
|
try {
|
|
228
477
|
options.onDeferredError(error);
|
|
229
478
|
} catch {}
|
|
230
479
|
});
|
|
231
|
-
if (options.signal) trackResourceOperation({}, completion, options.signal);
|
|
232
|
-
throw new ResourceCleanupDeferredError(graceMs, owner.resources.size
|
|
480
|
+
if (options.signal) trackResourceOperation({}, completion, options.signal, 'cleanup', cleanupObservation);
|
|
481
|
+
throw new ResourceCleanupDeferredError(graceMs, owner.resources.size, [
|
|
482
|
+
...owner.blockers
|
|
483
|
+
]);
|
|
233
484
|
}
|
|
234
485
|
return outcome.value;
|
|
235
486
|
}
|
|
@@ -252,6 +503,6 @@ async function cleanupScopeResources(signal, cleanup, options) {
|
|
|
252
503
|
run: options.beforeCleanup
|
|
253
504
|
} : void 0);
|
|
254
505
|
}
|
|
255
|
-
export { RESOURCE_CLEANUP_GRACE_MS, RESOURCE_RECOVERY_GRACE_MS, ResourceCleanupDeferredError, assertResourceAvailable, cleanupResources, cleanupScopeResources, createResourceScope, getResourceCleanupCompletion, linkResourceSignal, recoverCancelledScopeResources, trackResourceOperation, waitForResourceIdle };
|
|
506
|
+
export { RESOURCE_CLEANUP_GRACE_MS, RESOURCE_RECOVERY_GRACE_MS, ResourceAdmissionError, ResourceCleanupDeferredError, admitResourceOperation, assertResourceAvailable, cleanupResources, cleanupScopeResources, createResourceScope, getPendingScopeOperations, getResourceCleanupCompletion, getResourceCleanupSnapshot, hasPendingResourceOperation, isResourceAdmissionError, linkResourceSignal, observeResourceOperations, recordOperation, recoverCancelledScopeResources, retainResourceForScope, setResourceOperationIdentity, trackResourceOperation, waitForResourceIdle };
|
|
256
507
|
|
|
257
508
|
//# sourceMappingURL=resource-operations.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-runner/engine/resource-operations.mjs","sources":["../../../../src/test-runner/engine/resource-operations.ts"],"sourcesContent":["import { WorkflowError } from '../errors';\n\ninterface CleanupOwner {\n resources: Set<object>;\n signal?: AbortSignal;\n}\n\ninterface ResourceScope {\n operations: Map<object, number>;\n resources?: Set<object>;\n}\n\ninterface ResourceHandle {\n operations: Map<Promise<unknown>, AbortSignal>;\n retiring: Set<CleanupOwner>;\n cleanup?: Promise<unknown>;\n}\n\ninterface ResourceState {\n resources: WeakMap<object, ResourceHandle>;\n scopes: WeakMap<AbortSignal, ResourceScope>;\n parents: WeakMap<AbortSignal, AbortSignal>;\n cleanupOwners: WeakMap<AbortSignal, CleanupOwner>;\n}\n\n// CJS and ESM hosts must agree about ownership of the same Agent instance.\nconst stateKey = Symbol.for('@midscene/core/resource-operations/v3');\nconst host = globalThis as typeof globalThis & { [stateKey]?: ResourceState };\nhost[stateKey] ??= {\n resources: new WeakMap(),\n scopes: new WeakMap(),\n parents: new WeakMap(),\n cleanupOwners: new WeakMap(),\n};\nconst { resources, scopes, parents, cleanupOwners } = host[stateKey];\nconst resourceHandle = (resource: object): ResourceHandle => {\n let handle = resources.get(resource);\n if (!handle) {\n handle = { operations: new Map(), retiring: new Set() };\n resources.set(resource, handle);\n }\n return handle;\n};\n\n/** Actual teardown completion belongs to the owner, never to a thrown error. */\nexport const getResourceCleanupCompletion = (\n owner: object,\n): Promise<unknown> | undefined => resources.get(owner)?.cleanup;\n\nexport const RESOURCE_CLEANUP_GRACE_MS = 1000;\nexport const RESOURCE_RECOVERY_GRACE_MS = 30_000;\n\n/** Resource ancestry is independent of cancellation: cleanup can outlive abort. */\nexport function linkResourceSignal(signal: AbortSignal, parent?: AbortSignal) {\n if (parent && parent !== signal) parents.set(signal, parent);\n}\n\nfunction* ownerSignals(signal?: AbortSignal): Generator<AbortSignal> {\n const seen = new Set<AbortSignal>();\n for (\n let owner = signal;\n owner && !seen.has(owner);\n owner = parents.get(owner)\n ) {\n seen.add(owner);\n yield owner;\n }\n}\n\n/** Each Case/document owns its cleanup, even when cancellation is shared. */\nexport function createResourceScope(parent?: AbortSignal) {\n const controller = new AbortController();\n const scope: ResourceScope = { operations: new Map(), resources: new Set() };\n scopes.set(controller.signal, scope);\n linkResourceSignal(controller.signal, parent);\n const abort = () => controller.abort(parent?.reason);\n if (parent?.aborted) abort();\n else parent?.addEventListener('abort', abort, { once: true });\n return {\n signal: controller.signal,\n dispose() {\n parent?.removeEventListener('abort', abort);\n scope.resources = undefined;\n if (!scope.operations.size) scopes.delete(controller.signal);\n },\n };\n}\n\nexport function assertResourceAvailable(\n resource: object,\n exclusive = false,\n signal?: AbortSignal,\n): boolean {\n const retiringOwners = resources.get(resource)?.retiring;\n const isOwnerCleanup = [...ownerSignals(signal)].some((ownerSignal) => {\n const owner = cleanupOwners.get(ownerSignal);\n return owner !== undefined && retiringOwners?.has(owner);\n });\n if (retiringOwners?.size && !isOwnerCleanup)\n throw new WorkflowError(\n 'A resource cannot execute overlapping Runner operations while its cleanup is pending.',\n { code: 'RESOURCE_CLEANUP_PENDING' },\n );\n const pending = resources.get(resource)?.operations;\n if (exclusive && pending?.size && !isOwnerCleanup)\n throw new WorkflowError(\n 'The same Agent instance cannot execute overlapping Runner operations.',\n { code: 'RESOURCE_OPERATION_PENDING' },\n );\n if (\n pending &&\n !isOwnerCleanup &&\n [...pending.values()].some((signal) => signal.aborted)\n )\n throw new WorkflowError(\n 'Cannot reuse an Agent while an aborted YAML action or Runner operation is still running.',\n { code: 'RESOURCE_OPERATION_PENDING' },\n );\n return isOwnerCleanup;\n}\n\nconst retainForCleanup = (owner: CleanupOwner, resource: object) => {\n owner.resources.add(resource);\n resourceHandle(resource).retiring.add(owner);\n};\n\n/** Track actual completion, not the Runner timeout race. */\nexport function trackResourceOperation<T>(\n resource: object,\n operation: Promise<T>,\n signal: AbortSignal,\n): Promise<T> {\n const active = resourceHandle(resource).operations;\n active.set(operation, signal);\n const ownerScopes: Array<[AbortSignal, ResourceScope]> = [];\n let retainedByScope = false;\n for (const ownerSignal of ownerSignals(signal)) {\n const scope: ResourceScope = scopes.get(ownerSignal) ?? {\n operations: new Map(),\n };\n scopes.set(ownerSignal, scope);\n scope.operations.set(resource, (scope.operations.get(resource) ?? 0) + 1);\n ownerScopes.push([ownerSignal, scope]);\n // Only the closest owner retains idle resources for its teardown. Parents\n // wait on active operations, not every Agent used by completed Cases.\n if (!retainedByScope && scope.resources) {\n scope.resources.add(resource);\n retainedByScope = true;\n }\n const cleanupOwner = cleanupOwners.get(ownerSignal);\n if (cleanupOwner) retainForCleanup(cleanupOwner, resource);\n }\n const finish = () => {\n active.delete(operation);\n for (const [ownerSignal, scope] of ownerScopes) {\n const count = scope.operations.get(resource)! - 1;\n if (count) scope.operations.set(resource, count);\n else scope.operations.delete(resource);\n if (!scope.operations.size && !scope.resources)\n scopes.delete(ownerSignal);\n }\n };\n void operation.then(finish, finish);\n return operation;\n}\n\nexport async function waitForResourceIdle(resource: object): Promise<void> {\n while (resources.get(resource)?.operations?.size)\n await Promise.allSettled([...resources.get(resource)!.operations.keys()]);\n}\n\n/** Wait for this scope's cancelled work before scheduling its next Step. */\nexport async function recoverCancelledScopeResources(\n signal: AbortSignal,\n graceMs = RESOURCE_RECOVERY_GRACE_MS,\n): Promise<void> {\n signal.throwIfAborted();\n const pendingOperations = () => {\n const pending: Array<{\n operation: Promise<unknown>;\n signal: AbortSignal;\n resource: object;\n }> = [];\n for (const resource of scopes.get(signal)?.operations.keys() ?? []) {\n for (const [operation, ownerSignal] of resources.get(resource)\n ?.operations ?? []) {\n if (\n ownerSignal.aborted &&\n [...ownerSignals(ownerSignal)].includes(signal)\n )\n pending.push({ operation, signal: ownerSignal, resource });\n }\n }\n return pending;\n };\n let pending = pendingOperations();\n if (!pending.length) return;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let onAbort!: () => void;\n const stopped = new Promise<never>((_resolve, reject) => {\n onAbort = () => reject(signal.reason);\n signal.addEventListener('abort', onAbort, { once: true });\n timer = setTimeout(\n () =>\n reject(\n new WorkflowError(\n `Cancelled operations did not finish within ${graceMs}ms; execution cannot continue safely.`,\n {\n code: 'RESOURCE_RECOVERY_TIMEOUT',\n details: {\n graceMs,\n resourceCount: new Set(pending.map(({ resource }) => resource))\n .size,\n },\n cause: pending[0]?.signal.reason,\n },\n ),\n ),\n graceMs,\n );\n });\n try {\n while (pending.length) {\n await Promise.race([\n Promise.allSettled(pending.map(({ operation }) => operation)),\n stopped,\n ]);\n signal.throwIfAborted();\n pending = pendingOperations();\n }\n } finally {\n clearTimeout(timer);\n signal.removeEventListener('abort', onAbort);\n }\n}\n\nexport class ResourceCleanupDeferredError extends WorkflowError {\n constructor(graceMs: number, resourceCount: number) {\n super(\n 'Resource cleanup was deferred because an operation did not stop after cancellation. The resource remains quarantined and cleanup will run when that operation settles.',\n {\n code: 'RESOURCE_CLEANUP_DEFERRED',\n details: { graceMs, resourceCount },\n },\n );\n }\n}\n\ninterface CleanupOptions {\n /** Stable owner of this cleanup, distinct from borrowed resource identities. */\n owner?: object;\n /** Parent scope observes deferred completion without host-specific forwarding. */\n signal?: AbortSignal;\n graceMs?: number;\n onDeferredError(error: unknown): void;\n}\n\n/** Bound draining, but keep ownership until the actual cleanup has finished. */\nasync function runResourceCleanup<T>(\n resources: readonly object[],\n cleanup: () => Promise<T>,\n options: CleanupOptions,\n beforeCleanup?: {\n signal: AbortSignal;\n run(signal: AbortSignal): Promise<void>;\n },\n): Promise<T> {\n const owner: CleanupOwner = {\n resources: new Set(),\n signal: beforeCleanup?.signal,\n };\n for (const resource of resources) retainForCleanup(owner, resource);\n if (owner.signal) cleanupOwners.set(owner.signal, owner);\n\n let defer!: () => void;\n const deferred = new Promise<{ deferred: true }>((resolve) => {\n defer = () => resolve({ deferred: true });\n });\n const graceMs = options.graceMs ?? RESOURCE_CLEANUP_GRACE_MS;\n const drain = async () => {\n const pending = [...owner.resources].filter(\n (resource) => resourceHandle(resource).operations.size,\n );\n if (!pending.length) return;\n const timer = setTimeout(defer, graceMs);\n try {\n await Promise.all(pending.map(waitForResourceIdle));\n } finally {\n clearTimeout(timer);\n }\n };\n const completion = (async () => {\n try {\n if (beforeCleanup) {\n // Authored lifecycle cleanup must be allowed to stop or reset a\n // resource whose timed-out operation is still settling. Its signal is\n // registered as the retiring owner's cleanup signal, so unrelated\n // scopes remain quarantined while this hook runs.\n await beforeCleanup.run(beforeCleanup.signal);\n }\n // Lifecycle hooks can themselves time out with an action still running.\n // Drain both the original work and any operation started by the hook\n // before disposing the resource.\n await drain();\n return await cleanup();\n } finally {\n if (owner.signal) cleanupOwners.delete(owner.signal);\n for (const resource of owner.resources) {\n const owners = resourceHandle(resource).retiring;\n owners.delete(owner);\n }\n }\n })();\n const ownerKey = options.owner ?? beforeCleanup?.signal ?? resources[0];\n if (ownerKey) resourceHandle(ownerKey).cleanup = completion;\n const outcome = await Promise.race([\n completion.then((value) => ({ value })),\n deferred,\n ]);\n if ('deferred' in outcome) {\n void completion.catch((error) => {\n try {\n options.onDeferredError(error);\n } catch {\n // The returned completion still carries the original cleanup failure.\n }\n });\n if (options.signal) trackResourceOperation({}, completion, options.signal);\n throw new ResourceCleanupDeferredError(graceMs, owner.resources.size);\n }\n return outcome.value;\n}\n\nexport function cleanupResources<T>(\n resources: readonly object[],\n cleanup: () => Promise<T>,\n options: CleanupOptions,\n): Promise<T> {\n return runResourceCleanup(resources, cleanup, options);\n}\n\nexport async function cleanupScopeResources<T>(\n signal: AbortSignal,\n cleanup: () => Promise<T>,\n options: CleanupOptions & {\n beforeCleanup?(signal: AbortSignal): Promise<void>;\n },\n): Promise<T> {\n const cleanupSignal = new AbortController().signal;\n linkResourceSignal(cleanupSignal, signal);\n const scope = scopes.get(signal);\n return runResourceCleanup(\n [...(scope?.resources ?? []), ...(scope?.operations.keys() ?? [])],\n cleanup,\n { ...options, owner: signal, signal },\n options.beforeCleanup\n ? { signal: cleanupSignal, run: options.beforeCleanup }\n : undefined,\n );\n}\n"],"names":["stateKey","Symbol","host","globalThis","WeakMap","resources","scopes","parents","cleanupOwners","resourceHandle","resource","handle","Map","Set","getResourceCleanupCompletion","owner","RESOURCE_CLEANUP_GRACE_MS","RESOURCE_RECOVERY_GRACE_MS","linkResourceSignal","signal","parent","ownerSignals","seen","createResourceScope","controller","AbortController","scope","abort","undefined","assertResourceAvailable","exclusive","retiringOwners","isOwnerCleanup","ownerSignal","WorkflowError","pending","retainForCleanup","trackResourceOperation","operation","active","ownerScopes","retainedByScope","cleanupOwner","finish","count","waitForResourceIdle","Promise","recoverCancelledScopeResources","graceMs","pendingOperations","timer","onAbort","stopped","_resolve","reject","setTimeout","clearTimeout","ResourceCleanupDeferredError","resourceCount","runResourceCleanup","cleanup","options","beforeCleanup","defer","deferred","resolve","drain","completion","owners","ownerKey","outcome","value","error","cleanupResources","cleanupScopeResources","cleanupSignal"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"test-runner/engine/resource-operations.mjs","sources":["../../../../src/test-runner/engine/resource-operations.ts"],"sourcesContent":["import { getDebug } from '@midscene/shared/logger';\nimport { uuid } from '@midscene/shared/utils';\nimport type {\n TestRunReportCleanup,\n TestRunReportOperationEvent,\n TestRunReportOperationReference,\n} from '../../test-run-report';\nimport { WorkflowError } from '../errors';\nimport { sanitizeReportError } from '../reporting/sanitize';\n\ninterface OperationIdentity {\n scopeId: string;\n stepId?: string;\n node?: string;\n}\ninterface ResourceOperation {\n signal: AbortSignal;\n operationId: string;\n identity?: OperationIdentity;\n settled?: boolean;\n blockers?: Set<ResourceOperation>;\n}\ntype OperationListener = (event: Readonly<TestRunReportOperationEvent>) => void;\n\ninterface CleanupOwner {\n resources: Set<object>;\n signal?: AbortSignal;\n blockers: Set<ResourceOperation>;\n}\n\ninterface ResourceScope {\n operations: Map<object, number>;\n resources?: Set<object>;\n cleanupOwner?: CleanupOwner;\n}\n\ninterface ResourceHandle {\n operations: Map<Promise<unknown>, ResourceOperation>;\n retiring: Set<CleanupOwner>;\n cleanup?: Promise<unknown>;\n cleanupState?: {\n operationId?: string;\n status: 'pending' | 'completed' | 'failed';\n blockers: Set<ResourceOperation>;\n error?: unknown;\n };\n}\n\ninterface ResourceState {\n resources: WeakMap<object, ResourceHandle>;\n scopes: WeakMap<AbortSignal, ResourceScope>;\n parents: WeakMap<AbortSignal, AbortSignal>;\n cleanupOwners: WeakMap<AbortSignal, CleanupOwner>;\n identities: WeakMap<AbortSignal, OperationIdentity>;\n listeners: WeakMap<AbortSignal, Set<OperationListener>>;\n}\n\n// CJS and ESM hosts must agree about ownership of the same Agent instance.\nconst admissionErrorKey = Symbol.for('@midscene/core/resource-admission-error');\nconst stateKey = Symbol.for('@midscene/core/resource-operations/v4');\nconst host = globalThis as typeof globalThis & { [stateKey]?: ResourceState };\nhost[stateKey] ??= {\n resources: new WeakMap(),\n scopes: new WeakMap(),\n parents: new WeakMap(),\n cleanupOwners: new WeakMap(),\n identities: new WeakMap(),\n listeners: new WeakMap(),\n};\nconst { resources, scopes, parents, cleanupOwners, identities, listeners } =\n host[stateKey];\nconst resourceHandle = (resource: object): ResourceHandle => {\n let handle = resources.get(resource);\n if (!handle) {\n handle = {\n operations: new Map(),\n retiring: new Set(),\n };\n resources.set(resource, handle);\n }\n return handle;\n};\n\n/** Actual teardown completion belongs to the owner, never to a thrown error. */\nexport const getResourceCleanupCompletion = (\n owner: object,\n): Promise<unknown> | undefined => resources.get(owner)?.cleanup;\n\nfunction blockingReferences(\n blockers: Iterable<ResourceOperation>,\n): TestRunReportOperationReference[] {\n const references = new Map<string, TestRunReportOperationReference>();\n const visited = new Set<ResourceOperation>();\n const visit = (record: ResourceOperation) => {\n if (visited.has(record)) return;\n visited.add(record);\n if (!record.settled)\n references.set(record.operationId, {\n operationId: record.operationId,\n ...record.identity,\n });\n for (const blocker of record.blockers ?? []) visit(blocker);\n };\n for (const blocker of blockers) visit(blocker);\n return [...references.values()];\n}\n\n/** Read a copy at the publication boundary; never expose the live cleanup state. */\nexport function getResourceCleanupSnapshot(\n owner: object,\n): TestRunReportCleanup {\n const state = resources.get(owner)?.cleanupState;\n return freezeEvent({\n capturedAt: new Date().toISOString(),\n status: state?.status ?? 'not-required',\n ...(state?.operationId ? { operationId: state.operationId } : {}),\n ...(state?.status === 'pending'\n ? { blockingOperations: blockingReferences(state.blockers) }\n : {}),\n ...(state?.error === undefined\n ? {}\n : { error: sanitizeReportError(state.error) }),\n });\n}\n\nexport const RESOURCE_CLEANUP_GRACE_MS = 1000;\nexport const RESOURCE_RECOVERY_GRACE_MS = 30_000;\nconst RESOURCE_TEARDOWN_GRACE_MS = 10_000;\n\nexport function getPendingScopeOperations(\n signal: AbortSignal,\n): TestRunReportOperationReference[] {\n return freezeEvent(\n blockingReferences(\n [...(scopes.get(signal)?.operations.keys() ?? [])].flatMap((resource) => [\n ...(resources.get(resource)?.operations.values() ?? []),\n ]),\n ),\n );\n}\n\n/** Snapshotting must not wait for a timed-out operation to release its resource. */\nexport const hasPendingResourceOperation = (resource: object): boolean =>\n (resources.get(resource)?.operations.size ?? 0) > 0;\n\n/** Resource ancestry is independent of cancellation: cleanup can outlive abort. */\nexport function linkResourceSignal(signal: AbortSignal, parent?: AbortSignal) {\n if (parent && parent !== signal) parents.set(signal, parent);\n}\n\nfunction* ownerSignals(signal?: AbortSignal): Generator<AbortSignal> {\n const seen = new Set<AbortSignal>();\n for (\n let owner = signal;\n owner && !seen.has(owner);\n owner = parents.get(owner)\n ) {\n seen.add(owner);\n yield owner;\n }\n}\n\n/** Each Case/document owns its cleanup, even when cancellation is shared. */\nexport function createResourceScope(parent?: AbortSignal) {\n const controller = new AbortController();\n const scope: ResourceScope = { operations: new Map(), resources: new Set() };\n scopes.set(controller.signal, scope);\n linkResourceSignal(controller.signal, parent);\n const abort = () => controller.abort(parent?.reason);\n if (parent?.aborted) abort();\n else parent?.addEventListener('abort', abort, { once: true });\n return {\n signal: controller.signal,\n dispose() {\n parent?.removeEventListener('abort', abort);\n scope.resources = undefined;\n if (!scope.operations.size) scopes.delete(controller.signal);\n },\n };\n}\n\nexport function assertResourceAvailable(\n resource: object,\n exclusive = false,\n signal?: AbortSignal,\n): boolean {\n const retiringOwners = resources.get(resource)?.retiring;\n const isOwnerCleanup = [...ownerSignals(signal)].some((ownerSignal) => {\n const owner = cleanupOwners.get(ownerSignal);\n return owner !== undefined && retiringOwners?.has(owner);\n });\n if (retiringOwners?.size && !isOwnerCleanup)\n throw new WorkflowError(\n 'A resource cannot execute overlapping Runner operations while its cleanup is pending.',\n { code: 'RESOURCE_CLEANUP_PENDING' },\n );\n const pending = resources.get(resource)?.operations;\n if (exclusive && pending?.size)\n throw new WorkflowError(\n 'The same Agent instance cannot execute overlapping Runner operations.',\n { code: 'RESOURCE_OPERATION_PENDING' },\n );\n if (\n pending &&\n [...pending.values()].some((operation) => operation.signal.aborted)\n )\n throw new WorkflowError(\n 'Cannot reuse an Agent while an aborted YAML action or Runner operation is still running.',\n { code: 'RESOURCE_OPERATION_PENDING' },\n );\n return isOwnerCleanup;\n}\n\n/** Capture subscribers at registration: late settlement stays with its original run. */\nexport function observeResourceOperations(\n signal: AbortSignal,\n listener: OperationListener,\n) {\n const subscribers = listeners.get(signal) ?? new Set<OperationListener>();\n listeners.set(signal, subscribers);\n subscribers.add(listener);\n return () => {\n subscribers.delete(listener);\n if (!subscribers.size) listeners.delete(signal);\n };\n}\n\nexport function setResourceOperationIdentity(\n signal: AbortSignal,\n identity: OperationIdentity,\n) {\n identities.set(signal, Object.freeze({ ...identity }));\n}\n\nfunction freezeEvent<T>(value: T): T {\n if (typeof value === 'object' && value !== null && !Object.isFrozen(value)) {\n for (const child of Object.values(value)) freezeEvent(child);\n Object.freeze(value);\n }\n return value;\n}\n\nexport function recordOperation(\n operation: Promise<unknown>,\n signal: AbortSignal,\n operationKind: TestRunReportOperationEvent['operationKind'],\n) {\n const owners = [...ownerSignals(signal)];\n const identity = owners.map((owner) => identities.get(owner)).find(Boolean);\n const record: ResourceOperation = { signal, operationId: uuid(), identity };\n const subscribers = new Set(\n owners.flatMap((owner) => [...(listeners.get(owner) ?? [])]),\n );\n let settled = false;\n const emit = (\n event: Omit<\n TestRunReportOperationEvent,\n 'operationId' | 'operationKind' | 'at'\n >,\n ) => {\n const frozen = freezeEvent({\n ...identity,\n operationId: record.operationId,\n operationKind,\n at: new Date().toISOString(),\n ...event,\n });\n for (const subscriber of subscribers) {\n try {\n subscriber(frozen);\n } catch (error) {\n getDebug('runner:operations', { console: true })(\n `Operation observer failed: ${String(error)}`,\n );\n }\n }\n };\n const abort = () =>\n emit({\n kind: 'cancel-requested',\n error: sanitizeReportError(signal.reason),\n });\n emit({ kind: 'started' });\n if (signal.aborted) abort();\n else signal.addEventListener('abort', abort, { once: true });\n const finish = (\n status: 'success' | 'failed' | 'not-run',\n error?: unknown,\n ) => {\n settled = true;\n record.settled = true;\n signal.removeEventListener('abort', abort);\n emit({\n kind: 'settled',\n status,\n ...(status !== 'success' ? { error: sanitizeReportError(error) } : {}),\n });\n };\n void operation.then(\n () => finish('success'),\n (error) =>\n finish(isResourceAdmissionError(error) ? 'not-run' : 'failed', error),\n );\n return {\n record,\n runnerReturned(status: 'success' | 'failed' | 'not-run') {\n emit({ kind: 'runner-returned', status, pending: !settled });\n },\n cleanupDeferred(blockers: Iterable<ResourceOperation>) {\n emit({\n kind: 'cleanup-deferred',\n blockingOperations: blockingReferences(blockers),\n });\n },\n };\n}\n\n/** A scheduling decision, distinct from an action that ran and failed. */\nexport class ResourceAdmissionError extends WorkflowError {\n readonly blockedBy?: string;\n readonly [admissionErrorKey] = true;\n constructor(code: string, blockers: readonly ResourceOperation[]) {\n super(\n 'The Agent is occupied by an unfinished operation or pending cleanup; this action was not started.',\n {\n code,\n details: {\n blockingOperations: blockers.map(({ operationId, identity }) => ({\n operationId,\n ...identity,\n })),\n },\n },\n );\n this.blockedBy = blockers.find(\n (blocker) => blocker.identity?.stepId,\n )?.identity?.stepId;\n }\n}\n\nexport function isResourceAdmissionError(\n error: unknown,\n): error is ResourceAdmissionError {\n return (\n typeof error === 'object' &&\n error !== null &&\n (error as ResourceAdmissionError)[admissionErrorKey] === true\n );\n}\n\nexport function admitResourceOperation(\n resource: object,\n signal: AbortSignal,\n exclusive = true,\n): boolean {\n let isCleanupOwner: boolean;\n try {\n isCleanupOwner = assertResourceAvailable(resource, exclusive, signal);\n } catch (error) {\n if (\n error instanceof WorkflowError &&\n ['RESOURCE_OPERATION_PENDING', 'RESOURCE_CLEANUP_PENDING'].includes(\n error.code,\n )\n )\n throw new ResourceAdmissionError(error.code, [\n ...(resources.get(resource)?.operations.values() ?? []),\n ...[...(resources.get(resource)?.retiring ?? [])].flatMap((owner) => [\n ...owner.blockers,\n ]),\n ]);\n throw error;\n }\n return isCleanupOwner;\n}\n\nconst retainForCleanup = (owner: CleanupOwner, resource: object) => {\n owner.resources.add(resource);\n const handle = resourceHandle(resource);\n for (const operation of handle.operations.values())\n owner.blockers.add(operation);\n handle.retiring.add(owner);\n};\n\n/** Acquisition establishes cleanup ownership even if action admission is rejected. */\nexport function retainResourceForScope(resource: object, signal: AbortSignal) {\n let retained = false;\n for (const ownerSignal of ownerSignals(signal)) {\n const scope = scopes.get(ownerSignal);\n if (!retained && scope?.resources) {\n scope.resources.add(resource);\n retained = true;\n }\n if (scope?.cleanupOwner) retainForCleanup(scope.cleanupOwner, resource);\n }\n}\n\n/** Track actual completion, not the Runner timeout race. */\nexport function trackResourceOperation<T>(\n resource: object,\n operation: Promise<T>,\n signal: AbortSignal,\n kind: TestRunReportOperationEvent['operationKind'] = 'resource',\n existingObservation?: ReturnType<typeof recordOperation>,\n): Promise<T> {\n const observation =\n existingObservation ?? recordOperation(operation, signal, kind);\n const active = resourceHandle(resource).operations;\n active.set(operation, observation.record);\n const ownerScopes: Array<[AbortSignal, ResourceScope]> = [];\n let retainedByScope = false;\n for (const ownerSignal of ownerSignals(signal)) {\n const scope: ResourceScope = scopes.get(ownerSignal) ?? {\n operations: new Map(),\n };\n scopes.set(ownerSignal, scope);\n scope.operations.set(resource, (scope.operations.get(resource) ?? 0) + 1);\n ownerScopes.push([ownerSignal, scope]);\n // Only the closest owner retains idle resources for its teardown. Parents\n // wait on active operations, not every Agent used by completed Cases.\n if (!retainedByScope && scope.resources) {\n scope.resources.add(resource);\n retainedByScope = true;\n }\n const cleanupOwner = cleanupOwners.get(ownerSignal);\n if (cleanupOwner) {\n cleanupOwner.blockers.add(observation.record);\n retainForCleanup(cleanupOwner, resource);\n }\n }\n const finish = () => {\n active.delete(operation);\n for (const [ownerSignal, scope] of ownerScopes) {\n const count = scope.operations.get(resource)! - 1;\n if (count) scope.operations.set(resource, count);\n else scope.operations.delete(resource);\n if (!scope.operations.size && !scope.resources)\n scopes.delete(ownerSignal);\n }\n };\n void operation.then(finish, finish);\n return operation;\n}\n\nexport async function waitForResourceIdle(resource: object): Promise<void> {\n while (resources.get(resource)?.operations?.size)\n await Promise.allSettled([...resources.get(resource)!.operations.keys()]);\n}\n\n/** Wait for this scope's cancelled work before scheduling its next Step. */\nexport async function recoverCancelledScopeResources(\n signal: AbortSignal,\n graceMs = RESOURCE_RECOVERY_GRACE_MS,\n): Promise<number> {\n signal.throwIfAborted();\n const startedAt = Date.now();\n const pendingOperations = () => {\n const pending: Array<{\n operation: Promise<unknown>;\n signal: AbortSignal;\n resource: object;\n record: ResourceOperation;\n }> = [];\n for (const resource of scopes.get(signal)?.operations.keys() ?? []) {\n for (const [operation, tracked] of resources.get(resource)?.operations ??\n []) {\n const ownerSignal = tracked.signal;\n if (\n ownerSignal.aborted &&\n [...ownerSignals(ownerSignal)].includes(signal)\n )\n pending.push({\n operation,\n signal: ownerSignal,\n resource,\n record: tracked,\n });\n }\n }\n return pending;\n };\n let pending = pendingOperations();\n if (!pending.length) return 0;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let onAbort!: () => void;\n const stopped = new Promise<never>((_resolve, reject) => {\n onAbort = () => reject(signal.reason);\n signal.addEventListener('abort', onAbort, { once: true });\n timer = setTimeout(() => {\n const blockingOperations = blockingReferences(\n pending.map(({ record }) => record),\n );\n const blockedByStepId = blockingOperations.every(\n ({ stepId }) => stepId === blockingOperations[0]?.stepId,\n )\n ? blockingOperations[0]?.stepId\n : undefined;\n reject(\n new WorkflowError(\n `The previous operation was still running after ${graceMs / 1000} s. Later steps were not started.`,\n {\n code: 'RESOURCE_RECOVERY_TIMEOUT',\n details: {\n graceMs,\n waitedMs: Date.now() - startedAt,\n resourceCount: new Set(pending.map(({ resource }) => resource))\n .size,\n blockingOperations,\n blockedByStepId,\n },\n cause: pending[0]?.signal.reason,\n },\n ),\n );\n }, graceMs);\n });\n try {\n while (pending.length) {\n await Promise.race([\n Promise.allSettled(pending.map(({ operation }) => operation)),\n stopped,\n ]);\n signal.throwIfAborted();\n pending = pendingOperations();\n }\n return Date.now() - startedAt;\n } finally {\n clearTimeout(timer);\n signal.removeEventListener('abort', onAbort);\n }\n}\n\nexport class ResourceCleanupDeferredError extends WorkflowError {\n constructor(\n graceMs: number,\n resourceCount: number,\n blockers: readonly ResourceOperation[] = [],\n ) {\n super(\n `Resource cleanup did not finish within ${graceMs}ms. Resources remain quarantined until cleanup settles.`,\n {\n code: 'RESOURCE_CLEANUP_DEFERRED',\n details: {\n graceMs,\n resourceCount,\n blockingOperations: blockers.map(({ operationId, identity }) => ({\n operationId,\n ...identity,\n })),\n },\n },\n );\n }\n}\n\ninterface CleanupOptions {\n /** Stable owner of this cleanup, distinct from borrowed resource identities. */\n owner?: object;\n /** Parent scope observes deferred completion without host-specific forwarding. */\n signal?: AbortSignal;\n graceMs?: number;\n onDeferredError(error: unknown): void;\n}\n\n/** Bound waiting, but keep ownership until the actual cleanup has finished. */\nasync function runResourceCleanup<T>(\n resources: readonly object[],\n cleanup: () => Promise<T>,\n options: CleanupOptions,\n beforeCleanup?: {\n signal: AbortSignal;\n run(signal: AbortSignal): Promise<void>;\n },\n): Promise<T> {\n const owner: CleanupOwner = {\n resources: new Set(),\n blockers: new Set(),\n signal: beforeCleanup?.signal,\n };\n for (const resource of resources) retainForCleanup(owner, resource);\n if (owner.signal) cleanupOwners.set(owner.signal, owner);\n const ownerScope = options.signal ? scopes.get(options.signal) : undefined;\n if (ownerScope) ownerScope.cleanupOwner = owner;\n\n let defer!: () => void;\n const deferred = new Promise<{ deferred: true }>((resolve) => {\n defer = () => resolve({ deferred: true });\n });\n let graceMs = options.graceMs ?? RESOURCE_CLEANUP_GRACE_MS;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const drain = async () => {\n timer = setTimeout(defer, graceMs);\n try {\n // An acquisition may finish late and add another shared resource to its\n // owner. Keep draining that resource before calling any disposer.\n while (true) {\n const pending = [...owner.resources].filter(\n (resource) => resourceHandle(resource).operations.size,\n );\n if (!pending.length) return;\n await Promise.all(pending.map(waitForResourceIdle));\n }\n } finally {\n clearTimeout(timer);\n }\n };\n const completion = (async () => {\n try {\n if (beforeCleanup) {\n // Hooks may use idle resources owned by this cleanup. Busy resources\n // still reject overlapping actions, including cleanup actions.\n await beforeCleanup.run(beforeCleanup.signal);\n }\n // Lifecycle hooks can themselves time out with an action still running.\n // Drain both the original work and any operation started by the hook\n // before disposing the resource.\n await drain();\n graceMs = options.graceMs ?? RESOURCE_TEARDOWN_GRACE_MS;\n timer = setTimeout(defer, graceMs);\n return await cleanup();\n } finally {\n clearTimeout(timer);\n if (owner.signal) cleanupOwners.delete(owner.signal);\n if (ownerScope?.cleanupOwner === owner)\n ownerScope.cleanupOwner = undefined;\n for (const resource of owner.resources) {\n const owners = resourceHandle(resource).retiring;\n owners.delete(owner);\n }\n }\n })();\n const cleanupObservation = options.signal\n ? recordOperation(completion, options.signal, 'cleanup')\n : undefined;\n const ownerKey = options.owner ?? beforeCleanup?.signal ?? resources[0];\n const cleanupState: NonNullable<ResourceHandle['cleanupState']> = {\n status: 'pending',\n operationId: cleanupObservation?.record.operationId,\n blockers: owner.blockers,\n };\n if (cleanupObservation) cleanupObservation.record.blockers = owner.blockers;\n if (ownerKey) {\n resourceHandle(ownerKey).cleanup = completion;\n resourceHandle(ownerKey).cleanupState = cleanupState;\n }\n void completion.then(\n () => {\n cleanupState.status = 'completed';\n },\n (error) => {\n cleanupState.status = 'failed';\n cleanupState.error = error;\n },\n );\n const outcome = await Promise.race([\n completion.then((value) => ({ value })),\n deferred,\n ]);\n if ('deferred' in outcome) {\n cleanupObservation?.cleanupDeferred(owner.blockers);\n void completion.catch((error) => {\n try {\n options.onDeferredError(error);\n } catch {\n // The returned completion still carries the original cleanup failure.\n }\n });\n if (options.signal)\n trackResourceOperation(\n {},\n completion,\n options.signal,\n 'cleanup',\n cleanupObservation,\n );\n throw new ResourceCleanupDeferredError(graceMs, owner.resources.size, [\n ...owner.blockers,\n ]);\n }\n return outcome.value;\n}\n\nexport function cleanupResources<T>(\n resources: readonly object[],\n cleanup: () => Promise<T>,\n options: CleanupOptions,\n): Promise<T> {\n return runResourceCleanup(resources, cleanup, options);\n}\n\nexport async function cleanupScopeResources<T>(\n signal: AbortSignal,\n cleanup: () => Promise<T>,\n options: CleanupOptions & {\n beforeCleanup?(signal: AbortSignal): Promise<void>;\n },\n): Promise<T> {\n const cleanupSignal = new AbortController().signal;\n linkResourceSignal(cleanupSignal, signal);\n const scope = scopes.get(signal);\n return runResourceCleanup(\n [...(scope?.resources ?? []), ...(scope?.operations.keys() ?? [])],\n cleanup,\n { ...options, owner: signal, signal },\n options.beforeCleanup\n ? { signal: cleanupSignal, run: options.beforeCleanup }\n : undefined,\n );\n}\n"],"names":["admissionErrorKey","Symbol","stateKey","host","globalThis","WeakMap","resources","scopes","parents","cleanupOwners","identities","listeners","resourceHandle","resource","handle","Map","Set","getResourceCleanupCompletion","owner","blockingReferences","blockers","references","visited","visit","record","blocker","getResourceCleanupSnapshot","state","freezeEvent","Date","undefined","sanitizeReportError","RESOURCE_CLEANUP_GRACE_MS","RESOURCE_RECOVERY_GRACE_MS","RESOURCE_TEARDOWN_GRACE_MS","getPendingScopeOperations","signal","hasPendingResourceOperation","linkResourceSignal","parent","ownerSignals","seen","createResourceScope","controller","AbortController","scope","abort","assertResourceAvailable","exclusive","retiringOwners","isOwnerCleanup","ownerSignal","WorkflowError","pending","operation","observeResourceOperations","listener","subscribers","setResourceOperationIdentity","identity","Object","value","child","recordOperation","operationKind","owners","Boolean","uuid","settled","emit","event","frozen","subscriber","error","getDebug","String","finish","status","isResourceAdmissionError","_admissionErrorKey","ResourceAdmissionError","code","operationId","admitResourceOperation","isCleanupOwner","retainForCleanup","retainResourceForScope","retained","trackResourceOperation","kind","existingObservation","observation","active","ownerScopes","retainedByScope","cleanupOwner","count","waitForResourceIdle","Promise","recoverCancelledScopeResources","graceMs","startedAt","pendingOperations","tracked","timer","onAbort","stopped","_resolve","reject","setTimeout","blockingOperations","blockedByStepId","stepId","clearTimeout","ResourceCleanupDeferredError","resourceCount","runResourceCleanup","cleanup","options","beforeCleanup","ownerScope","defer","deferred","resolve","drain","completion","cleanupObservation","ownerKey","cleanupState","outcome","cleanupResources","cleanupScopeResources","cleanupSignal"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function phaseResult(plannedStepCount, steps) {
|
|
2
|
+
const executedStepCount = steps.filter((step)=>'not-run' !== step.status).length;
|
|
3
|
+
return {
|
|
4
|
+
plannedStepCount,
|
|
5
|
+
executedStepCount,
|
|
6
|
+
status: 0 === plannedStepCount ? 'not-required' : steps.some((step)=>'failed' === step.status) ? 'failed' : executedStepCount < plannedStepCount ? 'not-run' : 'success'
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function decideRetry(options) {
|
|
10
|
+
const reason = options.infrastructureError ? 'infrastructure-error' : options.cleanupPending ? 'cleanup-pending' : options.cleanupFailed ? 'cleanup-failed' : options.operationPending ? 'operation-pending' : options.fatalError ? 'fatal-error' : options.interrupted ? 'interrupted' : options.shouldStop ? 'bail' : 'success' === options.status ? 'passed' : options.attemptIndex >= options.retryCount ? 'budget-exhausted' : 'attempt-failed';
|
|
11
|
+
return {
|
|
12
|
+
scope: options.scope,
|
|
13
|
+
retryCount: options.retryCount,
|
|
14
|
+
attemptIndex: options.attemptIndex,
|
|
15
|
+
decision: 'attempt-failed' === reason ? 'retry' : 'stop',
|
|
16
|
+
reason
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export { decideRetry, phaseResult };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=result-metadata.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-runner/engine/result-metadata.mjs","sources":["../../../../src/test-runner/engine/result-metadata.ts"],"sourcesContent":["import type {\n TestRunReportPhaseResult,\n TestRunReportRetryDecision,\n} from '../../test-run-report';\nimport type { StepRunResult } from './types';\n\nexport function phaseResult(\n plannedStepCount: number,\n steps: readonly StepRunResult[],\n): TestRunReportPhaseResult {\n const executedStepCount = steps.filter(\n (step) => step.status !== 'not-run',\n ).length;\n return {\n plannedStepCount,\n executedStepCount,\n status:\n plannedStepCount === 0\n ? 'not-required'\n : steps.some((step) => step.status === 'failed')\n ? 'failed'\n : executedStepCount < plannedStepCount\n ? 'not-run'\n : 'success',\n };\n}\n\n/** Facts at the retry boundary, separate from the eventual cleanup outcome. */\nexport function decideRetry(options: {\n scope: 'case' | 'document';\n retryCount: number;\n attemptIndex: number;\n status: 'success' | 'failed';\n cleanupPending?: boolean;\n cleanupFailed?: boolean;\n operationPending?: boolean;\n infrastructureError?: boolean;\n fatalError?: boolean;\n interrupted?: boolean;\n shouldStop?: boolean;\n}): TestRunReportRetryDecision {\n const reason = options.infrastructureError\n ? 'infrastructure-error'\n : options.cleanupPending\n ? 'cleanup-pending'\n : options.cleanupFailed\n ? 'cleanup-failed'\n : options.operationPending\n ? 'operation-pending'\n : options.fatalError\n ? 'fatal-error'\n : options.interrupted\n ? 'interrupted'\n : options.shouldStop\n ? 'bail'\n : options.status === 'success'\n ? 'passed'\n : options.attemptIndex >= options.retryCount\n ? 'budget-exhausted'\n : 'attempt-failed';\n return {\n scope: options.scope,\n retryCount: options.retryCount,\n attemptIndex: options.attemptIndex,\n decision: reason === 'attempt-failed' ? 'retry' : 'stop',\n reason,\n };\n}\n"],"names":["phaseResult","plannedStepCount","steps","executedStepCount","step","decideRetry","options","reason"],"mappings":"AAMO,SAASA,YACdC,gBAAwB,EACxBC,KAA+B;IAE/B,MAAMC,oBAAoBD,MAAM,MAAM,CACpC,CAACE,OAASA,AAAgB,cAAhBA,KAAK,MAAM,EACrB,MAAM;IACR,OAAO;QACLH;QACAE;QACA,QACEF,AAAqB,MAArBA,mBACI,iBACAC,MAAM,IAAI,CAAC,CAACE,OAASA,AAAgB,aAAhBA,KAAK,MAAM,IAC9B,WACAD,oBAAoBF,mBAClB,YACA;IACZ;AACF;AAGO,SAASI,YAAYC,OAY3B;IACC,MAAMC,SAASD,QAAQ,mBAAmB,GACtC,yBACAA,QAAQ,cAAc,GACpB,oBACAA,QAAQ,aAAa,GACnB,mBACAA,QAAQ,gBAAgB,GACtB,sBACAA,QAAQ,UAAU,GAChB,gBACAA,QAAQ,WAAW,GACjB,gBACAA,QAAQ,UAAU,GAChB,SACAA,AAAmB,cAAnBA,QAAQ,MAAM,GACZ,WACAA,QAAQ,YAAY,IAAIA,QAAQ,UAAU,GACxC,qBACA;IACpB,OAAO;QACL,OAAOA,QAAQ,KAAK;QACpB,YAAYA,QAAQ,UAAU;QAC9B,cAAcA,QAAQ,YAAY;QAClC,UAAUC,AAAW,qBAAXA,SAA8B,UAAU;QAClDA;IACF;AACF"}
|