@workglow/test-contract 0.6.1
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/README.md +232 -0
- package/dist/ai-provider/assertions/capabilityHonesty.d.ts +7 -0
- package/dist/ai-provider/assertions/dispose.d.ts +7 -0
- package/dist/ai-provider/assertions/inferAdvertisesRegistered.d.ts +15 -0
- package/dist/ai-provider/assertions/inferServesInferred.d.ts +20 -0
- package/dist/ai-provider/assertions/pricingMatchesModality.d.ts +36 -0
- package/dist/ai-provider/assertions/registryCoverage.d.ts +13 -0
- package/dist/ai-provider/assertions/sessionReuse.d.ts +7 -0
- package/dist/ai-provider/assertions/signalHonoring.d.ts +7 -0
- package/dist/ai-provider/assertions/structuredGeneration.d.ts +7 -0
- package/dist/ai-provider/assertions/textGenerationSmoke.d.ts +7 -0
- package/dist/ai-provider/assertions/toolCallAccumulator.d.ts +7 -0
- package/dist/ai-provider/assertions/toolCallMultiTurn.d.ts +7 -0
- package/dist/ai-provider/assertions/usageNormalization.d.ts +61 -0
- package/dist/ai-provider/fixtures.d.ts +8 -0
- package/dist/ai-provider/runAiProviderConformance.d.ts +7 -0
- package/dist/ai-provider/types.d.ts +75 -0
- package/dist/ai-provider.d.ts +25 -0
- package/dist/ai-provider.js +876 -0
- package/dist/ai-provider.js.map +26 -0
- package/dist/browser-context/ConformanceMockContext.d.ts +54 -0
- package/dist/browser-context/assertions/ariaRoundTrip.d.ts +7 -0
- package/dist/browser-context/assertions/capabilityHonesty.d.ts +7 -0
- package/dist/browser-context/assertions/itExpectFail.d.ts +8 -0
- package/dist/browser-context/assertions/networkIntrospection.d.ts +7 -0
- package/dist/browser-context/assertions/tabsLifecycle.d.ts +7 -0
- package/dist/browser-context/fixtures.d.ts +8 -0
- package/dist/browser-context/runIBrowserContextConformance.d.ts +7 -0
- package/dist/browser-context/types.d.ts +48 -0
- package/dist/browser-context.d.ts +18 -0
- package/dist/browser-context.js +567 -0
- package/dist/browser-context.js.map +17 -0
- package/dist/creditExhaustedSkip.d.ts +39 -0
- package/dist/creditExhaustedSkip.js +163 -0
- package/dist/creditExhaustedSkip.js.map +10 -0
- package/dist/entitlement-profile/assertions/denialShape.d.ts +13 -0
- package/dist/entitlement-profile/assertions/dispose.d.ts +16 -0
- package/dist/entitlement-profile/assertions/hierarchyHonoring.d.ts +11 -0
- package/dist/entitlement-profile/assertions/optionalNeverDenied.d.ts +11 -0
- package/dist/entitlement-profile/assertions/requestEntitlementShape.d.ts +12 -0
- package/dist/entitlement-profile/assertions/resourceScoping.d.ts +16 -0
- package/dist/entitlement-profile/assertions/subscribeGrant.d.ts +11 -0
- package/dist/entitlement-profile/assertions/subscribeReload.d.ts +11 -0
- package/dist/entitlement-profile/assertions/subscribeRevocation.d.ts +12 -0
- package/dist/entitlement-profile/assertions/surfaceCoverage.d.ts +7 -0
- package/dist/entitlement-profile/assertions/unsubscribeIdempotent.d.ts +7 -0
- package/dist/entitlement-profile/fixtures.d.ts +21 -0
- package/dist/entitlement-profile/runEntitlementProfileConformance.d.ts +7 -0
- package/dist/entitlement-profile/types.d.ts +45 -0
- package/dist/entitlement-profile.d.ts +23 -0
- package/dist/entitlement-profile.js +436 -0
- package/dist/entitlement-profile.js.map +22 -0
- package/dist/human-connector/MockHumanConnector.d.ts +29 -0
- package/dist/human-connector/assertions/abort.d.ts +7 -0
- package/dist/human-connector/assertions/capabilityHonesty.d.ts +7 -0
- package/dist/human-connector/assertions/concurrentIsolation.d.ts +7 -0
- package/dist/human-connector/assertions/multiTurnFollowUp.d.ts +7 -0
- package/dist/human-connector/assertions/notifyDisplayFastResolve.d.ts +7 -0
- package/dist/human-connector/assertions/roundtrip.d.ts +7 -0
- package/dist/human-connector/fixtures.d.ts +8 -0
- package/dist/human-connector/runHumanConnectorConformance.d.ts +9 -0
- package/dist/human-connector/types.d.ts +91 -0
- package/dist/human-connector.d.ts +19 -0
- package/dist/human-connector.js +843 -0
- package/dist/human-connector.js.map +20 -0
- package/dist/itExpectFail.d.ts +15 -0
- package/dist/itExpectFail.js +180 -0
- package/dist/itExpectFail.js.map +11 -0
- package/dist/storage-migrations/assertions/appliesAndRecords.d.ts +7 -0
- package/dist/storage-migrations/assertions/concurrentRunsSerialize.d.ts +18 -0
- package/dist/storage-migrations/assertions/ensureBookkeepingIdempotent.d.ts +7 -0
- package/dist/storage-migrations/assertions/failedMigrationLeavesNoPartialSchema.d.ts +14 -0
- package/dist/storage-migrations/assertions/failedMigrationNotRecorded.d.ts +7 -0
- package/dist/storage-migrations/assertions/idempotentRun.d.ts +7 -0
- package/dist/storage-migrations/assertions/incrementalApplication.d.ts +7 -0
- package/dist/storage-migrations/runMigrationRunnerContract.d.ts +9 -0
- package/dist/storage-migrations/types.d.ts +78 -0
- package/dist/storage-migrations.d.ts +18 -0
- package/dist/storage-migrations.js +473 -0
- package/dist/storage-migrations.js.map +20 -0
- package/dist/tabular-migrations/assertions/addAndDropIndex.d.ts +7 -0
- package/dist/tabular-migrations/assertions/addColumn.d.ts +7 -0
- package/dist/tabular-migrations/assertions/backfill.d.ts +7 -0
- package/dist/tabular-migrations/assertions/dropColumn.d.ts +7 -0
- package/dist/tabular-migrations/assertions/failedMigrationNotRecorded.d.ts +7 -0
- package/dist/tabular-migrations/assertions/freshDbFastPath.d.ts +7 -0
- package/dist/tabular-migrations/assertions/incrementalApplication.d.ts +7 -0
- package/dist/tabular-migrations/assertions/renameColumn.d.ts +7 -0
- package/dist/tabular-migrations/runTabularMigrationContract.d.ts +8 -0
- package/dist/tabular-migrations/types.d.ts +28 -0
- package/dist/tabular-migrations.d.ts +19 -0
- package/dist/tabular-migrations.js +257 -0
- package/dist/tabular-migrations.js.map +18 -0
- package/dist/tabular-storage/assertions/countMatchesQuery.d.ts +7 -0
- package/dist/tabular-storage/assertions/guardParity.d.ts +25 -0
- package/dist/tabular-storage/assertions/inListCriterion.d.ts +14 -0
- package/dist/tabular-storage/assertions/joinBoundedLeftRead.d.ts +29 -0
- package/dist/tabular-storage/assertions/joinPushdown.d.ts +13 -0
- package/dist/tabular-storage/assertions/notInListCriterion.d.ts +15 -0
- package/dist/tabular-storage/assertions/strategyParity.d.ts +31 -0
- package/dist/tabular-storage/assertions/subscribeToChanges.d.ts +21 -0
- package/dist/tabular-storage/assertions/vectorColumnFormat.d.ts +7 -0
- package/dist/tabular-storage/assertions/withConnectionTransaction.d.ts +7 -0
- package/dist/tabular-storage/assertions/withTransactionRollback.d.ts +7 -0
- package/dist/tabular-storage/fixtures.d.ts +33 -0
- package/dist/tabular-storage/joinFixtures.d.ts +66 -0
- package/dist/tabular-storage/runTabularJoinContract.d.ts +24 -0
- package/dist/tabular-storage/runTabularStorageContract.d.ts +9 -0
- package/dist/tabular-storage/types.d.ts +102 -0
- package/dist/tabular-storage.d.ts +25 -0
- package/dist/tabular-storage.js +1477 -0
- package/dist/tabular-storage.js.map +27 -0
- package/dist/vector-storage/assertions/dimensionValidation.d.ts +17 -0
- package/dist/vector-storage/assertions/legacyEncoding.d.ts +16 -0
- package/dist/vector-storage/assertions/metadataFilter.d.ts +16 -0
- package/dist/vector-storage/assertions/searchOptions.d.ts +16 -0
- package/dist/vector-storage/assertions/shared.d.ts +23 -0
- package/dist/vector-storage/assertions/similarityRanking.d.ts +18 -0
- package/dist/vector-storage/assertions/similaritySearchEvent.d.ts +16 -0
- package/dist/vector-storage/assertions/vectorRoundTrip.d.ts +17 -0
- package/dist/vector-storage/runVectorStorageContract.d.ts +20 -0
- package/dist/vector-storage/types.d.ts +87 -0
- package/dist/vector-storage.d.ts +19 -0
- package/dist/vector-storage.js +412 -0
- package/dist/vector-storage.js.map +21 -0
- package/dist/web-search/assertions/capabilityAgreement.d.ts +23 -0
- package/dist/web-search/assertions/credentialNaming.d.ts +21 -0
- package/dist/web-search/assertions/dateFiltering.d.ts +22 -0
- package/dist/web-search/assertions/domainNormalization.d.ts +24 -0
- package/dist/web-search/assertions/maxResultsCeiling.d.ts +22 -0
- package/dist/web-search/assertions/reportedQuery.d.ts +18 -0
- package/dist/web-search/assertions/signalThreading.d.ts +20 -0
- package/dist/web-search/fixtures.d.ts +40 -0
- package/dist/web-search/runWebSearchProviderConformance.d.ts +24 -0
- package/dist/web-search/types.d.ts +79 -0
- package/dist/web-search.d.ts +19 -0
- package/dist/web-search.js +560 -0
- package/dist/web-search.js.map +18 -0
- package/dist/worker-proxy/assertions/backlogOrdering.d.ts +7 -0
- package/dist/worker-proxy/assertions/disposeTerminatesWorker.d.ts +8 -0
- package/dist/worker-proxy/assertions/errorPropagation.d.ts +7 -0
- package/dist/worker-proxy/assertions/providerCallHelpers.d.ts +23 -0
- package/dist/worker-proxy/browserOnlyStub.d.ts +7 -0
- package/dist/worker-proxy/runWorkerProxyBoundary.d.ts +7 -0
- package/dist/worker-proxy/types.d.ts +47 -0
- package/dist/worker-proxy.d.ts +16 -0
- package/dist/worker-proxy.js +389 -0
- package/dist/worker-proxy.js.map +17 -0
- package/package.json +120 -0
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
// src/human-connector/MockHumanConnector.ts
|
|
2
|
+
class Script {
|
|
3
|
+
_received = [];
|
|
4
|
+
_queue = [];
|
|
5
|
+
_pendingDeferreds = [];
|
|
6
|
+
get received() {
|
|
7
|
+
return this._received;
|
|
8
|
+
}
|
|
9
|
+
recordReceived(req) {
|
|
10
|
+
this._received.push(req);
|
|
11
|
+
}
|
|
12
|
+
push(entry) {
|
|
13
|
+
this._queue.push({ kind: "immediate", entry });
|
|
14
|
+
}
|
|
15
|
+
pushDeferred() {
|
|
16
|
+
const def = {
|
|
17
|
+
kind: "deferred",
|
|
18
|
+
resolved: { value: undefined },
|
|
19
|
+
settled: { done: false },
|
|
20
|
+
waiters: [],
|
|
21
|
+
rejecters: []
|
|
22
|
+
};
|
|
23
|
+
this._queue.push(def);
|
|
24
|
+
this._pendingDeferreds.push(def);
|
|
25
|
+
return {
|
|
26
|
+
release: (response) => {
|
|
27
|
+
if (def.settled.done)
|
|
28
|
+
return;
|
|
29
|
+
def.settled.done = true;
|
|
30
|
+
def.resolved.value = response;
|
|
31
|
+
for (const w of def.waiters)
|
|
32
|
+
w(response);
|
|
33
|
+
def.waiters.length = 0;
|
|
34
|
+
def.rejecters.length = 0;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
shift() {
|
|
39
|
+
return this._queue.shift();
|
|
40
|
+
}
|
|
41
|
+
clear() {
|
|
42
|
+
for (const def of this._pendingDeferreds) {
|
|
43
|
+
if (def.settled.done)
|
|
44
|
+
continue;
|
|
45
|
+
def.settled.done = true;
|
|
46
|
+
const err = new Error("MockHumanConnector script cleared while deferred was pending");
|
|
47
|
+
err.name = "AbortError";
|
|
48
|
+
for (const r of def.rejecters)
|
|
49
|
+
r(err);
|
|
50
|
+
def.rejecters.length = 0;
|
|
51
|
+
def.waiters.length = 0;
|
|
52
|
+
}
|
|
53
|
+
this._pendingDeferreds.length = 0;
|
|
54
|
+
this._received.length = 0;
|
|
55
|
+
this._queue.length = 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function defaultAbortError(signal) {
|
|
59
|
+
if (signal.reason instanceof Error)
|
|
60
|
+
return signal.reason;
|
|
61
|
+
const err = new Error("The operation was aborted");
|
|
62
|
+
err.name = "AbortError";
|
|
63
|
+
return err;
|
|
64
|
+
}
|
|
65
|
+
function awaitDeferred(def, signal) {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
if (signal.aborted) {
|
|
68
|
+
reject(defaultAbortError(signal));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (def.settled.done && def.resolved.value !== undefined) {
|
|
72
|
+
resolve(def.resolved.value);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const onAbort = () => {
|
|
76
|
+
signal.removeEventListener("abort", onAbort);
|
|
77
|
+
reject(defaultAbortError(signal));
|
|
78
|
+
};
|
|
79
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
80
|
+
def.waiters.push((res) => {
|
|
81
|
+
signal.removeEventListener("abort", onAbort);
|
|
82
|
+
resolve(res);
|
|
83
|
+
});
|
|
84
|
+
def.rejecters.push((err) => {
|
|
85
|
+
signal.removeEventListener("abort", onAbort);
|
|
86
|
+
reject(err);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class MockHumanConnector {
|
|
92
|
+
defaultAction;
|
|
93
|
+
_script;
|
|
94
|
+
followUp;
|
|
95
|
+
constructor(opts = {}) {
|
|
96
|
+
this.defaultAction = opts.defaultAction ?? "accept";
|
|
97
|
+
this._script = new Script;
|
|
98
|
+
if (opts.supportsFollowUp ?? true) {
|
|
99
|
+
this.followUp = (request, _previous, signal) => this.send(request, signal);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
get script() {
|
|
103
|
+
return this._script;
|
|
104
|
+
}
|
|
105
|
+
async send(request, signal) {
|
|
106
|
+
if (signal.aborted)
|
|
107
|
+
throw defaultAbortError(signal);
|
|
108
|
+
this._script.recordReceived(request);
|
|
109
|
+
if (request.kind === "notify" || request.kind === "display") {
|
|
110
|
+
return {
|
|
111
|
+
requestId: request.requestId,
|
|
112
|
+
action: this.defaultAction,
|
|
113
|
+
content: undefined,
|
|
114
|
+
done: true
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const entry = this._script.shift();
|
|
118
|
+
if (entry === undefined) {
|
|
119
|
+
return {
|
|
120
|
+
requestId: request.requestId,
|
|
121
|
+
action: this.defaultAction,
|
|
122
|
+
content: undefined,
|
|
123
|
+
done: true
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (entry.kind === "deferred") {
|
|
127
|
+
const res = await awaitDeferred(entry, signal);
|
|
128
|
+
return this.shape(request, res);
|
|
129
|
+
}
|
|
130
|
+
const resolved = typeof entry.entry === "function" ? await entry.entry(request) : entry.entry;
|
|
131
|
+
return this.shape(request, resolved);
|
|
132
|
+
}
|
|
133
|
+
shape(request, response) {
|
|
134
|
+
const carriesContent = request.kind === "elicit" && response.action === "accept";
|
|
135
|
+
return {
|
|
136
|
+
...response,
|
|
137
|
+
requestId: request.requestId,
|
|
138
|
+
content: carriesContent ? response.content : undefined
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// src/human-connector/fixtures.ts
|
|
143
|
+
var elicitSchema = {
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: {
|
|
146
|
+
approved: {
|
|
147
|
+
type: "boolean",
|
|
148
|
+
title: "Approved",
|
|
149
|
+
description: "Whether the request is approved"
|
|
150
|
+
},
|
|
151
|
+
reason: { type: "string", title: "Reason" }
|
|
152
|
+
},
|
|
153
|
+
required: ["approved"],
|
|
154
|
+
additionalProperties: false
|
|
155
|
+
};
|
|
156
|
+
var emptySchema = {
|
|
157
|
+
type: "object",
|
|
158
|
+
properties: {},
|
|
159
|
+
additionalProperties: true
|
|
160
|
+
};
|
|
161
|
+
var DEFAULT_HUMAN_CONFORMANCE_FIXTURE = {
|
|
162
|
+
elicitContentSchema: elicitSchema,
|
|
163
|
+
elicitAcceptContent: { approved: true, reason: "looks good" },
|
|
164
|
+
notifyRequest: {
|
|
165
|
+
message: "Job completed.",
|
|
166
|
+
contentSchema: emptySchema,
|
|
167
|
+
contentData: { jobId: "test-1" }
|
|
168
|
+
},
|
|
169
|
+
displayRequest: {
|
|
170
|
+
message: "Here is the result.",
|
|
171
|
+
contentSchema: emptySchema,
|
|
172
|
+
contentData: { result: 42 }
|
|
173
|
+
},
|
|
174
|
+
confirmRequest: {
|
|
175
|
+
message: 'Run workflow "nightly-export"?',
|
|
176
|
+
contentSchema: {
|
|
177
|
+
type: "object",
|
|
178
|
+
properties: {
|
|
179
|
+
action: { type: "string", title: "Action" },
|
|
180
|
+
reaches: { type: "string", title: "Reaches" }
|
|
181
|
+
},
|
|
182
|
+
additionalProperties: true
|
|
183
|
+
},
|
|
184
|
+
contentData: { action: "Run workflow", reaches: "network:http → https://example.test" }
|
|
185
|
+
},
|
|
186
|
+
abortGraceMs: 1000
|
|
187
|
+
};
|
|
188
|
+
function resolveHumanConformanceFixture(override) {
|
|
189
|
+
if (!override)
|
|
190
|
+
return DEFAULT_HUMAN_CONFORMANCE_FIXTURE;
|
|
191
|
+
return { ...DEFAULT_HUMAN_CONFORMANCE_FIXTURE, ...override };
|
|
192
|
+
}
|
|
193
|
+
// src/human-connector/runHumanConnectorConformance.ts
|
|
194
|
+
import { afterAll, beforeAll, beforeEach, describe as describe7 } from "vitest";
|
|
195
|
+
|
|
196
|
+
// src/human-connector/assertions/abort.ts
|
|
197
|
+
import { describe, expect, it as it2 } from "vitest";
|
|
198
|
+
|
|
199
|
+
// src/creditExhaustedSkip.ts
|
|
200
|
+
import { it as vitestIt } from "vitest";
|
|
201
|
+
var BILLING_CODES = new Set([
|
|
202
|
+
"insufficient_quota",
|
|
203
|
+
"billing_hard_limit_reached",
|
|
204
|
+
"insufficient_credits",
|
|
205
|
+
"insufficient_balance"
|
|
206
|
+
]);
|
|
207
|
+
var CREDIT_MESSAGE = /insufficient credits|not enough credits|no credits remaining|out of credits|insufficient[\s_-]balance|balance is (?:too low|insufficient)|exceeded[\s\S]{0,80}credits|monthly included credits|payment required|exceeded your current quota|check your plan and billing|billing_hard_limit/i;
|
|
208
|
+
var HTTP_402_MESSAGE = /\bHTTP\/?\d?(?:\.\d)?\s*402\b|\bstatus(?:\s*code)?\s*[:=]\s*402\b|\berror code:?\s*402\b|(?:^|:\s+)402\s+[a-z]/im;
|
|
209
|
+
function isCreditExhaustedError(err) {
|
|
210
|
+
const seen = new Set;
|
|
211
|
+
let current = err;
|
|
212
|
+
while (current !== undefined && current !== null && !seen.has(current)) {
|
|
213
|
+
seen.add(current);
|
|
214
|
+
if (matchesCreditExhaustion(current))
|
|
215
|
+
return true;
|
|
216
|
+
current = causeOf(current);
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
function matchesCreditExhaustion(err) {
|
|
221
|
+
if (typeof err === "string")
|
|
222
|
+
return messageLooksLikeCreditExhaustion(err);
|
|
223
|
+
if (!err || typeof err !== "object")
|
|
224
|
+
return false;
|
|
225
|
+
const rec = err;
|
|
226
|
+
const status = numericStatus(rec);
|
|
227
|
+
if (status === 402)
|
|
228
|
+
return true;
|
|
229
|
+
const code = stringish(rec.code) ?? stringish(nested(rec.error, "code"));
|
|
230
|
+
const type = stringish(rec.type) ?? stringish(nested(rec.error, "type"));
|
|
231
|
+
if (code && BILLING_CODES.has(code))
|
|
232
|
+
return true;
|
|
233
|
+
if (type && BILLING_CODES.has(type))
|
|
234
|
+
return true;
|
|
235
|
+
const message = [
|
|
236
|
+
stringish(rec.message),
|
|
237
|
+
stringish(nested(rec.error, "message")),
|
|
238
|
+
stringifyUnknown(err)
|
|
239
|
+
].filter((part) => typeof part === "string").join(`
|
|
240
|
+
`);
|
|
241
|
+
return messageLooksLikeCreditExhaustion(message);
|
|
242
|
+
}
|
|
243
|
+
function messageLooksLikeCreditExhaustion(message) {
|
|
244
|
+
return CREDIT_MESSAGE.test(message) || HTTP_402_MESSAGE.test(message);
|
|
245
|
+
}
|
|
246
|
+
function numericStatus(rec) {
|
|
247
|
+
for (const key of ["status", "statusCode"]) {
|
|
248
|
+
const value = rec[key];
|
|
249
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
250
|
+
return value;
|
|
251
|
+
}
|
|
252
|
+
const nestedStatus = nested(rec.error, "status");
|
|
253
|
+
if (typeof nestedStatus === "number" && Number.isFinite(nestedStatus))
|
|
254
|
+
return nestedStatus;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
function nested(value, key) {
|
|
258
|
+
if (!value || typeof value !== "object")
|
|
259
|
+
return;
|
|
260
|
+
return value[key];
|
|
261
|
+
}
|
|
262
|
+
function stringish(value) {
|
|
263
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
264
|
+
}
|
|
265
|
+
function stringifyUnknown(err) {
|
|
266
|
+
if (err instanceof Error)
|
|
267
|
+
return err.message;
|
|
268
|
+
return String(err);
|
|
269
|
+
}
|
|
270
|
+
function causeOf(err) {
|
|
271
|
+
if (!err || typeof err !== "object")
|
|
272
|
+
return;
|
|
273
|
+
return err.cause;
|
|
274
|
+
}
|
|
275
|
+
function creditSkipNote(err) {
|
|
276
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
277
|
+
return `skipped: provider out of credits (${message})`;
|
|
278
|
+
}
|
|
279
|
+
var SKIP_OVERRIDE_ENV = "WORKGLOW_CREDIT_EXHAUSTED_SKIP";
|
|
280
|
+
function isTruthyFlag(value) {
|
|
281
|
+
return !/^(?:0|false|off|no)$/i.test(value.trim());
|
|
282
|
+
}
|
|
283
|
+
function creditExhaustionSkipsTest(env = process.env) {
|
|
284
|
+
const override = env[SKIP_OVERRIDE_ENV];
|
|
285
|
+
if (override !== undefined && override.trim() !== "")
|
|
286
|
+
return isTruthyFlag(override);
|
|
287
|
+
for (const key of ["CI", "GITHUB_ACTIONS"]) {
|
|
288
|
+
const value = env[key];
|
|
289
|
+
if (value !== undefined && value.trim() !== "" && isTruthyFlag(value))
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
function reportLocalCreditFailure(err) {
|
|
295
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
296
|
+
console.error(`provider out of credits (${message})
|
|
297
|
+
` + ` Failing because this is not CI. Top the account up, or set ${SKIP_OVERRIDE_ENV}=1 to skip instead.`);
|
|
298
|
+
}
|
|
299
|
+
async function runWithCreditSkip(ctx, fn) {
|
|
300
|
+
try {
|
|
301
|
+
return await fn();
|
|
302
|
+
} catch (err) {
|
|
303
|
+
if (isCreditExhaustedError(err)) {
|
|
304
|
+
if (!creditExhaustionSkipsTest()) {
|
|
305
|
+
reportLocalCreditFailure(err);
|
|
306
|
+
throw err;
|
|
307
|
+
}
|
|
308
|
+
ctx.skip(creditSkipNote(err));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function wrapTestBodyForCreditSkip(fn) {
|
|
315
|
+
return async function wrapped(...args) {
|
|
316
|
+
try {
|
|
317
|
+
return await fn.apply(this, args);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (!isCreditExhaustedError(err))
|
|
320
|
+
throw err;
|
|
321
|
+
if (!creditExhaustionSkipsTest()) {
|
|
322
|
+
reportLocalCreditFailure(err);
|
|
323
|
+
throw err;
|
|
324
|
+
}
|
|
325
|
+
const ctx = args[0];
|
|
326
|
+
if (ctx && typeof ctx.skip === "function") {
|
|
327
|
+
ctx.skip(creditSkipNote(err));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
console.warn(creditSkipNote(err));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function wrapCallable(fn) {
|
|
336
|
+
return new Proxy(fn, {
|
|
337
|
+
apply(target, thisArg, argArray) {
|
|
338
|
+
const wrapped = argArray.map((arg) => typeof arg === "function" ? wrapTestBodyForCreditSkip(arg) : arg);
|
|
339
|
+
const result = Reflect.apply(target, thisArg, wrapped);
|
|
340
|
+
return typeof result === "function" ? wrapCallable(result) : result;
|
|
341
|
+
},
|
|
342
|
+
get(target, prop, receiver) {
|
|
343
|
+
const value = Reflect.get(target, prop, receiver);
|
|
344
|
+
if (typeof value === "function") {
|
|
345
|
+
return wrapCallable(value.bind(target));
|
|
346
|
+
}
|
|
347
|
+
return value;
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
var it = wrapCallable(vitestIt);
|
|
352
|
+
|
|
353
|
+
// src/itExpectFail.ts
|
|
354
|
+
var itExpectFail = (name, fn, timeout) => {
|
|
355
|
+
const native = it.fails;
|
|
356
|
+
if (typeof native === "function") {
|
|
357
|
+
native(name, fn, timeout);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
it(`${name} [expected-fail]`, async () => {
|
|
361
|
+
let passed = false;
|
|
362
|
+
try {
|
|
363
|
+
await fn();
|
|
364
|
+
passed = true;
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (isCreditExhaustedError(err))
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
369
|
+
if (passed) {
|
|
370
|
+
throw new Error(`Test "${name}" was marked as expected-fail but passed. Remove its name from opts.expectedFailures.`);
|
|
371
|
+
}
|
|
372
|
+
}, timeout);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// src/human-connector/assertions/abort.ts
|
|
376
|
+
function isAbortError(err) {
|
|
377
|
+
if (!err || typeof err !== "object")
|
|
378
|
+
return false;
|
|
379
|
+
const e = err;
|
|
380
|
+
return e.name === "AbortError" || /abort/i.test(e.message ?? "");
|
|
381
|
+
}
|
|
382
|
+
function elicitReq(fixture, requestId) {
|
|
383
|
+
return {
|
|
384
|
+
requestId,
|
|
385
|
+
targetHumanId: "default",
|
|
386
|
+
kind: "elicit",
|
|
387
|
+
message: "Please confirm.",
|
|
388
|
+
contentSchema: fixture.elicitContentSchema,
|
|
389
|
+
contentData: undefined,
|
|
390
|
+
expectsResponse: true,
|
|
391
|
+
mode: "single",
|
|
392
|
+
metadata: undefined
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function abortBlock(opts, fixture, getHandle) {
|
|
396
|
+
const expectFails = new Set(opts.expectedFailures ?? []);
|
|
397
|
+
const itBefore = expectFails.has("abort.beforeSend") ? itExpectFail : it2;
|
|
398
|
+
const itMid = expectFails.has("abort.midElicit") ? itExpectFail : it2;
|
|
399
|
+
describe.skipIf(!opts.capabilities.elicit)("Abort", () => {
|
|
400
|
+
itBefore("send() with already-aborted signal rejects with AbortError", async () => {
|
|
401
|
+
const { connector } = getHandle();
|
|
402
|
+
const ac = new AbortController;
|
|
403
|
+
ac.abort();
|
|
404
|
+
let caught;
|
|
405
|
+
try {
|
|
406
|
+
await connector.send(elicitReq(fixture, "ab-pre-1"), ac.signal);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
caught = err;
|
|
409
|
+
}
|
|
410
|
+
expect(caught).toBeDefined();
|
|
411
|
+
expect(isAbortError(caught)).toBe(true);
|
|
412
|
+
}, opts.timeout);
|
|
413
|
+
if (opts.capabilities.abortMidElicit) {
|
|
414
|
+
itMid("send() rejects with AbortError when signal aborts mid-elicit", async () => {
|
|
415
|
+
const { connector, script } = getHandle();
|
|
416
|
+
const handle = script.pushDeferred();
|
|
417
|
+
const ac = new AbortController;
|
|
418
|
+
const start = Date.now();
|
|
419
|
+
const promise = connector.send(elicitReq(fixture, "ab-mid-1"), ac.signal);
|
|
420
|
+
setTimeout(() => ac.abort(), 25);
|
|
421
|
+
let caught;
|
|
422
|
+
try {
|
|
423
|
+
await promise;
|
|
424
|
+
} catch (err) {
|
|
425
|
+
caught = err;
|
|
426
|
+
}
|
|
427
|
+
const elapsed = Date.now() - start;
|
|
428
|
+
expect(caught).toBeDefined();
|
|
429
|
+
expect(isAbortError(caught)).toBe(true);
|
|
430
|
+
expect(elapsed).toBeLessThan(fixture.abortGraceMs * 4 + 2000);
|
|
431
|
+
expect(() => handle.release({
|
|
432
|
+
requestId: "x",
|
|
433
|
+
action: "accept",
|
|
434
|
+
content: undefined,
|
|
435
|
+
done: true
|
|
436
|
+
})).not.toThrow();
|
|
437
|
+
}, opts.timeout);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/human-connector/assertions/capabilityHonesty.ts
|
|
443
|
+
import { describe as describe2, expect as expect2, it as it3 } from "vitest";
|
|
444
|
+
function buildReq(fixture, kind, requestId) {
|
|
445
|
+
const base = kind === "elicit" ? { message: "elicit", contentSchema: fixture.elicitContentSchema, contentData: undefined } : kind === "notify" ? fixture.notifyRequest : kind === "confirm" ? fixture.confirmRequest : fixture.displayRequest;
|
|
446
|
+
return {
|
|
447
|
+
requestId,
|
|
448
|
+
targetHumanId: "default",
|
|
449
|
+
kind,
|
|
450
|
+
message: base.message,
|
|
451
|
+
contentSchema: base.contentSchema,
|
|
452
|
+
contentData: base.contentData,
|
|
453
|
+
expectsResponse: kind === "elicit" || kind === "confirm",
|
|
454
|
+
mode: "single",
|
|
455
|
+
metadata: undefined
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function capabilityHonestyBlock(opts, fixture, getHandle) {
|
|
459
|
+
const expectFails = new Set(opts.expectedFailures ?? []);
|
|
460
|
+
const itFn = expectFails.has("capabilityHonesty") ? itExpectFail : it3;
|
|
461
|
+
describe2("Capability honesty", () => {
|
|
462
|
+
itFn("multiTurn:false implies followUp is undefined (not silent no-op)", () => {
|
|
463
|
+
if (opts.capabilities.multiTurn)
|
|
464
|
+
return;
|
|
465
|
+
const { connector } = getHandle();
|
|
466
|
+
expect2(connector.followUp).toBeUndefined();
|
|
467
|
+
}, opts.timeout);
|
|
468
|
+
for (const kind of ["notify", "display", "elicit", "confirm"]) {
|
|
469
|
+
itFn(`${kind}:false implies the connector either throws or surfaces a non-accept action (no silent accept)`, async () => {
|
|
470
|
+
if (opts.capabilities[kind])
|
|
471
|
+
return;
|
|
472
|
+
const { connector } = getHandle();
|
|
473
|
+
const ac = new AbortController;
|
|
474
|
+
let caught;
|
|
475
|
+
let res;
|
|
476
|
+
try {
|
|
477
|
+
res = await connector.send(buildReq(fixture, kind, `cap-${kind}`), ac.signal);
|
|
478
|
+
} catch (err) {
|
|
479
|
+
caught = err;
|
|
480
|
+
}
|
|
481
|
+
if (caught) {
|
|
482
|
+
expect2(caught).toBeDefined();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
expect2(res).toBeDefined();
|
|
486
|
+
expect2(res.action).not.toBe("accept");
|
|
487
|
+
}, opts.timeout);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/human-connector/assertions/concurrentIsolation.ts
|
|
493
|
+
import { describe as describe3, expect as expect3, it as it4 } from "vitest";
|
|
494
|
+
function elicitReq2(fixture, requestId) {
|
|
495
|
+
return {
|
|
496
|
+
requestId,
|
|
497
|
+
targetHumanId: "default",
|
|
498
|
+
kind: "elicit",
|
|
499
|
+
message: `Please confirm ${requestId}.`,
|
|
500
|
+
contentSchema: fixture.elicitContentSchema,
|
|
501
|
+
contentData: undefined,
|
|
502
|
+
expectsResponse: true,
|
|
503
|
+
mode: "single",
|
|
504
|
+
metadata: undefined
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function concurrentIsolationBlock(opts, fixture, getHandle) {
|
|
508
|
+
const expectFails = new Set(opts.expectedFailures ?? []);
|
|
509
|
+
const itConcurrent = expectFails.has("concurrent.isolation") ? itExpectFail : it4;
|
|
510
|
+
describe3.skipIf(!opts.capabilities.concurrent || !opts.capabilities.elicit)("Concurrent isolation", () => {
|
|
511
|
+
itConcurrent("two concurrent send() calls each receive their own scripted response", async () => {
|
|
512
|
+
const { connector, script } = getHandle();
|
|
513
|
+
const tagFromMessage = (message) => {
|
|
514
|
+
const m = message.match(/Please confirm (.+)\./);
|
|
515
|
+
return m ? m[1] : "unknown";
|
|
516
|
+
};
|
|
517
|
+
script.push((req) => {
|
|
518
|
+
const tag = tagFromMessage(req.message);
|
|
519
|
+
return {
|
|
520
|
+
requestId: req.requestId,
|
|
521
|
+
action: "accept",
|
|
522
|
+
content: { approved: true, reason: tag },
|
|
523
|
+
done: true
|
|
524
|
+
};
|
|
525
|
+
});
|
|
526
|
+
script.push((req) => {
|
|
527
|
+
const tag = tagFromMessage(req.message);
|
|
528
|
+
return {
|
|
529
|
+
requestId: req.requestId,
|
|
530
|
+
action: "accept",
|
|
531
|
+
content: { approved: true, reason: tag },
|
|
532
|
+
done: true
|
|
533
|
+
};
|
|
534
|
+
});
|
|
535
|
+
const ac = new AbortController;
|
|
536
|
+
const p1 = connector.send(elicitReq2(fixture, "conc-A"), ac.signal);
|
|
537
|
+
const p2 = connector.send(elicitReq2(fixture, "conc-B"), ac.signal);
|
|
538
|
+
const [a, b] = await Promise.all([p1, p2]);
|
|
539
|
+
expect3(a.requestId).toBe("conc-A");
|
|
540
|
+
expect3(a.content.reason).toBe("conc-A");
|
|
541
|
+
expect3(b.requestId).toBe("conc-B");
|
|
542
|
+
expect3(b.content.reason).toBe("conc-B");
|
|
543
|
+
}, opts.timeout);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/human-connector/assertions/multiTurnFollowUp.ts
|
|
548
|
+
import { describe as describe4, expect as expect4, it as it5 } from "vitest";
|
|
549
|
+
function elicitReq3(fixture, requestId, mode) {
|
|
550
|
+
return {
|
|
551
|
+
requestId,
|
|
552
|
+
targetHumanId: "default",
|
|
553
|
+
kind: "elicit",
|
|
554
|
+
message: "Multi-turn step.",
|
|
555
|
+
contentSchema: fixture.elicitContentSchema,
|
|
556
|
+
contentData: undefined,
|
|
557
|
+
expectsResponse: true,
|
|
558
|
+
mode,
|
|
559
|
+
metadata: undefined
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function isAbortError2(err) {
|
|
563
|
+
if (!err || typeof err !== "object")
|
|
564
|
+
return false;
|
|
565
|
+
const e = err;
|
|
566
|
+
return e.name === "AbortError" || /abort/i.test(e.message ?? "");
|
|
567
|
+
}
|
|
568
|
+
function multiTurnFollowUpBlock(opts, fixture, getHandle) {
|
|
569
|
+
const expectFails = new Set(opts.expectedFailures ?? []);
|
|
570
|
+
const itFn = expectFails.has("multiTurn.followUp") ? itExpectFail : it5;
|
|
571
|
+
describe4.skipIf(!opts.capabilities.multiTurn || !opts.capabilities.elicit)("Multi-turn followUp", () => {
|
|
572
|
+
itFn("followUp() called after done=false response returns terminal response", async () => {
|
|
573
|
+
const { connector, script } = getHandle();
|
|
574
|
+
script.push({
|
|
575
|
+
requestId: "x",
|
|
576
|
+
action: "accept",
|
|
577
|
+
content: { approved: false, reason: "step1" },
|
|
578
|
+
done: false
|
|
579
|
+
});
|
|
580
|
+
script.push({
|
|
581
|
+
requestId: "x",
|
|
582
|
+
action: "accept",
|
|
583
|
+
content: { approved: true, reason: "step2" },
|
|
584
|
+
done: true
|
|
585
|
+
});
|
|
586
|
+
const ac = new AbortController;
|
|
587
|
+
const req = elicitReq3(fixture, "mt-1", "multi-turn");
|
|
588
|
+
const first = await connector.send(req, ac.signal);
|
|
589
|
+
expect4(first.done).toBe(false);
|
|
590
|
+
expect4(typeof connector.followUp).toBe("function");
|
|
591
|
+
const second = await connector.followUp(req, first, ac.signal);
|
|
592
|
+
expect4(second.done).toBe(true);
|
|
593
|
+
expect4(second.content).toEqual({ approved: true, reason: "step2" });
|
|
594
|
+
}, opts.timeout);
|
|
595
|
+
if (opts.capabilities.abortMidElicit) {
|
|
596
|
+
itFn("followUp() honors AbortSignal mid-flight", async () => {
|
|
597
|
+
const { connector, script } = getHandle();
|
|
598
|
+
script.push({
|
|
599
|
+
requestId: "x",
|
|
600
|
+
action: "accept",
|
|
601
|
+
content: { approved: false, reason: "step1" },
|
|
602
|
+
done: false
|
|
603
|
+
});
|
|
604
|
+
script.pushDeferred();
|
|
605
|
+
const ac = new AbortController;
|
|
606
|
+
const req = elicitReq3(fixture, "mt-2", "multi-turn");
|
|
607
|
+
const first = await connector.send(req, ac.signal);
|
|
608
|
+
expect4(first.done).toBe(false);
|
|
609
|
+
const promise = connector.followUp(req, first, ac.signal);
|
|
610
|
+
setTimeout(() => ac.abort(), 25);
|
|
611
|
+
let caught;
|
|
612
|
+
try {
|
|
613
|
+
await promise;
|
|
614
|
+
} catch (err) {
|
|
615
|
+
caught = err;
|
|
616
|
+
}
|
|
617
|
+
expect4(caught).toBeDefined();
|
|
618
|
+
expect4(isAbortError2(caught)).toBe(true);
|
|
619
|
+
}, opts.timeout);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/human-connector/assertions/notifyDisplayFastResolve.ts
|
|
625
|
+
import { describe as describe5, expect as expect5, it as it6 } from "vitest";
|
|
626
|
+
function buildReq2(fixture, kind, requestId) {
|
|
627
|
+
const base = kind === "notify" ? fixture.notifyRequest : fixture.displayRequest;
|
|
628
|
+
return {
|
|
629
|
+
requestId,
|
|
630
|
+
targetHumanId: "default",
|
|
631
|
+
kind,
|
|
632
|
+
message: base.message,
|
|
633
|
+
contentSchema: base.contentSchema,
|
|
634
|
+
contentData: base.contentData,
|
|
635
|
+
expectsResponse: false,
|
|
636
|
+
mode: "single",
|
|
637
|
+
metadata: undefined
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function elicitReq4(fixture, requestId) {
|
|
641
|
+
return {
|
|
642
|
+
requestId,
|
|
643
|
+
targetHumanId: "default",
|
|
644
|
+
kind: "elicit",
|
|
645
|
+
message: "Follow-up after fast-resolve.",
|
|
646
|
+
contentSchema: fixture.elicitContentSchema,
|
|
647
|
+
contentData: undefined,
|
|
648
|
+
expectsResponse: true,
|
|
649
|
+
mode: "single",
|
|
650
|
+
metadata: undefined
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function block(kind, enabled, failId, opts, fixture, getHandle) {
|
|
654
|
+
const expectFails = new Set(opts.expectedFailures ?? []);
|
|
655
|
+
const itFn = expectFails.has(failId) ? itExpectFail : it6;
|
|
656
|
+
describe5.skipIf(!enabled || !opts.capabilities.elicit)(`${kind} fast-resolve`, () => {
|
|
657
|
+
itFn(`${kind} resolves with action=accept, done=true, and does not consume scripted elicit responses`, async () => {
|
|
658
|
+
const { connector, script } = getHandle();
|
|
659
|
+
const sentinel = { approved: true, reason: "sentinel" };
|
|
660
|
+
script.push({ requestId: "ignored", action: "accept", content: sentinel, done: true });
|
|
661
|
+
const ac = new AbortController;
|
|
662
|
+
const res = await connector.send(buildReq2(fixture, kind, `${kind}-1`), ac.signal);
|
|
663
|
+
expect5(res.action).toBe("accept");
|
|
664
|
+
expect5(res.done).toBe(true);
|
|
665
|
+
expect5(res.content).toBeUndefined();
|
|
666
|
+
const elicitRes = await connector.send(elicitReq4(fixture, `${kind}-elicit-1`), ac.signal);
|
|
667
|
+
expect5(elicitRes.action).toBe("accept");
|
|
668
|
+
expect5(elicitRes.content).toEqual(sentinel);
|
|
669
|
+
expect5(elicitRes.requestId).toBe(`${kind}-elicit-1`);
|
|
670
|
+
}, opts.timeout);
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
function notifyDisplayFastResolveBlock(opts, fixture, getHandle) {
|
|
674
|
+
block("notify", opts.capabilities.notify, "notify.fastResolve", opts, fixture, getHandle);
|
|
675
|
+
block("display", opts.capabilities.display, "display.fastResolve", opts, fixture, getHandle);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// src/human-connector/assertions/roundtrip.ts
|
|
679
|
+
import { describe as describe6, expect as expect6, it as it7 } from "vitest";
|
|
680
|
+
function elicitReq5(fixture, requestId) {
|
|
681
|
+
return {
|
|
682
|
+
requestId,
|
|
683
|
+
targetHumanId: "default",
|
|
684
|
+
kind: "elicit",
|
|
685
|
+
message: "Please confirm.",
|
|
686
|
+
contentSchema: fixture.elicitContentSchema,
|
|
687
|
+
contentData: undefined,
|
|
688
|
+
expectsResponse: true,
|
|
689
|
+
mode: "single",
|
|
690
|
+
metadata: undefined
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function confirmReq(fixture, requestId) {
|
|
694
|
+
return {
|
|
695
|
+
requestId,
|
|
696
|
+
targetHumanId: "default",
|
|
697
|
+
kind: "confirm",
|
|
698
|
+
message: fixture.confirmRequest.message,
|
|
699
|
+
contentSchema: fixture.confirmRequest.contentSchema,
|
|
700
|
+
contentData: fixture.confirmRequest.contentData,
|
|
701
|
+
expectsResponse: true,
|
|
702
|
+
mode: "single",
|
|
703
|
+
metadata: undefined
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function roundtripBlock(opts, fixture, getHandle) {
|
|
707
|
+
const expectFails = new Set(opts.expectedFailures ?? []);
|
|
708
|
+
const itAccept = expectFails.has("roundtrip.accept") ? itExpectFail : it7;
|
|
709
|
+
const itDecline = expectFails.has("roundtrip.decline") ? itExpectFail : it7;
|
|
710
|
+
const itCancel = expectFails.has("roundtrip.cancel") ? itExpectFail : it7;
|
|
711
|
+
const itConfirmAccept = expectFails.has("roundtrip.confirm.accept") ? itExpectFail : it7;
|
|
712
|
+
const itConfirmDecline = expectFails.has("roundtrip.confirm.decline") ? itExpectFail : it7;
|
|
713
|
+
const itConfirmDetails = expectFails.has("roundtrip.confirm.details") ? itExpectFail : it7;
|
|
714
|
+
const itConfirmNoContent = expectFails.has("roundtrip.confirm.noContent") ? itExpectFail : it7;
|
|
715
|
+
describe6.skipIf(!opts.capabilities.elicit)("Roundtrip elicit", () => {
|
|
716
|
+
itAccept("accept echoes requestId, returns done=true, surfaces content", async () => {
|
|
717
|
+
const { connector, script } = getHandle();
|
|
718
|
+
script.push({
|
|
719
|
+
requestId: "ignored",
|
|
720
|
+
action: "accept",
|
|
721
|
+
content: fixture.elicitAcceptContent,
|
|
722
|
+
done: true
|
|
723
|
+
});
|
|
724
|
+
const ac = new AbortController;
|
|
725
|
+
const res = await connector.send(elicitReq5(fixture, "rt-accept-1"), ac.signal);
|
|
726
|
+
expect6(res.requestId).toBe("rt-accept-1");
|
|
727
|
+
expect6(res.action).toBe("accept");
|
|
728
|
+
expect6(res.done).toBe(true);
|
|
729
|
+
expect6(res.content).toEqual(fixture.elicitAcceptContent);
|
|
730
|
+
}, opts.timeout);
|
|
731
|
+
itDecline("decline surfaces with content=undefined, no throw", async () => {
|
|
732
|
+
const { connector, script } = getHandle();
|
|
733
|
+
script.push({ requestId: "x", action: "decline", content: undefined, done: true });
|
|
734
|
+
const ac = new AbortController;
|
|
735
|
+
const res = await connector.send(elicitReq5(fixture, "rt-dec-1"), ac.signal);
|
|
736
|
+
expect6(res.requestId).toBe("rt-dec-1");
|
|
737
|
+
expect6(res.action).toBe("decline");
|
|
738
|
+
expect6(res.done).toBe(true);
|
|
739
|
+
expect6(res.content).toBeUndefined();
|
|
740
|
+
}, opts.timeout);
|
|
741
|
+
itCancel("cancel surfaces with content=undefined, no throw", async () => {
|
|
742
|
+
const { connector, script } = getHandle();
|
|
743
|
+
script.push({ requestId: "x", action: "cancel", content: undefined, done: true });
|
|
744
|
+
const ac = new AbortController;
|
|
745
|
+
const res = await connector.send(elicitReq5(fixture, "rt-can-1"), ac.signal);
|
|
746
|
+
expect6(res.requestId).toBe("rt-can-1");
|
|
747
|
+
expect6(res.action).toBe("cancel");
|
|
748
|
+
expect6(res.done).toBe(true);
|
|
749
|
+
expect6(res.content).toBeUndefined();
|
|
750
|
+
}, opts.timeout);
|
|
751
|
+
});
|
|
752
|
+
describe6.skipIf(!opts.capabilities.confirm)("Roundtrip confirm", () => {
|
|
753
|
+
itConfirmAccept("a decision reaches the caller as the person's own answer", async () => {
|
|
754
|
+
const { connector, script } = getHandle();
|
|
755
|
+
script.push({ requestId: "x", action: "accept", content: undefined, done: true });
|
|
756
|
+
const ac = new AbortController;
|
|
757
|
+
const res = await connector.send(confirmReq(fixture, "rt-confirm-accept"), ac.signal);
|
|
758
|
+
expect6(res.requestId).toBe("rt-confirm-accept");
|
|
759
|
+
expect6(res.action).toBe("accept");
|
|
760
|
+
expect6(res.done).toBe(true);
|
|
761
|
+
}, opts.timeout);
|
|
762
|
+
itConfirmDecline("a refusal is a refusal, not an unanswered accept", async () => {
|
|
763
|
+
const { connector, script } = getHandle();
|
|
764
|
+
script.push({ requestId: "x", action: "decline", content: undefined, done: true });
|
|
765
|
+
const ac = new AbortController;
|
|
766
|
+
const res = await connector.send(confirmReq(fixture, "rt-confirm-decline"), ac.signal);
|
|
767
|
+
expect6(res.requestId).toBe("rt-confirm-decline");
|
|
768
|
+
expect6(res.action).toBe("decline");
|
|
769
|
+
expect6(res.done).toBe(true);
|
|
770
|
+
}, opts.timeout);
|
|
771
|
+
itConfirmNoContent("an accepted confirm answers with the decision and nothing else", async () => {
|
|
772
|
+
const { connector, script } = getHandle();
|
|
773
|
+
script.push({
|
|
774
|
+
requestId: "x",
|
|
775
|
+
action: "accept",
|
|
776
|
+
content: { action: "something else" },
|
|
777
|
+
done: true
|
|
778
|
+
});
|
|
779
|
+
const ac = new AbortController;
|
|
780
|
+
const res = await connector.send(confirmReq(fixture, "rt-confirm-content"), ac.signal);
|
|
781
|
+
expect6(res.action).toBe("accept");
|
|
782
|
+
expect6(res.content).toBeUndefined();
|
|
783
|
+
}, opts.timeout);
|
|
784
|
+
itConfirmDetails("the action's details reach the person deciding", async () => {
|
|
785
|
+
const { connector, script } = getHandle();
|
|
786
|
+
script.push({ requestId: "x", action: "decline", content: undefined, done: true });
|
|
787
|
+
const ac = new AbortController;
|
|
788
|
+
await connector.send(confirmReq(fixture, "rt-confirm-details"), ac.signal);
|
|
789
|
+
const received = script.received.at(-1);
|
|
790
|
+
const shown = JSON.stringify({
|
|
791
|
+
message: received?.message,
|
|
792
|
+
contentData: received?.contentData
|
|
793
|
+
});
|
|
794
|
+
for (const value of Object.values(fixture.confirmRequest.contentData ?? {})) {
|
|
795
|
+
if (typeof value === "string")
|
|
796
|
+
expect6(shown).toContain(value);
|
|
797
|
+
}
|
|
798
|
+
}, opts.timeout);
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// src/human-connector/runHumanConnectorConformance.ts
|
|
803
|
+
function runHumanConnectorConformance(opts) {
|
|
804
|
+
describe7.skipIf(opts.skip)(`IHumanConnector conformance: ${opts.name}`, () => {
|
|
805
|
+
let handle;
|
|
806
|
+
const getHandle = () => {
|
|
807
|
+
if (!handle)
|
|
808
|
+
throw new Error("conformance handle not initialized");
|
|
809
|
+
return handle;
|
|
810
|
+
};
|
|
811
|
+
beforeAll(async () => {
|
|
812
|
+
handle = await opts.factory();
|
|
813
|
+
}, opts.timeout);
|
|
814
|
+
beforeEach(() => {
|
|
815
|
+
handle?.script.clear();
|
|
816
|
+
});
|
|
817
|
+
afterAll(async () => {
|
|
818
|
+
if (handle)
|
|
819
|
+
await handle.dispose();
|
|
820
|
+
});
|
|
821
|
+
const fixture = resolveHumanConformanceFixture(opts.fixture);
|
|
822
|
+
roundtripBlock(opts, fixture, getHandle);
|
|
823
|
+
abortBlock(opts, fixture, getHandle);
|
|
824
|
+
concurrentIsolationBlock(opts, fixture, getHandle);
|
|
825
|
+
notifyDisplayFastResolveBlock(opts, fixture, getHandle);
|
|
826
|
+
multiTurnFollowUpBlock(opts, fixture, getHandle);
|
|
827
|
+
capabilityHonestyBlock(opts, fixture, getHandle);
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
export {
|
|
831
|
+
DEFAULT_HUMAN_CONFORMANCE_FIXTURE,
|
|
832
|
+
MockHumanConnector,
|
|
833
|
+
abortBlock,
|
|
834
|
+
capabilityHonestyBlock,
|
|
835
|
+
concurrentIsolationBlock,
|
|
836
|
+
multiTurnFollowUpBlock,
|
|
837
|
+
notifyDisplayFastResolveBlock,
|
|
838
|
+
resolveHumanConformanceFixture,
|
|
839
|
+
roundtripBlock,
|
|
840
|
+
runHumanConnectorConformance
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
//# debugId=1244A5B943D10CD864756E2164756E21
|