@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.
Files changed (189) hide show
  1. package/README.md +47 -22
  2. package/assets/crabline-banner.svg +20 -0
  3. package/dist/src/bin/crabline.js.map +1 -1
  4. package/dist/src/cli/program.d.ts +25 -1
  5. package/dist/src/cli/program.js +579 -82
  6. package/dist/src/cli/program.js.map +1 -1
  7. package/dist/src/config/load.js +59 -6
  8. package/dist/src/config/load.js.map +1 -1
  9. package/dist/src/config/schema.d.ts +116 -92
  10. package/dist/src/config/schema.js +179 -73
  11. package/dist/src/config/schema.js.map +1 -1
  12. package/dist/src/core/errors.js +9 -2
  13. package/dist/src/core/errors.js.map +1 -1
  14. package/dist/src/core/exit-codes.js.map +1 -1
  15. package/dist/src/core/matcher.js +7 -6
  16. package/dist/src/core/matcher.js.map +1 -1
  17. package/dist/src/core/message-template.js.map +1 -1
  18. package/dist/src/core/nonces.d.ts +1 -0
  19. package/dist/src/core/nonces.js +4 -2
  20. package/dist/src/core/nonces.js.map +1 -1
  21. package/dist/src/core/reporters.d.ts +1 -0
  22. package/dist/src/core/reporters.js +42 -4
  23. package/dist/src/core/reporters.js.map +1 -1
  24. package/dist/src/core/run.d.ts +3 -0
  25. package/dist/src/core/run.js +415 -107
  26. package/dist/src/core/run.js.map +1 -1
  27. package/dist/src/core/safe-regex.d.ts +3 -0
  28. package/dist/src/core/safe-regex.js +21 -0
  29. package/dist/src/core/safe-regex.js.map +1 -0
  30. package/dist/src/index.d.ts +2 -2
  31. package/dist/src/index.js +1 -1
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/openclaw/artifact-generation.d.ts +38 -0
  34. package/dist/src/openclaw/artifact-generation.js +314 -0
  35. package/dist/src/openclaw/artifact-generation.js.map +1 -0
  36. package/dist/src/openclaw/bridges/matrix.js +97 -12
  37. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  38. package/dist/src/openclaw/bridges/mattermost.js +32 -10
  39. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  40. package/dist/src/openclaw/bridges/signal.js +55 -18
  41. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  42. package/dist/src/openclaw/bridges/slack.js +85 -7
  43. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  44. package/dist/src/openclaw/bridges/telegram.js +124 -30
  45. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  46. package/dist/src/openclaw/bridges/whatsapp.js +78 -17
  47. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  48. package/dist/src/openclaw/bridges/zalo.js +14 -5
  49. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  50. package/dist/src/openclaw/outbound-contract.d.ts +5 -0
  51. package/dist/src/openclaw/outbound-contract.js +40 -0
  52. package/dist/src/openclaw/outbound-contract.js.map +1 -0
  53. package/dist/src/openclaw/private-file.d.ts +26 -0
  54. package/dist/src/openclaw/private-file.js +411 -0
  55. package/dist/src/openclaw/private-file.js.map +1 -0
  56. package/dist/src/openclaw/shared.d.ts +21 -2
  57. package/dist/src/openclaw/shared.js +115 -18
  58. package/dist/src/openclaw/shared.js.map +1 -1
  59. package/dist/src/openclaw/smoke-lock.d.ts +63 -0
  60. package/dist/src/openclaw/smoke-lock.js +949 -0
  61. package/dist/src/openclaw/smoke-lock.js.map +1 -0
  62. package/dist/src/openclaw.d.ts +16 -8
  63. package/dist/src/openclaw.js +194 -52
  64. package/dist/src/openclaw.js.map +1 -1
  65. package/dist/src/providers/builtin/discord.d.ts +10 -1
  66. package/dist/src/providers/builtin/discord.js +144 -18
  67. package/dist/src/providers/builtin/discord.js.map +1 -1
  68. package/dist/src/providers/builtin/external-webhook-auth.d.ts +11 -0
  69. package/dist/src/providers/builtin/external-webhook-auth.js +10 -0
  70. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -0
  71. package/dist/src/providers/builtin/feishu.d.ts +19 -2
  72. package/dist/src/providers/builtin/feishu.js +307 -25
  73. package/dist/src/providers/builtin/feishu.js.map +1 -1
  74. package/dist/src/providers/builtin/googlechat.d.ts +20 -1
  75. package/dist/src/providers/builtin/googlechat.js +178 -19
  76. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  77. package/dist/src/providers/builtin/imessage.d.ts +4 -0
  78. package/dist/src/providers/builtin/imessage.js +26 -11
  79. package/dist/src/providers/builtin/imessage.js.map +1 -1
  80. package/dist/src/providers/builtin/loopback.js +92 -30
  81. package/dist/src/providers/builtin/loopback.js.map +1 -1
  82. package/dist/src/providers/builtin/matrix.d.ts +11 -1
  83. package/dist/src/providers/builtin/matrix.js +53 -29
  84. package/dist/src/providers/builtin/matrix.js.map +1 -1
  85. package/dist/src/providers/builtin/mattermost.d.ts +10 -0
  86. package/dist/src/providers/builtin/mattermost.js +38 -15
  87. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  88. package/dist/src/providers/builtin/msteams.d.ts +17 -1
  89. package/dist/src/providers/builtin/msteams.js +113 -14
  90. package/dist/src/providers/builtin/msteams.js.map +1 -1
  91. package/dist/src/providers/builtin/native-local-mock.d.ts +2 -9
  92. package/dist/src/providers/builtin/native-local-mock.js +12 -35
  93. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  94. package/dist/src/providers/builtin/script.d.ts +11 -15
  95. package/dist/src/providers/builtin/script.js +566 -104
  96. package/dist/src/providers/builtin/script.js.map +1 -1
  97. package/dist/src/providers/builtin/slack.d.ts +6 -0
  98. package/dist/src/providers/builtin/slack.js +99 -40
  99. package/dist/src/providers/builtin/slack.js.map +1 -1
  100. package/dist/src/providers/builtin/telegram.d.ts +6 -0
  101. package/dist/src/providers/builtin/telegram.js +64 -43
  102. package/dist/src/providers/builtin/telegram.js.map +1 -1
  103. package/dist/src/providers/builtin/whatsapp.d.ts +28 -8
  104. package/dist/src/providers/builtin/whatsapp.js +567 -30
  105. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  106. package/dist/src/providers/builtin/zalo.js +30 -19
  107. package/dist/src/providers/builtin/zalo.js.map +1 -1
  108. package/dist/src/providers/catalog.d.ts +3 -3
  109. package/dist/src/providers/catalog.js +3 -3
  110. package/dist/src/providers/catalog.js.map +1 -1
  111. package/dist/src/providers/local-mock.d.ts +11 -2
  112. package/dist/src/providers/local-mock.js +322 -96
  113. package/dist/src/providers/local-mock.js.map +1 -1
  114. package/dist/src/providers/native-ids.d.ts +1 -0
  115. package/dist/src/providers/native-ids.js +6 -1
  116. package/dist/src/providers/native-ids.js.map +1 -1
  117. package/dist/src/providers/recorder.d.ts +30 -2
  118. package/dist/src/providers/recorder.js +539 -70
  119. package/dist/src/providers/recorder.js.map +1 -1
  120. package/dist/src/providers/registry.d.ts +25 -1
  121. package/dist/src/providers/registry.js +280 -40
  122. package/dist/src/providers/registry.js.map +1 -1
  123. package/dist/src/providers/signed-jwt.d.ts +29 -0
  124. package/dist/src/providers/signed-jwt.js +202 -0
  125. package/dist/src/providers/signed-jwt.js.map +1 -0
  126. package/dist/src/providers/slack-ids.js.map +1 -1
  127. package/dist/src/providers/target-normalizers.d.ts +26 -0
  128. package/dist/src/providers/target-normalizers.js +276 -0
  129. package/dist/src/providers/target-normalizers.js.map +1 -0
  130. package/dist/src/providers/types.d.ts +5 -0
  131. package/dist/src/providers/types.js.map +1 -1
  132. package/dist/src/providers/webhook-server.d.ts +2 -0
  133. package/dist/src/providers/webhook-server.js +72 -25
  134. package/dist/src/providers/webhook-server.js.map +1 -1
  135. package/dist/src/servers/http.d.ts +23 -3
  136. package/dist/src/servers/http.js +213 -20
  137. package/dist/src/servers/http.js.map +1 -1
  138. package/dist/src/servers/index.js +20 -21
  139. package/dist/src/servers/index.js.map +1 -1
  140. package/dist/src/servers/matrix.js +465 -51
  141. package/dist/src/servers/matrix.js.map +1 -1
  142. package/dist/src/servers/mattermost.d.ts +5 -0
  143. package/dist/src/servers/mattermost.js +306 -58
  144. package/dist/src/servers/mattermost.js.map +1 -1
  145. package/dist/src/servers/pending-events.d.ts +2 -0
  146. package/dist/src/servers/pending-events.js +11 -0
  147. package/dist/src/servers/pending-events.js.map +1 -0
  148. package/dist/src/servers/recorder.d.ts +5 -0
  149. package/dist/src/servers/recorder.js +46 -3
  150. package/dist/src/servers/recorder.js.map +1 -1
  151. package/dist/src/servers/signal.d.ts +2 -0
  152. package/dist/src/servers/signal.js +444 -56
  153. package/dist/src/servers/signal.js.map +1 -1
  154. package/dist/src/servers/slack.d.ts +5 -0
  155. package/dist/src/servers/slack.js +515 -49
  156. package/dist/src/servers/slack.js.map +1 -1
  157. package/dist/src/servers/telegram.d.ts +111 -0
  158. package/dist/src/servers/telegram.js +811 -152
  159. package/dist/src/servers/telegram.js.map +1 -1
  160. package/dist/src/servers/webhook-target.d.ts +40 -0
  161. package/dist/src/servers/webhook-target.js +349 -0
  162. package/dist/src/servers/webhook-target.js.map +1 -0
  163. package/dist/src/servers/websocket.d.ts +2 -0
  164. package/dist/src/servers/websocket.js +29 -0
  165. package/dist/src/servers/websocket.js.map +1 -0
  166. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +65 -1
  167. package/dist/src/servers/whatsapp-baileys-websocket.js +723 -99
  168. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  169. package/dist/src/servers/whatsapp-jid.d.ts +5 -0
  170. package/dist/src/servers/whatsapp-jid.js +32 -0
  171. package/dist/src/servers/whatsapp-jid.js.map +1 -0
  172. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +5 -0
  173. package/dist/src/servers/whatsapp-wire/binary-node.js +87 -20
  174. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  175. package/dist/src/servers/whatsapp-wire/crypto.d.ts +6 -1
  176. package/dist/src/servers/whatsapp-wire/crypto.js +99 -45
  177. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  178. package/dist/src/servers/whatsapp-wire/handshake.js +39 -4
  179. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  180. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  181. package/dist/src/servers/whatsapp.d.ts +12 -7
  182. package/dist/src/servers/whatsapp.js +269 -107
  183. package/dist/src/servers/whatsapp.js.map +1 -1
  184. package/dist/src/servers/zalo.d.ts +14 -0
  185. package/dist/src/servers/zalo.js +521 -88
  186. package/dist/src/servers/zalo.js.map +1 -1
  187. package/docs/channel-setup.md +197 -19
  188. package/fixtures/examples/crabline.example.yaml +10 -0
  189. package/package.json +20 -15
