archal 0.9.2 → 0.9.4

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 ADDED
@@ -0,0 +1,215 @@
1
+ # archal
2
+
3
+ Pre-deployment testing for AI agents via Archal's hosted digital twins.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install --save-dev archal
9
+ ```
10
+
11
+ Then authenticate:
12
+
13
+ ```bash
14
+ npx archal login
15
+ ```
16
+
17
+ ## Vitest Integration (primary use case)
18
+
19
+ Test your code against hosted GitHub / Stripe / Slack / Jira / Supabase / Google Workspace twins without hitting real APIs. Your SDKs work unchanged — `archal/vitest` transparently routes their traffic to the twin.
20
+
21
+ ### Quick start — single `vitest.config.ts`
22
+
23
+ ```ts
24
+ import { defineConfig } from 'vitest/config';
25
+ import { archalVitestConfig } from 'archal/vitest';
26
+
27
+ export default defineConfig({
28
+ test: archalVitestConfig({
29
+ services: {
30
+ stripe: { mode: 'route', seed: 'small-business' },
31
+ },
32
+ }),
33
+ });
34
+ ```
35
+
36
+ Your existing tests work unchanged:
37
+
38
+ ```ts
39
+ import { it, expect } from 'vitest';
40
+ import Stripe from 'stripe';
41
+
42
+ it('creates a customer', async () => {
43
+ const stripe = new Stripe('sk_test_fake'); // fake key is fine
44
+ const customer = await stripe.customers.create({ // goes to twin, not real Stripe
45
+ email: 'test@example.com',
46
+ });
47
+ expect(customer.id).toMatch(/^cus_/);
48
+ });
49
+ ```
50
+
51
+ ### Multi-project monorepos — `vitest.workspace.ts`
52
+
53
+ ```ts
54
+ import { archalVitestProject } from 'archal/vitest';
55
+
56
+ export default [
57
+ archalVitestProject({
58
+ name: 'stripe-integration-tests',
59
+ services: {
60
+ stripe: { mode: 'route', seed: 'small-business' },
61
+ slack: { mode: 'route' },
62
+ },
63
+ }),
64
+ ];
65
+ ```
66
+
67
+ ### Per-test state isolation
68
+
69
+ ```ts
70
+ import { beforeEach } from 'vitest';
71
+ import { resetArchalTwins } from 'archal/vitest';
72
+
73
+ beforeEach(async () => {
74
+ await resetArchalTwins();
75
+ });
76
+ ```
77
+
78
+ ### Webhook testing
79
+
80
+ Test your webhook handlers against the twins' queued events. The hosted twin
81
+ runs in AWS ECS so it can't POST to your localhost — instead, your test pulls
82
+ queued deliveries with `waitForArchalWebhook()` and invokes your handler
83
+ directly with the exact payload the twin would have sent.
84
+
85
+ ```ts
86
+ import { it, expect } from 'vitest';
87
+ import Stripe from 'stripe';
88
+ import { waitForArchalWebhook } from 'archal/vitest';
89
+ import { handleStripeWebhook } from './src/webhooks';
90
+
91
+ const stripe = new Stripe('sk_test_fake');
92
+
93
+ it('records a subscription when customer.subscription.created fires', async () => {
94
+ // 1. Register a webhook endpoint — the twin needs to know what events to queue
95
+ await stripe.webhookEndpoints.create({
96
+ url: 'http://test.local/stripe-wh',
97
+ enabled_events: ['customer.subscription.created'],
98
+ });
99
+
100
+ // 2. Trigger the event
101
+ const customer = await stripe.customers.create({ email: 'wh@x.com' });
102
+ const sub = await stripe.subscriptions.create({
103
+ customer: customer.id,
104
+ items: [{ price: 'price_existing_in_seed' }],
105
+ });
106
+
107
+ // 3. Pull the queued delivery
108
+ const event = await waitForArchalWebhook('stripe', 'customer.subscription.created');
109
+ expect(event.payload.data.object.id).toBe(sub.id);
110
+
111
+ // 4. Invoke your handler with the exact payload
112
+ await handleStripeWebhook(event.body, event.headers['Stripe-Signature'], process.env.STRIPE_WEBHOOK_SECRET);
113
+
114
+ // 5. Assert your handler did the right thing
115
+ // expect(await db.subscriptions.count()).toBe(1);
116
+ });
117
+ ```
118
+
119
+ **Important**: register the webhook endpoint on the twin *before* firing the
120
+ event. If no endpoint is registered, the twin has nothing to queue and
121
+ `waitForArchalWebhook()` will time out.
122
+
123
+ **Signature verification**: `event.body` is the raw JSON string the twin
124
+ would have POSTed. Pass it and `event.headers['Stripe-Signature']` directly
125
+ to `stripe.webhooks.constructEvent(body, sig, secret)` — the signature is
126
+ HMAC-computed against the body bytes, so re-serializing `event.payload`
127
+ would break verification. Use the secret from the endpoint you registered.
128
+
129
+ **Supported services**:
130
+
131
+ | Service | Support | Notes |
132
+ |---|---|---|
133
+ | Stripe | ✅ | |
134
+ | GitHub | ✅ | |
135
+ | Slack | ✅ | Receiver-side signature only — see Slack caveat below |
136
+ | Jira | ✅ | Delivered via history buffer (also POSTed to registered URLs) |
137
+ | Linear | ✅ | Delivered via history buffer (also POSTed to registered URLs) |
138
+ | Supabase | ❌ | Supabase webhooks are database-triggered; test against real Postgres or `supabase_functions.pg_net` directly |
139
+ | Google Workspace | ❌ | No webhook API — uses GCP Pub/Sub push notifications |
140
+
141
+ **Parallel workers caveat**: `waitForArchalWebhook()` consumes deliveries
142
+ from a shared queue by default. When vitest runs test files in parallel
143
+ across workers, worker A can swallow worker B's event. If your tests
144
+ depend on webhook events, set `testIsolation: 'serial'` in your config.
145
+ Alternatively, pass `consume: false` to leave the queue intact after a
146
+ match and use `listArchalWebhooks('stripe')` for sequence assertions.
147
+
148
+ **Slack caveat**: Slack verifies webhook signatures at the receiver using
149
+ the timestamp header + signing secret, not a sender-side signature header.
150
+ Archal's Slack twin does not add a `X-Slack-Signature` header to queued
151
+ deliveries. If your handler verifies signatures, stub that verification
152
+ in tests or compute the header yourself from `delivery.body`.
153
+
154
+ ### Test isolation across parallel workers
155
+
156
+ Each vitest worker is automatically routed to its own per-worker state on the twin — parallel tests across workers don't see each other's writes. This is transparent: the integration reads `VITEST_WORKER_ID` in each worker process and tags every outbound SDK request with an `X-Archal-Worker-Id` header. The twin maintains a separate `StateEngine` per worker id, seeded from the baseline on first request.
157
+
158
+ ```ts
159
+ // Test A, in worker 1
160
+ const customer = await stripe.customers.create({ email: 'a@x.com' });
161
+ // Test B, in worker 2 (running in parallel, same twin session)
162
+ const list = await stripe.customers.list();
163
+ // list does NOT include a@x.com — each worker's state is isolated
164
+ ```
165
+
166
+ **Currently isolation-enabled twins**: Stripe, GitHub, Slack, Jira, Linear.
167
+
168
+ **Twins without isolation** (Supabase, Google Workspace, Telegram, Ramp, Browser) fall back to shared state — the worker header is sent but the twin ignores it. Writes from all workers land in the same baseline state. If your tests depend on global assertions against these twins, use `testIsolation: 'serial'` (see below).
169
+
170
+ **Memory note**: per-worker state costs N × baseline size per twin process. For 4 workers × 50 KB seed, that's ~200 KB per twin. Not a concern in practice.
171
+
172
+ #### Forcing serial tests
173
+
174
+ If your test suite needs full serialization (for example, it asserts on global counts across tests, or you're hitting one of the non-isolated twins), opt in to `testIsolation: 'serial'`:
175
+
176
+ ```ts
177
+ archalVitestConfig({
178
+ services: { stripe: { mode: 'route' } },
179
+ testIsolation: 'serial', // maxWorkers: 1, fileParallelism: false
180
+ })
181
+ ```
182
+
183
+ ## What you see in the terminal
184
+
185
+ On the first `vitest run`, session provisioning takes about 30 seconds (ECS Fargate cold start). The integration prints a progress line every few seconds so the wait is visible:
186
+
187
+ ```
188
+ [archal] provisioning stripe twin... 5s
189
+ [archal] provisioning stripe twins... 10s
190
+ ...
191
+ ```
192
+
193
+ Subsequent runs with the same configuration reuse the warm session (~2 seconds).
194
+
195
+ At the end of every run, the estimated twin-minute usage is printed:
196
+
197
+ ```
198
+ [archal] ~2 twin-minutes for this run (38.5s × 1 twin: stripe)
199
+ ```
200
+
201
+ Silence these outputs with `ARCHAL_VITEST_QUIET_PROGRESS=1` and `ARCHAL_VITEST_QUIET_USAGE=1`.
202
+
203
+ ## Authentication
204
+
205
+ In priority order:
206
+
207
+ 1. `ARCHAL_VITEST_TOKEN` env var
208
+ 2. `ARCHAL_TOKEN` env var
209
+ 3. Stored credentials from `archal login` in `~/.archal/credentials.json`
210
+
211
+ ## Docs
212
+
213
+ Full documentation: https://archal.ai/docs
214
+
215
+ Full `@archal/vitest` reference: https://github.com/Archal-Labs/archal/tree/main/packages/vitest/README.md
package/dist/vitest.d.ts CHANGED
@@ -1 +1 @@
1
- export { ArchalVitestProjectOptions, ArchalVitestProjectTestOptions, archalVitestProject, bootstrapArchalVitestRouting, getInstalledArchalVitestRuntime, getInstalledArchalVitestSession } from '@archal/vitest';
1
+ export { ArchalVitestBuiltTestConfig, ArchalVitestProjectOptions, ArchalVitestProjectTestOptions, ArchalVitestPublicSessionSnapshot, ArchalWebhookDelivery, WaitForWebhookOptions, archalVitestConfig, archalVitestProject, bootstrapArchalVitestRouting, clearArchalWebhooks, getInstalledArchalVitestRuntime, getInstalledArchalVitestSession, listArchalWebhooks, resetArchalTwins, waitForArchalWebhook } from '@archal/vitest';
package/dist/vitest.js CHANGED
@@ -1,13 +1,23 @@
1
1
  // src/vitest.ts
2
2
  import {
3
+ archalVitestConfig,
3
4
  archalVitestProject,
4
5
  bootstrapArchalVitestRouting,
6
+ clearArchalWebhooks,
5
7
  getInstalledArchalVitestRuntime,
6
- getInstalledArchalVitestSession
8
+ getInstalledArchalVitestSession,
9
+ listArchalWebhooks,
10
+ resetArchalTwins,
11
+ waitForArchalWebhook
7
12
  } from "@archal/vitest";
8
13
  export {
14
+ archalVitestConfig,
9
15
  archalVitestProject,
10
16
  bootstrapArchalVitestRouting,
17
+ clearArchalWebhooks,
11
18
  getInstalledArchalVitestRuntime,
12
- getInstalledArchalVitestSession
19
+ getInstalledArchalVitestSession,
20
+ listArchalWebhooks,
21
+ resetArchalTwins,
22
+ waitForArchalWebhook
13
23
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archal",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "Pre-deployment testing for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,6 +37,7 @@
37
37
  ],
38
38
  "scripts": {
39
39
  "build": "tsup",
40
+ "prepare": "node scripts/prepare.cjs",
40
41
  "typecheck": "tsc --noEmit"
41
42
  },
42
43
  "dependencies": {