archal 0.9.3 → 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 +215 -0
- package/bin/archal.cjs +0 -0
- package/dist/vitest.d.ts +1 -1
- package/dist/vitest.js +12 -2
- package/package.json +9 -8
- package/LICENSE +0 -8
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/bin/archal.cjs
CHANGED
|
File without changes
|
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.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"description": "Pre-deployment testing for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,9 +35,14 @@
|
|
|
35
35
|
"bin",
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"prepare": "node scripts/prepare.cjs",
|
|
41
|
+
"typecheck": "tsc --noEmit"
|
|
42
|
+
},
|
|
38
43
|
"dependencies": {
|
|
39
|
-
"@archal/cli": "
|
|
40
|
-
"@archal/vitest": "
|
|
44
|
+
"@archal/cli": "workspace:^",
|
|
45
|
+
"@archal/vitest": "workspace:^"
|
|
41
46
|
},
|
|
42
47
|
"peerDependencies": {
|
|
43
48
|
"vitest": ">=2.0.0"
|
|
@@ -50,9 +55,5 @@
|
|
|
50
55
|
"devDependencies": {
|
|
51
56
|
"tsup": "^8.5.0",
|
|
52
57
|
"typescript": "^5.9.0"
|
|
53
|
-
},
|
|
54
|
-
"scripts": {
|
|
55
|
-
"build": "tsup",
|
|
56
|
-
"typecheck": "tsc --noEmit"
|
|
57
58
|
}
|
|
58
|
-
}
|
|
59
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
Copyright (c) 2026 Archal Labs, Inc. All rights reserved.
|
|
2
|
-
|
|
3
|
-
This software is proprietary and confidential. No part of this software may be
|
|
4
|
-
reproduced, distributed, or transmitted in any form or by any means, including
|
|
5
|
-
photocopying, recording, or other electronic or mechanical methods, without the
|
|
6
|
-
prior written permission of Archal Labs, Inc.
|
|
7
|
-
|
|
8
|
-
For licensing inquiries, visit https://archal.ai or contact support@archal.ai.
|