@@ -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 fixture = params.manifest.fixtures.find((entry) => entry.id === params.fixtureId);
10
- if (!fixture) {
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
- if (!provider.supports.includes(mode)) {
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 === "probe") {
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
- const probeResult = await provider.probe(contextBase);
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
- return (result = toFailure(fixture.id, fixture.provider, mode, error));
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
- let attempts = 0;
67
- const maxAttempts = fixture.retries + 1;
68
- let lastFailure = null;
69
- while (attempts < maxAttempts) {
70
- attempts += 1;
71
- const nonce = createNonce(fixture.id);
72
- try {
73
- const outboundText = createOutboundText({ ...fixture, mode }, nonce);
74
- const since = new Date().toISOString();
75
- const accepted = await provider.send({
76
- ...contextBase,
77
- mode,
78
- nonce,
79
- text: outboundText,
80
- });
81
- diagnostics.push(`accepted message ${accepted.messageId}`);
82
- if (mode === "send") {
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
- const inbound = await provider.waitForInbound({
93
- ...contextBase,
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
- await provider.cleanup?.();
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
- const diagnostic = `cleanup failed: ${ensureErrorMessage(error)}`;
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: "assertion",
540
+ failureKind: fallbackKind,
233
541
  fixtureId,
234
542
  mode,
235
543
  nonce,