@openclaw/crabline 0.1.9 → 0.1.10
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 +47 -22
- package/assets/crabline-banner.svg +20 -0
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +25 -1
- package/dist/src/cli/program.js +579 -82
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +59 -6
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +116 -92
- package/dist/src/config/schema.js +179 -73
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.js +9 -2
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/exit-codes.js.map +1 -1
- package/dist/src/core/matcher.js +7 -6
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/message-template.js.map +1 -1
- package/dist/src/core/nonces.d.ts +1 -0
- package/dist/src/core/nonces.js +4 -2
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +1 -0
- package/dist/src/core/reporters.js +42 -4
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -0
- package/dist/src/core/run.js +415 -107
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/core/safe-regex.d.ts +3 -0
- package/dist/src/core/safe-regex.js +21 -0
- package/dist/src/core/safe-regex.js.map +1 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/openclaw/artifact-generation.d.ts +38 -0
- package/dist/src/openclaw/artifact-generation.js +314 -0
- package/dist/src/openclaw/artifact-generation.js.map +1 -0
- package/dist/src/openclaw/bridges/matrix.js +97 -12
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +32 -10
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/signal.js +55 -18
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +85 -7
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +124 -30
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +78 -17
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +14 -5
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/outbound-contract.d.ts +5 -0
- package/dist/src/openclaw/outbound-contract.js +40 -0
- package/dist/src/openclaw/outbound-contract.js.map +1 -0
- package/dist/src/openclaw/private-file.d.ts +26 -0
- package/dist/src/openclaw/private-file.js +411 -0
- package/dist/src/openclaw/private-file.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +21 -2
- package/dist/src/openclaw/shared.js +115 -18
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw/smoke-lock.d.ts +63 -0
- package/dist/src/openclaw/smoke-lock.js +949 -0
- package/dist/src/openclaw/smoke-lock.js.map +1 -0
- package/dist/src/openclaw.d.ts +16 -8
- package/dist/src/openclaw.js +194 -52
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/providers/builtin/discord.d.ts +10 -1
- package/dist/src/providers/builtin/discord.js +144 -18
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +11 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +10 -0
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -0
- package/dist/src/providers/builtin/feishu.d.ts +19 -2
- package/dist/src/providers/builtin/feishu.js +307 -25
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +20 -1
- package/dist/src/providers/builtin/googlechat.js +178 -19
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.d.ts +4 -0
- package/dist/src/providers/builtin/imessage.js +26 -11
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +92 -30
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +11 -1
- package/dist/src/providers/builtin/matrix.js +53 -29
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +10 -0
- package/dist/src/providers/builtin/mattermost.js +38 -15
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +17 -1
- package/dist/src/providers/builtin/msteams.js +113 -14
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.d.ts +2 -9
- package/dist/src/providers/builtin/native-local-mock.js +12 -35
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.d.ts +11 -15
- package/dist/src/providers/builtin/script.js +566 -104
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +6 -0
- package/dist/src/providers/builtin/slack.js +99 -40
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +6 -0
- package/dist/src/providers/builtin/telegram.js +64 -43
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +28 -8
- package/dist/src/providers/builtin/whatsapp.js +567 -30
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.js +30 -19
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +3 -3
- package/dist/src/providers/catalog.js +3 -3
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +11 -2
- package/dist/src/providers/local-mock.js +322 -96
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +1 -0
- package/dist/src/providers/native-ids.js +6 -1
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +30 -2
- package/dist/src/providers/recorder.js +539 -70
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +25 -1
- package/dist/src/providers/registry.js +280 -40
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +29 -0
- package/dist/src/providers/signed-jwt.js +202 -0
- package/dist/src/providers/signed-jwt.js.map +1 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +26 -0
- package/dist/src/providers/target-normalizers.js +276 -0
- package/dist/src/providers/target-normalizers.js.map +1 -0
- package/dist/src/providers/types.d.ts +5 -0
- package/dist/src/providers/types.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +2 -0
- package/dist/src/providers/webhook-server.js +72 -25
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/http.d.ts +23 -3
- package/dist/src/servers/http.js +213 -20
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/index.js +20 -21
- package/dist/src/servers/index.js.map +1 -1
- package/dist/src/servers/matrix.js +465 -51
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +306 -58
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/pending-events.d.ts +2 -0
- package/dist/src/servers/pending-events.js +11 -0
- package/dist/src/servers/pending-events.js.map +1 -0
- package/dist/src/servers/recorder.d.ts +5 -0
- package/dist/src/servers/recorder.js +46 -3
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +2 -0
- package/dist/src/servers/signal.js +444 -56
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +5 -0
- package/dist/src/servers/slack.js +515 -49
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram.d.ts +111 -0
- package/dist/src/servers/telegram.js +811 -152
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +40 -0
- package/dist/src/servers/webhook-target.js +349 -0
- package/dist/src/servers/webhook-target.js.map +1 -0
- package/dist/src/servers/websocket.d.ts +2 -0
- package/dist/src/servers/websocket.js +29 -0
- package/dist/src/servers/websocket.js.map +1 -0
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +65 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +723 -99
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +5 -0
- package/dist/src/servers/whatsapp-jid.js +32 -0
- package/dist/src/servers/whatsapp-jid.js.map +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +5 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +87 -20
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +6 -1
- package/dist/src/servers/whatsapp-wire/crypto.js +99 -45
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.js +39 -4
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +12 -7
- package/dist/src/servers/whatsapp.js +269 -107
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +14 -0
- package/dist/src/servers/zalo.js +521 -88
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +197 -19
- package/fixtures/examples/crabline.example.yaml +10 -0
- package/package.json +20 -15
package/dist/src/core/run.js
CHANGED
|
@@ -1,43 +1,186 @@
|
|
|
1
1
|
import { matchesInbound } from "./matcher.js";
|
|
2
2
|
import { createOutboundText } from "./message-template.js";
|
|
3
3
|
import { createNonce } from "./nonces.js";
|
|
4
|
+
import { inboundRegexSafetyError } from "./safe-regex.js";
|
|
4
5
|
import { CrablineError, ensureErrorMessage } from "./errors.js";
|
|
6
|
+
import { EXIT_CODES } from "./exit-codes.js";
|
|
7
|
+
const INBOUND_DEADLINE_REACHED = Symbol("inbound deadline reached");
|
|
8
|
+
const INBOUND_ABORT_GRACE_MS = 250;
|
|
9
|
+
const MAX_EXCLUDED_INBOUND_IDS = 1_024;
|
|
10
|
+
const MAX_SCRIPT_STDIN_BYTES = 1024 * 1024;
|
|
11
|
+
class UnsettledProviderOperationError extends CrablineError {
|
|
12
|
+
}
|
|
5
13
|
function sleep(ms) {
|
|
6
14
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
15
|
}
|
|
16
|
+
async function raceInboundDeadline(operation, timeoutMs) {
|
|
17
|
+
let timer;
|
|
18
|
+
const deadline = new Promise((resolve) => {
|
|
19
|
+
timer = setTimeout(() => resolve(INBOUND_DEADLINE_REACHED), timeoutMs);
|
|
20
|
+
});
|
|
21
|
+
try {
|
|
22
|
+
return await Promise.race([operation, deadline]);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
if (timer) {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function withFixtureDeadline(operation, timeoutMs, operationName) {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const pending = Promise.resolve().then(async () => await operation(controller.signal));
|
|
33
|
+
const result = await raceInboundDeadline(pending, timeoutMs);
|
|
34
|
+
if (result === INBOUND_DEADLINE_REACHED) {
|
|
35
|
+
const timeoutError = new CrablineError(`Provider ${operationName} timed out after ${timeoutMs}ms.`, {
|
|
36
|
+
kind: "timeout",
|
|
37
|
+
});
|
|
38
|
+
controller.abort(timeoutError);
|
|
39
|
+
const settled = pending.then(() => true, () => true);
|
|
40
|
+
if ((await raceInboundDeadline(settled, INBOUND_ABORT_GRACE_MS)) === INBOUND_DEADLINE_REACHED) {
|
|
41
|
+
throw new UnsettledProviderOperationError(`Provider ${operationName} did not settle within ${INBOUND_ABORT_GRACE_MS}ms after abort.`, {
|
|
42
|
+
cause: timeoutError,
|
|
43
|
+
kind: "timeout",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
throw timeoutError;
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
async function withCleanupDeadline(operation, timeoutMs, operationName) {
|
|
51
|
+
const result = await raceInboundDeadline(operation, timeoutMs);
|
|
52
|
+
if (result === INBOUND_DEADLINE_REACHED) {
|
|
53
|
+
throw new CrablineError(`Provider ${operationName} timed out after ${timeoutMs}ms.`, {
|
|
54
|
+
kind: "timeout",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
function jsonStringByteLength(value, limit) {
|
|
60
|
+
let bytes = 2;
|
|
61
|
+
for (let index = 0; index < value.length && bytes <= limit; index += 1) {
|
|
62
|
+
const code = value.charCodeAt(index);
|
|
63
|
+
if (code === 0x22 || code === 0x5c || code === 0x08 || code === 0x09 || code === 0x0a) {
|
|
64
|
+
bytes += 2;
|
|
65
|
+
}
|
|
66
|
+
else if (code === 0x0c || code === 0x0d) {
|
|
67
|
+
bytes += 2;
|
|
68
|
+
}
|
|
69
|
+
else if (code <= 0x1f) {
|
|
70
|
+
bytes += 6;
|
|
71
|
+
}
|
|
72
|
+
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
73
|
+
const next = value.charCodeAt(index + 1);
|
|
74
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
75
|
+
bytes += 4;
|
|
76
|
+
index += 1;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
bytes += 6;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
83
|
+
bytes += 6;
|
|
84
|
+
}
|
|
85
|
+
else if (code <= 0x7f) {
|
|
86
|
+
bytes += 1;
|
|
87
|
+
}
|
|
88
|
+
else if (code <= 0x7ff) {
|
|
89
|
+
bytes += 2;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
bytes += 3;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return bytes;
|
|
96
|
+
}
|
|
97
|
+
function boundedJsonByteLength(value, limit, ancestors = new Set(), arrayEntry = false) {
|
|
98
|
+
if (value === null || value === undefined || typeof value === "function") {
|
|
99
|
+
return value === undefined && !arrayEntry ? 0 : 4;
|
|
100
|
+
}
|
|
101
|
+
if (typeof value === "string") {
|
|
102
|
+
return jsonStringByteLength(value, limit);
|
|
103
|
+
}
|
|
104
|
+
if (typeof value === "number") {
|
|
105
|
+
return Number.isFinite(value) ? String(value).length : 4;
|
|
106
|
+
}
|
|
107
|
+
if (typeof value === "boolean") {
|
|
108
|
+
return value ? 4 : 5;
|
|
109
|
+
}
|
|
110
|
+
if (typeof value !== "object") {
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
if (ancestors.has(value)) {
|
|
114
|
+
throw new CrablineError("Script command input must not contain circular values.", {
|
|
115
|
+
kind: "config",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
ancestors.add(value);
|
|
119
|
+
let bytes = 2;
|
|
120
|
+
let entries = 0;
|
|
121
|
+
if (Array.isArray(value)) {
|
|
122
|
+
for (const entry of value) {
|
|
123
|
+
if (entries > 0) {
|
|
124
|
+
bytes += 1;
|
|
125
|
+
}
|
|
126
|
+
bytes += boundedJsonByteLength(entry, limit - bytes, ancestors, true);
|
|
127
|
+
entries += 1;
|
|
128
|
+
if (bytes > limit) {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
135
|
+
if (entry === undefined || typeof entry === "function") {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (entries > 0) {
|
|
139
|
+
bytes += 1;
|
|
140
|
+
}
|
|
141
|
+
bytes += jsonStringByteLength(key, limit - bytes) + 1;
|
|
142
|
+
bytes += boundedJsonByteLength(entry, limit - bytes, ancestors);
|
|
143
|
+
entries += 1;
|
|
144
|
+
if (bytes > limit) {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
ancestors.delete(value);
|
|
150
|
+
return bytes;
|
|
151
|
+
}
|
|
152
|
+
export function assertScriptStdinPayloadSize(payload) {
|
|
153
|
+
if (boundedJsonByteLength(payload, MAX_SCRIPT_STDIN_BYTES) > MAX_SCRIPT_STDIN_BYTES) {
|
|
154
|
+
throw new CrablineError(`Script command input exceeded ${MAX_SCRIPT_STDIN_BYTES} bytes.`, {
|
|
155
|
+
kind: "config",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function scriptPayloadBase(context) {
|
|
160
|
+
return {
|
|
161
|
+
fixture: context.fixture,
|
|
162
|
+
provider: {
|
|
163
|
+
config: context.config,
|
|
164
|
+
id: context.providerId,
|
|
165
|
+
manifestPath: context.manifestPath,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async function abortAndDrainInboundWait(operation, controller) {
|
|
170
|
+
controller.abort(new Error("Inbound wait deadline reached."));
|
|
171
|
+
const settled = operation.then(() => true, () => true);
|
|
172
|
+
return (await raceInboundDeadline(settled, INBOUND_ABORT_GRACE_MS)) !== INBOUND_DEADLINE_REACHED;
|
|
173
|
+
}
|
|
8
174
|
export async function runFixtureCommand(params) {
|
|
9
|
-
const
|
|
10
|
-
if (!
|
|
175
|
+
const configuredFixture = params.manifest.fixtures.find((entry) => entry.id === params.fixtureId);
|
|
176
|
+
if (!configuredFixture) {
|
|
11
177
|
throw new CrablineError(`Unknown fixture: ${params.fixtureId}`, { kind: "config" });
|
|
12
178
|
}
|
|
179
|
+
const mode = params.modeOverride ?? configuredFixture.mode;
|
|
180
|
+
const fixture = mode === configuredFixture.mode ? configuredFixture : { ...configuredFixture, mode };
|
|
13
181
|
const provider = params.registry.resolve(fixture.provider, fixture.id);
|
|
14
|
-
const mode = params.modeOverride ?? fixture.mode;
|
|
15
182
|
const diagnostics = [];
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
diagnostics: [`provider ${fixture.provider} does not support mode ${mode}`],
|
|
19
|
-
failureKind: "config",
|
|
20
|
-
fixtureId: fixture.id,
|
|
21
|
-
mode,
|
|
22
|
-
ok: false,
|
|
23
|
-
providerId: fixture.provider,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
for (const envName of [
|
|
27
|
-
...fixture.env,
|
|
28
|
-
...(params.manifest.providers[fixture.provider]?.env ?? []),
|
|
29
|
-
]) {
|
|
30
|
-
if (!process.env[envName]) {
|
|
31
|
-
return {
|
|
32
|
-
diagnostics: [`missing env: ${envName}`],
|
|
33
|
-
failureKind: "config",
|
|
34
|
-
fixtureId: fixture.id,
|
|
35
|
-
mode,
|
|
36
|
-
ok: false,
|
|
37
|
-
providerId: fixture.provider,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
}
|
|
183
|
+
let abortDrainFailed = false;
|
|
41
184
|
const contextBase = {
|
|
42
185
|
config: params.manifest.providers[fixture.provider],
|
|
43
186
|
fixture,
|
|
@@ -47,9 +190,43 @@ export async function runFixtureCommand(params) {
|
|
|
47
190
|
};
|
|
48
191
|
let result;
|
|
49
192
|
try {
|
|
50
|
-
if (mode
|
|
193
|
+
if (!provider.supports.includes(mode)) {
|
|
194
|
+
result = {
|
|
195
|
+
diagnostics: [`provider ${fixture.provider} does not support mode ${mode}`],
|
|
196
|
+
failureKind: "config",
|
|
197
|
+
fixtureId: fixture.id,
|
|
198
|
+
mode,
|
|
199
|
+
ok: false,
|
|
200
|
+
providerId: fixture.provider,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (!result) {
|
|
204
|
+
for (const envName of [
|
|
205
|
+
...fixture.env,
|
|
206
|
+
...(params.manifest.providers[fixture.provider]?.env ?? []),
|
|
207
|
+
]) {
|
|
208
|
+
if (!process.env[envName]) {
|
|
209
|
+
result = {
|
|
210
|
+
diagnostics: [`missing env: ${envName}`],
|
|
211
|
+
failureKind: "config",
|
|
212
|
+
fixtureId: fixture.id,
|
|
213
|
+
mode,
|
|
214
|
+
ok: false,
|
|
215
|
+
providerId: fixture.provider,
|
|
216
|
+
};
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (result) {
|
|
222
|
+
// Preflight failures still flow through provider cleanup before returning.
|
|
223
|
+
}
|
|
224
|
+
else if (mode === "probe") {
|
|
51
225
|
try {
|
|
52
|
-
|
|
226
|
+
if (contextBase.config.adapter === "script") {
|
|
227
|
+
assertScriptStdinPayloadSize(scriptPayloadBase(contextBase));
|
|
228
|
+
}
|
|
229
|
+
const probeResult = await withFixtureDeadline(async (signal) => await provider.probe({ ...contextBase, signal }), fixture.timeoutMs, "probe");
|
|
53
230
|
return (result = {
|
|
54
231
|
diagnostics: probeResult.details,
|
|
55
232
|
failureKind: probeResult.healthy ? undefined : "connectivity",
|
|
@@ -60,28 +237,171 @@ export async function runFixtureCommand(params) {
|
|
|
60
237
|
});
|
|
61
238
|
}
|
|
62
239
|
catch (error) {
|
|
63
|
-
|
|
240
|
+
if (error instanceof UnsettledProviderOperationError) {
|
|
241
|
+
abortDrainFailed = true;
|
|
242
|
+
}
|
|
243
|
+
return (result = toFailure(fixture.id, fixture.provider, mode, error, "connectivity"));
|
|
64
244
|
}
|
|
65
245
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
246
|
+
else {
|
|
247
|
+
if (fixture.inboundMatch.strategy === "regex" && fixture.inboundMatch.pattern) {
|
|
248
|
+
try {
|
|
249
|
+
RegExp(fixture.inboundMatch.pattern, "u");
|
|
250
|
+
const safetyError = inboundRegexSafetyError(fixture.inboundMatch.pattern);
|
|
251
|
+
if (safetyError) {
|
|
252
|
+
throw new Error(safetyError);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
return (result = toFailure(fixture.id, fixture.provider, mode, new CrablineError(`Invalid inbound regex: ${ensureErrorMessage(error)}`, {
|
|
257
|
+
cause: error,
|
|
258
|
+
kind: "config",
|
|
259
|
+
}), "config"));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
let attempts = 0;
|
|
263
|
+
const maxAttempts = fixture.retries + 1;
|
|
264
|
+
let lastFailure = null;
|
|
265
|
+
while (attempts < maxAttempts) {
|
|
266
|
+
attempts += 1;
|
|
267
|
+
const nonce = createNonce(fixture.id);
|
|
268
|
+
try {
|
|
269
|
+
const outboundText = createOutboundText({ ...fixture, mode }, nonce);
|
|
270
|
+
const since = new Date().toISOString();
|
|
271
|
+
let accepted;
|
|
272
|
+
try {
|
|
273
|
+
const sendContext = {
|
|
274
|
+
...contextBase,
|
|
275
|
+
mode,
|
|
276
|
+
nonce,
|
|
277
|
+
text: outboundText,
|
|
278
|
+
};
|
|
279
|
+
if (contextBase.config.adapter === "script") {
|
|
280
|
+
assertScriptStdinPayloadSize({
|
|
281
|
+
...scriptPayloadBase(contextBase),
|
|
282
|
+
outbound: {
|
|
283
|
+
mode,
|
|
284
|
+
nonce,
|
|
285
|
+
target: fixture.target,
|
|
286
|
+
text: outboundText,
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
accepted = await withFixtureDeadline(async (signal) => await provider.send({ ...sendContext, signal }), fixture.timeoutMs, "send");
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
lastFailure = toFailure(fixture.id, fixture.provider, mode, error, "outbound", nonce);
|
|
294
|
+
if (error instanceof UnsettledProviderOperationError) {
|
|
295
|
+
abortDrainFailed = true;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (!accepted.accepted) {
|
|
301
|
+
lastFailure = toFailure(fixture.id, fixture.provider, mode, new CrablineError(`Provider rejected outbound message ${accepted.messageId}.`, {
|
|
302
|
+
kind: "outbound",
|
|
303
|
+
}), "outbound", nonce);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
diagnostics.push(`accepted message ${accepted.messageId}`);
|
|
307
|
+
if (mode === "send") {
|
|
308
|
+
return (result = {
|
|
309
|
+
diagnostics,
|
|
310
|
+
fixtureId: fixture.id,
|
|
311
|
+
mode,
|
|
312
|
+
nonce,
|
|
313
|
+
ok: true,
|
|
314
|
+
providerId: fixture.provider,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
const inboundDeadline = Date.now() + fixture.timeoutMs;
|
|
318
|
+
const seenInbound = new Set();
|
|
319
|
+
const excludedInboundIds = new Set();
|
|
320
|
+
let inbound;
|
|
321
|
+
try {
|
|
322
|
+
while (Date.now() < inboundDeadline) {
|
|
323
|
+
const timeoutMs = inboundDeadline - Date.now();
|
|
324
|
+
const controller = new AbortController();
|
|
325
|
+
const excludeIds = [...excludedInboundIds];
|
|
326
|
+
const waitContext = {
|
|
327
|
+
...contextBase,
|
|
328
|
+
excludeIds,
|
|
329
|
+
nonce,
|
|
330
|
+
signal: controller.signal,
|
|
331
|
+
since,
|
|
332
|
+
threadId: accepted.threadId,
|
|
333
|
+
timeoutMs,
|
|
334
|
+
};
|
|
335
|
+
if (contextBase.config.adapter === "script") {
|
|
336
|
+
assertScriptStdinPayloadSize({
|
|
337
|
+
...scriptPayloadBase(contextBase),
|
|
338
|
+
wait: {
|
|
339
|
+
excludeIds,
|
|
340
|
+
nonce,
|
|
341
|
+
since,
|
|
342
|
+
target: fixture.target,
|
|
343
|
+
threadId: accepted.threadId,
|
|
344
|
+
timeoutMs,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
const wait = provider.waitForInbound(waitContext);
|
|
349
|
+
const candidate = await raceInboundDeadline(wait, timeoutMs);
|
|
350
|
+
if (candidate === INBOUND_DEADLINE_REACHED) {
|
|
351
|
+
if (!(await abortAndDrainInboundWait(wait, controller))) {
|
|
352
|
+
abortDrainFailed = true;
|
|
353
|
+
throw new UnsettledProviderOperationError(`Provider inbound wait did not settle within ${INBOUND_ABORT_GRACE_MS}ms after abort.`, { kind: "inbound" });
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
if (!candidate) {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
const key = JSON.stringify([candidate.provider, candidate.threadId, candidate.id]);
|
|
361
|
+
if (seenInbound.has(key)) {
|
|
362
|
+
await sleep(Math.min(10, Math.max(0, inboundDeadline - Date.now())));
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
seenInbound.add(key);
|
|
366
|
+
if (matchesInbound(candidate, fixture.inboundMatch, nonce)) {
|
|
367
|
+
inbound = candidate;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
if (!excludedInboundIds.has(candidate.id)) {
|
|
371
|
+
if (excludedInboundIds.size >= MAX_EXCLUDED_INBOUND_IDS) {
|
|
372
|
+
throw new CrablineError(`Provider returned more than ${MAX_EXCLUDED_INBOUND_IDS} unmatched inbound message IDs.`, { kind: "inbound" });
|
|
373
|
+
}
|
|
374
|
+
excludedInboundIds.add(candidate.id);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
lastFailure = toFailure(fixture.id, fixture.provider, mode, error, "inbound", nonce);
|
|
380
|
+
if (abortDrainFailed) {
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (!inbound) {
|
|
386
|
+
lastFailure = {
|
|
387
|
+
diagnostics: [
|
|
388
|
+
...diagnostics,
|
|
389
|
+
`timed out waiting for inbound after ${fixture.timeoutMs}ms`,
|
|
390
|
+
],
|
|
391
|
+
failureKind: "timeout",
|
|
392
|
+
fixtureId: fixture.id,
|
|
393
|
+
mode,
|
|
394
|
+
nonce,
|
|
395
|
+
ok: false,
|
|
396
|
+
providerId: fixture.provider,
|
|
397
|
+
};
|
|
398
|
+
if (attempts < maxAttempts) {
|
|
399
|
+
await sleep(50);
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
83
403
|
return (result = {
|
|
84
|
-
diagnostics,
|
|
404
|
+
diagnostics: [...diagnostics, `matched inbound ${inbound.id}`],
|
|
85
405
|
fixtureId: fixture.id,
|
|
86
406
|
mode,
|
|
87
407
|
nonce,
|
|
@@ -89,76 +409,60 @@ export async function runFixtureCommand(params) {
|
|
|
89
409
|
providerId: fixture.provider,
|
|
90
410
|
});
|
|
91
411
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
nonce,
|
|
95
|
-
since,
|
|
96
|
-
threadId: accepted.threadId,
|
|
97
|
-
timeoutMs: fixture.timeoutMs,
|
|
98
|
-
});
|
|
99
|
-
if (!inbound) {
|
|
100
|
-
lastFailure = {
|
|
101
|
-
diagnostics: [
|
|
102
|
-
...diagnostics,
|
|
103
|
-
`timed out waiting for inbound after ${fixture.timeoutMs}ms`,
|
|
104
|
-
],
|
|
105
|
-
failureKind: "timeout",
|
|
106
|
-
fixtureId: fixture.id,
|
|
107
|
-
mode,
|
|
108
|
-
nonce,
|
|
109
|
-
ok: false,
|
|
110
|
-
providerId: fixture.provider,
|
|
111
|
-
};
|
|
112
|
-
await sleep(50);
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (!matchesInbound(inbound, fixture.inboundMatch, nonce)) {
|
|
116
|
-
lastFailure = {
|
|
117
|
-
diagnostics: [...diagnostics, `received non-matching inbound message ${inbound.id}`],
|
|
118
|
-
failureKind: "assertion",
|
|
119
|
-
fixtureId: fixture.id,
|
|
120
|
-
mode,
|
|
121
|
-
nonce,
|
|
122
|
-
ok: false,
|
|
123
|
-
providerId: fixture.provider,
|
|
124
|
-
};
|
|
125
|
-
await sleep(50);
|
|
126
|
-
continue;
|
|
412
|
+
catch (error) {
|
|
413
|
+
lastFailure = toFailure(fixture.id, fixture.provider, mode, error, "assertion", nonce);
|
|
127
414
|
}
|
|
128
|
-
return (result = {
|
|
129
|
-
diagnostics: [...diagnostics, `matched inbound ${inbound.id}`],
|
|
130
|
-
fixtureId: fixture.id,
|
|
131
|
-
mode,
|
|
132
|
-
nonce,
|
|
133
|
-
ok: true,
|
|
134
|
-
providerId: fixture.provider,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
catch (error) {
|
|
138
|
-
lastFailure = toFailure(fixture.id, fixture.provider, mode, error, nonce);
|
|
139
415
|
}
|
|
416
|
+
return (result = lastFailure ?? {
|
|
417
|
+
diagnostics: ["unknown failure"],
|
|
418
|
+
failureKind: "assertion",
|
|
419
|
+
fixtureId: fixture.id,
|
|
420
|
+
mode,
|
|
421
|
+
ok: false,
|
|
422
|
+
providerId: fixture.provider,
|
|
423
|
+
});
|
|
140
424
|
}
|
|
141
|
-
return (result = lastFailure ?? {
|
|
142
|
-
diagnostics: ["unknown failure"],
|
|
143
|
-
failureKind: "assertion",
|
|
144
|
-
fixtureId: fixture.id,
|
|
145
|
-
mode,
|
|
146
|
-
ok: false,
|
|
147
|
-
providerId: fixture.provider,
|
|
148
|
-
});
|
|
149
425
|
}
|
|
150
426
|
finally {
|
|
427
|
+
const cleanupErrors = [];
|
|
428
|
+
try {
|
|
429
|
+
provider.beginCleanup?.();
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
cleanupErrors.push(error);
|
|
433
|
+
}
|
|
151
434
|
try {
|
|
152
|
-
|
|
435
|
+
const cleanup = provider.cleanup?.();
|
|
436
|
+
if (cleanup) {
|
|
437
|
+
if (abortDrainFailed &&
|
|
438
|
+
(await raceInboundDeadline(cleanup, INBOUND_ABORT_GRACE_MS)) === INBOUND_DEADLINE_REACHED) {
|
|
439
|
+
cleanupErrors.push(new Error(`Provider cleanup did not settle within ${INBOUND_ABORT_GRACE_MS}ms after an aborted operation.`));
|
|
440
|
+
}
|
|
441
|
+
if (!abortDrainFailed) {
|
|
442
|
+
await withCleanupDeadline(cleanup, fixture.timeoutMs, "cleanup");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
153
445
|
}
|
|
154
446
|
catch (error) {
|
|
155
|
-
|
|
447
|
+
cleanupErrors.push(error);
|
|
448
|
+
}
|
|
449
|
+
if (cleanupErrors.length > 0) {
|
|
450
|
+
const cleanupError = cleanupErrors.length === 1
|
|
451
|
+
? cleanupErrors[0]
|
|
452
|
+
: new AggregateError(cleanupErrors, cleanupErrors.map((error) => ensureErrorMessage(error)).join("; "));
|
|
453
|
+
const diagnostic = `cleanup failed: ${ensureErrorMessage(cleanupError)}`;
|
|
156
454
|
if (result) {
|
|
157
455
|
result.diagnostics.push(diagnostic);
|
|
456
|
+
if (result.ok) {
|
|
457
|
+
result.exitCode = EXIT_CODES.ASSERTION;
|
|
458
|
+
result.failureKind = "assertion";
|
|
459
|
+
result.ok = false;
|
|
460
|
+
}
|
|
158
461
|
}
|
|
159
462
|
else {
|
|
160
463
|
result = {
|
|
161
464
|
diagnostics: [...diagnostics, diagnostic],
|
|
465
|
+
exitCode: EXIT_CODES.ASSERTION,
|
|
162
466
|
failureKind: "assertion",
|
|
163
467
|
fixtureId: fixture.id,
|
|
164
468
|
mode,
|
|
@@ -195,6 +499,9 @@ export function computeExitCode(result) {
|
|
|
195
499
|
if (result.ok) {
|
|
196
500
|
return 0;
|
|
197
501
|
}
|
|
502
|
+
if (result.exitCode !== undefined) {
|
|
503
|
+
return result.exitCode;
|
|
504
|
+
}
|
|
198
505
|
switch (result.failureKind) {
|
|
199
506
|
case "auth":
|
|
200
507
|
return 11;
|
|
@@ -214,11 +521,12 @@ export function computeExitCode(result) {
|
|
|
214
521
|
return 1;
|
|
215
522
|
}
|
|
216
523
|
}
|
|
217
|
-
function toFailure(fixtureId, providerId, mode, error, nonce) {
|
|
524
|
+
function toFailure(fixtureId, providerId, mode, error, fallbackKind, nonce) {
|
|
218
525
|
const diagnostics = [ensureErrorMessage(error)];
|
|
219
526
|
if (error instanceof CrablineError) {
|
|
220
527
|
return {
|
|
221
528
|
diagnostics,
|
|
529
|
+
exitCode: error.exitCode,
|
|
222
530
|
failureKind: error.kind,
|
|
223
531
|
fixtureId,
|
|
224
532
|
mode,
|
|
@@ -229,7 +537,7 @@ function toFailure(fixtureId, providerId, mode, error, nonce) {
|
|
|
229
537
|
}
|
|
230
538
|
return {
|
|
231
539
|
diagnostics,
|
|
232
|
-
failureKind:
|
|
540
|
+
failureKind: fallbackKind,
|
|
233
541
|
fixtureId,
|
|
234
542
|
mode,
|
|
235
543
|
nonce,
|