callx402 1.0.1 → 1.0.2
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/.github/npm-publish.yml.staged +27 -0
- package/.github/workflows/npm-publish.yml +29 -0
- package/README.md +161 -10
- package/bin/callx402.js +296 -13
- package/core/index.js +35 -6
- package/core/rail.js +233 -0
- package/core/veyline.js +240 -0
- package/docs/README.md +11 -9
- package/docs/action-reference.md +2 -2
- package/docs/cli-help-verified-2026-10-06.txt +36 -0
- package/docs/problem-map.md +30 -15
- package/docs/troubleshooting/cx402-duppay-001-duplicate-payment-risk.md +135 -0
- package/docs/troubleshooting/cx402-paidnores-001-paid-no-result.md +3 -1
- package/docs/troubleshooting/cx402-safretry-001-retry-after-payment.md +13 -1
- package/docs/what-is-callx402.md +5 -4
- package/logo-400.png +0 -0
- package/mcp/README.md +5 -0
- package/mcp/index.js +1 -1
- package/package.json +3 -2
- package/server.json +2 -2
- package/test/cli-dispatch.test.js +38 -19
- package/test/helpers.js +20 -0
- package/test/integrity.test.js +2 -1
- package/test/intent-pipeline.test.js +2 -3
- package/test/remote.test.js +2 -3
- package/test/safety.test.js +4 -5
- package/test/sdk.test.js +3 -4
- package/test/server.test.js +4 -5
- package/test/subsystems.test.js +7 -8
- package/test/veyline-commands.test.js +193 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: CX402-DUPPAY-001
|
|
3
|
+
title: "x402 duplicate-payment risk: one intent, two authorizations"
|
|
4
|
+
category: duplicate-payment-risk
|
|
5
|
+
severity: critical
|
|
6
|
+
economic_risk: "A retry that signs a second authorization for the same intent produces two on-chain transfers for one purchase. No error message, no warning."
|
|
7
|
+
search_phrases: ["x402 duplicate payment", "x402 paid twice", "x402 double payment", "x402 retry double charge", "x402 second authorization same intent"]
|
|
8
|
+
reading_minutes: 4
|
|
9
|
+
sources: ["https://github.com/x402-foundation/x402/issues/452", "https://github.com/x402-foundation/x402/issues/831", "https://github.com/x402-foundation/x402 (v2 spec, section 9)"]
|
|
10
|
+
related: ["CX402-SAFFRETRY-001", "CX402-SETTLEPEND-001", "CX402-PAIDNORES-001"]
|
|
11
|
+
updated: 2026-10-07
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# x402 duplicate-payment risk: one intent, two authorizations
|
|
15
|
+
|
|
16
|
+
## THE PROBLEM
|
|
17
|
+
|
|
18
|
+
You have one intent — pay for one thing — but the payment path may have
|
|
19
|
+
already produced an authorization. A retry that mints a *second*
|
|
20
|
+
authorization for the same intent can settle twice: two on-chain transfers,
|
|
21
|
+
one purchase. The protocol does not stop you; the second signature is a
|
|
22
|
+
fresh, valid instruction to move funds.
|
|
23
|
+
|
|
24
|
+
## WHY IT HAPPENS
|
|
25
|
+
|
|
26
|
+
Three protocol facts combine:
|
|
27
|
+
|
|
28
|
+
1. **Authorizations are bearer-like.** An EIP-3009 authorization, once
|
|
29
|
+
signed, instructs the token contract to move funds. Signing a second
|
|
30
|
+
one for the same intent creates a second independent instruction — the
|
|
31
|
+
chain cannot know you meant them as alternatives.
|
|
32
|
+
2. **Ambiguous states invite the second signature.** `settlement_pending`
|
|
33
|
+
("broadcast but unconfirmed"), timeouts after payment was sent, and
|
|
34
|
+
`AuthorizationUsed` reverts (which look like genuine failures) all leave
|
|
35
|
+
the operator unsure whether the first authorization settled. The
|
|
36
|
+
instinct is to sign again — exactly the wrong move while the first
|
|
37
|
+
authorization may still be live
|
|
38
|
+
([x402-foundation/x402#452](https://github.com/x402-foundation/x402/issues/452),
|
|
39
|
+
[x402-foundation/x402#831](https://github.com/x402-foundation/x402/issues/831)).
|
|
40
|
+
3. **Duplicate submission is undefined.** The x402 spec does not define how
|
|
41
|
+
facilitators handle the same payload twice, so there is no standard
|
|
42
|
+
"already settled" dedupe to save you.
|
|
43
|
+
|
|
44
|
+
## DO NOT DO THIS
|
|
45
|
+
|
|
46
|
+
Do not sign a second authorization for the same intent while the first
|
|
47
|
+
one's fate is unknown. "Unknown" is not "failed." A second signature is
|
|
48
|
+
never a status check — it is a second payment instruction.
|
|
49
|
+
|
|
50
|
+
Do not treat a retry as safe because the first attempt "probably" failed.
|
|
51
|
+
Probably is not evidence, and the cost of being wrong is a full second
|
|
52
|
+
payment.
|
|
53
|
+
|
|
54
|
+
## THE SAFE FIX
|
|
55
|
+
|
|
56
|
+
1. **Prove the first authorization dead before minting a second.** For
|
|
57
|
+
`settlement_pending`, the spec requires the broadcast hash — reconcile
|
|
58
|
+
it on chain. Confirmed means stop. Failed/dropped means (and only means)
|
|
59
|
+
it is safe to sign again.
|
|
60
|
+
2. **Separate the questions.** "Did the money move?" is answered by
|
|
61
|
+
settlement evidence, not by retrying. "Is this specific retry safe to
|
|
62
|
+
sign?" is answered only after the first question is settled.
|
|
63
|
+
3. **One nonce per intent, client-side.** Until the ecosystem standardizes
|
|
64
|
+
retry-by-nonce, never mint a second authorization for an intent while
|
|
65
|
+
the first is unresolved. Key retries by the EIP-3009 nonce where your
|
|
66
|
+
client supports it.
|
|
67
|
+
|
|
68
|
+
## VERIFY
|
|
69
|
+
|
|
70
|
+
Reconcile exactly one on-chain transfer per intent: one transaction hash,
|
|
71
|
+
correct recipient, correct amount, confirmed. If a retry produced a second
|
|
72
|
+
confirmed transfer for the same intent, the retry logic is unsafe — fix it
|
|
73
|
+
before it runs again. Log every retry decision with the evidence that
|
|
74
|
+
justified it.
|
|
75
|
+
|
|
76
|
+
## Using Callx402 for this condition
|
|
77
|
+
|
|
78
|
+
Start free and read-only. First, settle the money question with `resolve`:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
callx402 resolve --evidence '{"txHash":"0x...","network":"base"}'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Then get the read-only retry verdict with `recover`:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
callx402 recover <operationId> --identity <id>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
A SAFE_RETRY verdict is the free, read-only answer. If the settlement
|
|
91
|
+
behind the operation is UNKNOWN, `recover` refuses outright
|
|
92
|
+
(SETTLEMENT_UNKNOWN -> HUMAN_REVIEW) — no retry, no repay, no bypass.
|
|
93
|
+
|
|
94
|
+
When you need the production rail to judge the duplicate-payment risk of a
|
|
95
|
+
*specific* retry plan, use the paid one-off `duplicate_payment_risk`
|
|
96
|
+
action. Quote first (free), then pay deliberately — never blind-retry a
|
|
97
|
+
payment to reach it:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
# 1. Free quote: $0.25 on the x402 path
|
|
101
|
+
curl "https://payload-rail.fly.dev/v1/callx402/quote?action=duplicate_payment_risk&path=x402"
|
|
102
|
+
|
|
103
|
+
# 2. Invoke unauthenticated -> HTTP 402 with exact USDC terms (Base, 300s window)
|
|
104
|
+
curl -X POST https://payload-rail.fly.dev/v1/callx402/actions/duplicate_payment_risk \
|
|
105
|
+
-H 'Content-Type: application/json' -d '{"intent_summary":"..."}'
|
|
106
|
+
|
|
107
|
+
# 3. Pay from an authorized wallet, then retry with {txHash, quote_id}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The action's fail-closed rule: an ambiguous authorization state blocks the
|
|
111
|
+
retry rather than blessing it. For the adjacent narrowed question — "is
|
|
112
|
+
THIS specific retry safe to sign?" — the paid `safe_retry` action follows
|
|
113
|
+
the same quote-then-pay shape. Both are rail-only paid actions: there is no
|
|
114
|
+
CLI command or MCP tool for them (typing one at the CLI prints a usage
|
|
115
|
+
error that points to `docs/problem-map.md`). Start with the free
|
|
116
|
+
`resolve`/`recover` sequence above before spending.
|
|
117
|
+
|
|
118
|
+
Tested: the free sequence (`resolve` UNKNOWN -> exit 5, `recover`
|
|
119
|
+
read-only verdicts) is covered by the dispatch and veyline command suites.
|
|
120
|
+
The paid action's quote endpoint and 402 payment terms were verified live
|
|
121
|
+
against the rail on 2026-10-07; the paid verdict itself was not executed
|
|
122
|
+
(no payment was made).
|
|
123
|
+
|
|
124
|
+
Limitation: the free verdicts reason from recorded evidence only. If the
|
|
125
|
+
operation was never recorded, they say so instead of inventing a basis —
|
|
126
|
+
gather the settlement evidence first.
|
|
127
|
+
|
|
128
|
+
## SOURCES / VERIFICATION SCOPE
|
|
129
|
+
|
|
130
|
+
Protocol claims cite the x402 v2 spec and the issue tracker links in the
|
|
131
|
+
frontmatter, independently of callx402. Callx402 claims above were checked
|
|
132
|
+
against the source tree (`core/veyline.js`, `core/index.js`, `bin/callx402.js`),
|
|
133
|
+
the dispatch and veyline command test suites, and live rail responses
|
|
134
|
+
(quote endpoint, 402 terms, fee schedule) as of 2026-10-07. No payment was
|
|
135
|
+
made and no paid verdict was executed for this article.
|
|
@@ -84,7 +84,9 @@ callx402 evidence <operationId>
|
|
|
84
84
|
|
|
85
85
|
It accepts an operation id (and optionally `--dir` to point at a different ledger store). It evaluates the operation ledger's recorded events: the latest per-plane states (payment, execution, delivery), the protocols involved, and an event trail. It returns those states, the trail, and a plain statement of basis — including "no events recorded for this operation," stated plainly rather than hidden.
|
|
86
86
|
|
|
87
|
-
The follow-on sequence: `explain <operationId>` classifies the recorded state — NO_BASIS, INCOMPLETE (any UNKNOWN plane means do not retry, do not repay), KNOWN_SAFE, RECOVERY_CANDIDATE, or PARTIAL — and a RECOVERY_CANDIDATE verdict points you to `recover <operationId>`, which returns the read-only safe-recovery verdict described in the companion retry article.
|
|
87
|
+
The follow-on sequence: `explain <operationId>` classifies the recorded state — NO_BASIS, INCOMPLETE (any UNKNOWN plane means do not retry, do not repay), KNOWN_SAFE, RECOVERY_CANDIDATE, or PARTIAL — and a RECOVERY_CANDIDATE verdict points you to `recover <operationId>`, which returns the read-only safe-recovery verdict described in the companion retry article (`cx402-safretry-001-retry-after-payment.md`).
|
|
88
|
+
|
|
89
|
+
When the free read-only verdict is not enough and you need the production rail to recover the incident, the paid one-off `recover` rail action follows the quote-then-pay shape: free quote at `GET /v1/callx402/quote?action=recover&path=x402` ($0.50 on the x402 path), then `POST /v1/callx402/actions/recover` returns HTTP 402 with the exact USDC terms — pay deliberately from an authorized wallet and retry with `{txHash, quote_id}`. See `docs/problem-map.md` for the full problem-to-action map.
|
|
88
90
|
|
|
89
91
|
What the sequence deliberately refuses to do: all three steps are read-only. None of them re-executes the tool, re-settles the payment, or triggers a retry. `recover` states its verdict and stops there.
|
|
90
92
|
|
|
@@ -97,7 +97,19 @@ It returns the decision with its reasoning and a terminal assessment, ending wit
|
|
|
97
97
|
|
|
98
98
|
What it deliberately refuses to do: it never charges, never executes, and never retries on your behalf. When the settlement state behind the operation is UNKNOWN, the recovery path refuses outright — an explicit retry after an unknown settlement could double-spend, so the pipeline demands manual settlement resolution first (the `resolve` action in the companion settlement article) and offers no bypass.
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
For the narrowed question — "is THIS specific retry safe to sign?" — the paid one-off `safe_retry` rail action judges the concrete retry plan. It is rail-only (no CLI command; typing `callx402 safe_retry` prints a usage error pointing to `docs/problem-map.md`). Quote first (free), then pay deliberately:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
curl "https://payload-rail.fly.dev/v1/callx402/quote?action=safe_retry&path=x402"
|
|
104
|
+
# -> {"quote_id":"...","quoted_price_usd":"0.25", ...}
|
|
105
|
+
curl -X POST https://payload-rail.fly.dev/v1/callx402/actions/safe_retry \
|
|
106
|
+
-H 'Content-Type: application/json' -d '{"retry_plan":"..."}'
|
|
107
|
+
# -> HTTP 402 with exact USDC terms; pay from an authorized wallet, retry with {txHash, quote_id}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Its fail-closed rule: a retry that would double-pay is refused. Start with the free `recover` verdict above before spending. The sibling risk — a retry that would mint a second authorization for the same intent — is covered in the companion duplicate-payment article (`cx402-duppay-001-duplicate-payment-risk.md`).
|
|
111
|
+
|
|
112
|
+
Tested: yes. The veyline command suite covers RECOVERABLE and SAFE_RETRY decisions, the identity-from-evidence fallback, the unknown-identity refusal path, and the disabled-subsystem refusal (exit 3). The paid `safe_retry` quote endpoint and 402 terms were verified live against the rail on 2026-10-07; the paid verdict itself was not executed (no payment made).
|
|
101
113
|
|
|
102
114
|
Limitation: it reasons from recorded evidence only. If the operation was never recorded in the ledger, there is no basis for a verdict, and it says so instead of inventing one.
|
|
103
115
|
|
package/docs/what-is-callx402.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# What Callx402 Is, What It Can Do, and How to Use It
|
|
2
2
|
|
|
3
|
-
**Version:** 1.0.
|
|
3
|
+
**Version:** 1.0.1 · **Status:** published on npm as `callx402@1.0.1` · **Verified against:** `cli-help-verified-2026-10-06.txt`, `SPEC.md`, `core/`, `server/index.js`, `sdk/`, and the test suite (2026-10-06), plus a clean-install audit 2026-10-07.
|
|
4
4
|
|
|
5
5
|
## The one-paragraph definition
|
|
6
6
|
|
|
@@ -20,7 +20,7 @@ Callx402 sits between the caller and the subsystems. There are three entry point
|
|
|
20
20
|
|
|
21
21
|
All three entry points converge on one dispatcher, `core/index.js` (`runAction` / `runIntent`), and return one shared shape: the result envelope from SPEC section 4.
|
|
22
22
|
|
|
23
|
-
The subsystems themselves live in the
|
|
23
|
+
The subsystems themselves live in the Veyline Developer Primer v2.0.0 tree (x402-paid-api-starter-kit) (`~/workspace/products/x402-paid-api-starter-kit/v2.0.0`). Callx402 imports from that tree via lazy `require` and never writes anything under it; an integrity test ("zero files changed under the v2.0.0 tree") guards this invariant. A separate Veyline recovery track (`core/veyline.js`) resolves modules from the Veyline engineering recovery directory instead.
|
|
24
24
|
|
|
25
25
|
## The Veyline relationship
|
|
26
26
|
|
|
@@ -217,7 +217,7 @@ Hard invariants, enforced in the core and honored by the CLI, SDKs, and HTTP ser
|
|
|
217
217
|
- **The approval threshold fails closed.** Planned cost above the threshold (default $5.00 USD) without explicit `--approve` refuses with exit 4. Default budget is $1.00 USD.
|
|
218
218
|
- **Idempotency keys are honored.** A duplicate key returns the original stored result with `deduped:true`; nothing re-executes. The store is a file-backed JSONL log (`~/.config/callx402/idempotency.jsonl`, overridable with `CALLX402_IDEMPOTENCY_FILE`).
|
|
219
219
|
- **Disabled or unreachable subsystems fail closed.** They name the subsystem and its flag; success is never faked.
|
|
220
|
-
- **Feature flags are per-module.** Each subsystem exposes `FLAG` and `enabled()`; callx402 checks before dispatch and never force-enables anything that guards money movement. Default state is off: a fresh `callx402 status` reports 15/15 subsystems reachable and 0/15 enabled.
|
|
220
|
+
- **Feature flags are per-module.** Each subsystem exposes `FLAG` and `enabled()`; callx402 checks before dispatch and never force-enables anything that guards money movement. Default state is off: with the v2.0.0 tree present, a fresh `callx402 status` reports 15/15 subsystems reachable and 0/15 enabled; on a bare `npm install callx402` (no tree) it reports 0/15 reachable.
|
|
221
221
|
|
|
222
222
|
## Configuration
|
|
223
223
|
|
|
@@ -227,9 +227,10 @@ Config file: `~/.config/callx402/config.json` (override the path with `CALLX402_
|
|
|
227
227
|
|
|
228
228
|
## Current limitations
|
|
229
229
|
|
|
230
|
+
- **Subsystem commands need the v2.0.0 tree.** `diagnose`, `rescue`, `route`, `resolve`, `doctor`, `monitor`, `preflight`, and `inspect` dispatch into the Veyline Developer Primer v2.0.0 tree, which is a separate checkout, not an npm dependency. On a bare `npm install callx402` they exit 3 with `subsystem_unreachable`. Point `CALLX402_V2_ROOT` at the tree, or run in remote mode against your own callx402 server.
|
|
230
231
|
- **No live execution route is wired.** The `execute` pipeline is honest about this: without a real executable route it returns "no executable route available," moves no money, and issues no effect proof. Execution side effects would require a live route plus an enabled execution kernel.
|
|
231
232
|
- **Subsystems are disabled by default.** Each action exits 3 naming the flag (e.g. `PAYLOAD_SENTINEL=1`) until the operator enables it.
|
|
232
|
-
- **Rescue is triage only.** Even with auth,
|
|
233
|
+
- **Rescue is triage only.** Even with auth, this local command runs detection, quoting, and a free-vs-paid offer; paid execution is performed only through the paid on-demand callx402 action path, never by this local command.
|
|
233
234
|
- **Diagnosis and resolution depend on supplied evidence.** `diagnose` evaluates evidence deterministically; `resolve` on empty evidence yields UNKNOWN.
|
|
234
235
|
- **The capability graph is experimental** (`createGraph()` throws unless `PAYLOAD_MCP_FABRIC=1`), so `route` and `inspect` have nothing to rank or look up until tools are registered under that flag.
|
|
235
236
|
- **HTTP covers five POST routes only** (`/call`, `/rescue`, `/route`, `/resolve`, `/diagnose`); `monitor`, `preflight`, `inspect`, `evidence`, `explain`, `recover`, and `config` are CLI/SDK only.
|
package/logo-400.png
ADDED
|
Binary file
|
package/mcp/README.md
CHANGED
|
@@ -20,6 +20,11 @@ Only diagnostic, read-only actions are exposed. Nothing here charges, executes,
|
|
|
20
20
|
retries, or repays. If a subsystem is unreachable, the tool fails closed
|
|
21
21
|
instead of inventing an answer.
|
|
22
22
|
|
|
23
|
+
Setup note: `x402_status` works with zero setup. The other five tools
|
|
24
|
+
dispatch into the v2.0.0 subsystem tree, so they report
|
|
25
|
+
`subsystem_unreachable` until `CALLX402_V2_ROOT` points at the tree and the
|
|
26
|
+
matching flag is enabled (see the main README, "Full subsystem commands").
|
|
27
|
+
|
|
23
28
|
Tool names are namespaced `x402_*` — never `call_x402` (live collision on the
|
|
24
29
|
fiatdock marketplace).
|
|
25
30
|
|
package/mcp/index.js
CHANGED
package/package.json
CHANGED
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
"url": "https://github.com/Payloadhq/callx402"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"smoke": "node bin/callx402.js status --json"
|
|
27
|
+
"smoke": "node bin/callx402.js status --json",
|
|
28
|
+
"test": "node --test test/*.test.js"
|
|
28
29
|
},
|
|
29
|
-
"version": "1.0.
|
|
30
|
+
"version": "1.0.2",
|
|
30
31
|
"mcpName": "io.github.payload-tools/callx402"
|
|
31
32
|
}
|
package/server.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"transport": {
|
|
11
11
|
"type": "stdio"
|
|
12
12
|
},
|
|
13
|
-
"version": "1.0.
|
|
13
|
+
"version": "1.0.2"
|
|
14
14
|
}
|
|
15
15
|
],
|
|
16
16
|
"repository": {
|
|
@@ -18,6 +18,6 @@
|
|
|
18
18
|
"url": "https://github.com/Payloadhq/callx402"
|
|
19
19
|
},
|
|
20
20
|
"title": "callx402 MCP server",
|
|
21
|
-
"version": "1.0.
|
|
21
|
+
"version": "1.0.2",
|
|
22
22
|
"websiteUrl": "https://github.com/Payloadhq/callx402"
|
|
23
23
|
}
|
|
@@ -8,15 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const { test } = require('node:test');
|
|
11
|
-
const { scratchDir, cliEnv, runCLI, assertEnvelope } = require('./helpers');
|
|
12
|
-
|
|
11
|
+
const { scratchDir, cliEnv, runCLI, assertEnvelope, needsV2Tree } = require('./helpers');
|
|
13
12
|
const scratch = scratchDir('cli');
|
|
14
13
|
|
|
15
14
|
function env(extra) {
|
|
16
15
|
return cliEnv(scratch, extra);
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
test('diagnose: disabled subsystem -> exit 3, honest disabled envelope', async (t) => {
|
|
18
|
+
test('diagnose: disabled subsystem -> exit 3, honest disabled envelope', needsV2Tree, async (t) => {
|
|
20
19
|
const r = await runCLI(['diagnose', '--json'], env());
|
|
21
20
|
t.assert.strictEqual(r.code, 3);
|
|
22
21
|
assertEnvelope(t, r.json, 'diagnose');
|
|
@@ -27,7 +26,7 @@ test('diagnose: disabled subsystem -> exit 3, honest disabled envelope', async (
|
|
|
27
26
|
t.assert.match(r.json.error.message, /PAYLOAD_MCP_DOCTOR=1/);
|
|
28
27
|
});
|
|
29
28
|
|
|
30
|
-
test('diagnose: enabled -> exit 0, real doctor report with stages', async (t) => {
|
|
29
|
+
test('diagnose: enabled -> exit 0, real doctor report with stages', needsV2Tree, async (t) => {
|
|
31
30
|
const r = await runCLI(['diagnose', '--json'], env({ PAYLOAD_MCP_DOCTOR: '1' }));
|
|
32
31
|
t.assert.strictEqual(r.code, 0);
|
|
33
32
|
assertEnvelope(t, r.json, 'diagnose');
|
|
@@ -38,13 +37,13 @@ test('diagnose: enabled -> exit 0, real doctor report with stages', async (t) =>
|
|
|
38
37
|
t.assert.strictEqual(typeof r.json.data.report.whatFailed, 'string');
|
|
39
38
|
});
|
|
40
39
|
|
|
41
|
-
test('doctor: alias of diagnose -> envelope action is diagnose', async (t) => {
|
|
40
|
+
test('doctor: alias of diagnose -> envelope action is diagnose', needsV2Tree, async (t) => {
|
|
42
41
|
const r = await runCLI(['doctor', '--json'], env({ PAYLOAD_MCP_DOCTOR: '1' }));
|
|
43
42
|
t.assert.strictEqual(r.code, 0);
|
|
44
43
|
assertEnvelope(t, r.json, 'diagnose');
|
|
45
44
|
});
|
|
46
45
|
|
|
47
|
-
test('rescue: enabled but no auth -> auth_required, exit 3, nothing executed', async (t) => {
|
|
46
|
+
test('rescue: enabled but no auth -> auth_required, exit 3, nothing executed', needsV2Tree, async (t) => {
|
|
48
47
|
const r = await runCLI(['rescue', '--incident', 'inc-1', '--json'], env({ PAYLOAD_RESCUE: '1' }));
|
|
49
48
|
t.assert.strictEqual(r.code, 3);
|
|
50
49
|
assertEnvelope(t, r.json, 'rescue');
|
|
@@ -54,7 +53,7 @@ test('rescue: enabled but no auth -> auth_required, exit 3, nothing executed', a
|
|
|
54
53
|
t.assert.match(r.json.disposition, /no authorization/i);
|
|
55
54
|
});
|
|
56
55
|
|
|
57
|
-
test('rescue: with --auth -> real rescue triage, read-only, exit 0', async (t) => {
|
|
56
|
+
test('rescue: with --auth -> real rescue triage, read-only, exit 0', needsV2Tree, async (t) => {
|
|
58
57
|
const r = await runCLI(['rescue', '--incident', 'inc-1', '--auth', '--json'], env({ PAYLOAD_RESCUE: '1' }));
|
|
59
58
|
t.assert.strictEqual(r.code, 0);
|
|
60
59
|
assertEnvelope(t, r.json, 'rescue');
|
|
@@ -62,14 +61,14 @@ test('rescue: with --auth -> real rescue triage, read-only, exit 0', async (t) =
|
|
|
62
61
|
t.assert.match(r.json.disposition, /read-only/i);
|
|
63
62
|
});
|
|
64
63
|
|
|
65
|
-
test('route: disabled subsystem -> exit 3', async (t) => {
|
|
64
|
+
test('route: disabled subsystem -> exit 3', needsV2Tree, async (t) => {
|
|
66
65
|
const r = await runCLI(['route', '--goal', 'pick a tool', '--json'], env());
|
|
67
66
|
t.assert.strictEqual(r.code, 3);
|
|
68
67
|
assertEnvelope(t, r.json, 'route');
|
|
69
68
|
t.assert.strictEqual(r.json.error.code, 'subsystem_disabled');
|
|
70
69
|
});
|
|
71
70
|
|
|
72
|
-
test('route: enabled, no candidates -> honest no-route, exit 0', async (t) => {
|
|
71
|
+
test('route: enabled, no candidates -> honest no-route, exit 0', needsV2Tree, async (t) => {
|
|
73
72
|
const r = await runCLI(['route', '--goal', 'pick a tool', '--json'], env({ PAYLOAD_MCP_ROUTES: '1' }));
|
|
74
73
|
t.assert.strictEqual(r.code, 0);
|
|
75
74
|
assertEnvelope(t, r.json, 'route');
|
|
@@ -78,14 +77,14 @@ test('route: enabled, no candidates -> honest no-route, exit 0', async (t) => {
|
|
|
78
77
|
t.assert.strictEqual(r.json.data.candidates, 0);
|
|
79
78
|
});
|
|
80
79
|
|
|
81
|
-
test('route: missing --goal -> usage error, exit 2', async (t) => {
|
|
80
|
+
test('route: missing --goal -> usage error, exit 2', needsV2Tree, async (t) => {
|
|
82
81
|
const r = await runCLI(['route', '--json'], env({ PAYLOAD_MCP_ROUTES: '1' }));
|
|
83
82
|
t.assert.strictEqual(r.code, 2);
|
|
84
83
|
assertEnvelope(t, r.json, 'route');
|
|
85
84
|
t.assert.strictEqual(r.json.error.code, 'usage');
|
|
86
85
|
});
|
|
87
86
|
|
|
88
|
-
test('resolve: empty evidence -> settlement UNKNOWN, exit 5', async (t) => {
|
|
87
|
+
test('resolve: empty evidence -> settlement UNKNOWN, exit 5', needsV2Tree, async (t) => {
|
|
89
88
|
const r = await runCLI(['resolve', '--evidence', '{}', '--json'], env({ PAYLOAD_SETTLEMENT_RESOLVER: '1' }));
|
|
90
89
|
t.assert.strictEqual(r.code, 5);
|
|
91
90
|
assertEnvelope(t, r.json, 'resolve');
|
|
@@ -94,7 +93,7 @@ test('resolve: empty evidence -> settlement UNKNOWN, exit 5', async (t) => {
|
|
|
94
93
|
t.assert.strictEqual(r.json.error.code, 'settlement_unknown');
|
|
95
94
|
});
|
|
96
95
|
|
|
97
|
-
test('resolve: disabled subsystem -> exit 3', async (t) => {
|
|
96
|
+
test('resolve: disabled subsystem -> exit 3', needsV2Tree, async (t) => {
|
|
98
97
|
const r = await runCLI(['resolve', '--evidence', '{}', '--json'], env());
|
|
99
98
|
t.assert.strictEqual(r.code, 3);
|
|
100
99
|
assertEnvelope(t, r.json, 'resolve');
|
|
@@ -123,14 +122,14 @@ test('intent mode: bare quoted string dispatches execute', async (t) => {
|
|
|
123
122
|
assertEnvelope(t, r.json, 'execute');
|
|
124
123
|
});
|
|
125
124
|
|
|
126
|
-
test('monitor: disabled sentinel -> exit 3', async (t) => {
|
|
125
|
+
test('monitor: disabled sentinel -> exit 3', needsV2Tree, async (t) => {
|
|
127
126
|
const r = await runCLI(['monitor', '--once', '--json'], env());
|
|
128
127
|
t.assert.strictEqual(r.code, 3);
|
|
129
128
|
assertEnvelope(t, r.json, 'monitor');
|
|
130
129
|
t.assert.strictEqual(r.json.error.code, 'subsystem_disabled');
|
|
131
130
|
});
|
|
132
131
|
|
|
133
|
-
test('monitor: inert sentinel snapshot -> exit 0', async (t) => {
|
|
132
|
+
test('monitor: inert sentinel snapshot -> exit 0', needsV2Tree, async (t) => {
|
|
134
133
|
const r = await runCLI(['monitor', '--once', '--json'], env({ PAYLOAD_SENTINEL: '1' }));
|
|
135
134
|
t.assert.strictEqual(r.code, 0);
|
|
136
135
|
assertEnvelope(t, r.json, 'monitor');
|
|
@@ -138,7 +137,7 @@ test('monitor: inert sentinel snapshot -> exit 0', async (t) => {
|
|
|
138
137
|
t.assert.ok(r.json.data.snapshot, 'snapshot present');
|
|
139
138
|
});
|
|
140
139
|
|
|
141
|
-
test('status: exit 0, all subsystems reported', async (t) => {
|
|
140
|
+
test('status: exit 0, all subsystems reported', needsV2Tree, async (t) => {
|
|
142
141
|
const r = await runCLI(['status', '--json'], env());
|
|
143
142
|
t.assert.strictEqual(r.code, 0);
|
|
144
143
|
assertEnvelope(t, r.json, 'status');
|
|
@@ -151,7 +150,7 @@ test('status: exit 0, all subsystems reported', async (t) => {
|
|
|
151
150
|
t.assert.strictEqual(subs.doctor.flag, 'PAYLOAD_MCP_DOCTOR');
|
|
152
151
|
});
|
|
153
152
|
|
|
154
|
-
test('preflight: disabled -> exit 3; enabled -> exit 0 with report', async (t) => {
|
|
153
|
+
test('preflight: disabled -> exit 3; enabled -> exit 0 with report', needsV2Tree, async (t) => {
|
|
155
154
|
const off = await runCLI(['preflight', '--json'], env());
|
|
156
155
|
t.assert.strictEqual(off.code, 3);
|
|
157
156
|
assertEnvelope(t, off.json, 'preflight');
|
|
@@ -163,7 +162,7 @@ test('preflight: disabled -> exit 3; enabled -> exit 0 with report', async (t) =
|
|
|
163
162
|
t.assert.ok(on.json.data.report, 'preflight report present');
|
|
164
163
|
});
|
|
165
164
|
|
|
166
|
-
test('inspect: capability stats without query -> exit 0', async (t) => {
|
|
165
|
+
test('inspect: capability stats without query -> exit 0', needsV2Tree, async (t) => {
|
|
167
166
|
const r = await runCLI(['inspect', '--json'], env({ PAYLOAD_MCP_FABRIC: '1' }));
|
|
168
167
|
t.assert.strictEqual(r.code, 0);
|
|
169
168
|
assertEnvelope(t, r.json, 'inspect');
|
|
@@ -205,7 +204,7 @@ test('unknown command -> exit 2 and lists valid commands', async (t) => {
|
|
|
205
204
|
}
|
|
206
205
|
});
|
|
207
206
|
|
|
208
|
-
test('malformed input: bad JSON evidence -> exit 2 with clear message', async (t) => {
|
|
207
|
+
test('malformed input: bad JSON evidence -> exit 2 with clear message', needsV2Tree, async (t) => {
|
|
209
208
|
const r = await runCLI(['resolve', '--evidence', '{not-json', '--json'], env({ PAYLOAD_SETTLEMENT_RESOLVER: '1' }));
|
|
210
209
|
t.assert.strictEqual(r.code, 2);
|
|
211
210
|
assertEnvelope(t, r.json, 'resolve');
|
|
@@ -230,7 +229,8 @@ test('malformed input: bad flag values -> exit 2 with clear message', async (t)
|
|
|
230
229
|
test('--version prints version and exits 0', async (t) => {
|
|
231
230
|
const r = await runCLI(['--version'], env());
|
|
232
231
|
t.assert.strictEqual(r.code, 0);
|
|
233
|
-
|
|
232
|
+
const pkg = require('../package.json');
|
|
233
|
+
t.assert.strictEqual(r.stdout.trim(), `callx402 ${pkg.version}`);
|
|
234
234
|
});
|
|
235
235
|
|
|
236
236
|
test('--help exits 0 and documents commands', async (t) => {
|
|
@@ -240,6 +240,25 @@ test('--help exits 0 and documents commands', async (t) => {
|
|
|
240
240
|
t.assert.match(r.stdout, /Exit codes: 0 ok/);
|
|
241
241
|
});
|
|
242
242
|
|
|
243
|
+
test('<command> --help prints that command usage, not the global help', async (t) => {
|
|
244
|
+
const r = await runCLI(['recover', '--help'], env());
|
|
245
|
+
t.assert.strictEqual(r.code, 0);
|
|
246
|
+
t.assert.match(r.stdout, /callx402 recover/);
|
|
247
|
+
t.assert.match(r.stdout, /PAYLOAD_VEYLINE_RECOVERY=1/);
|
|
248
|
+
t.assert.doesNotMatch(r.stdout, /callx402 <command>/);
|
|
249
|
+
const r2 = await runCLI(['resolve', '--help'], env());
|
|
250
|
+
t.assert.strictEqual(r2.code, 0);
|
|
251
|
+
t.assert.match(r2.stdout, /settlement UNKNOWN, exit 5/);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('rail-only paid action as command -> exit 2 with routing hint', async (t) => {
|
|
255
|
+
const r = await runCLI(['safe_retry'], env());
|
|
256
|
+
t.assert.strictEqual(r.code, 2);
|
|
257
|
+
t.assert.match(r.stderr, /unknown command 'safe_retry'/);
|
|
258
|
+
t.assert.match(r.stderr, /paid on-demand rail action, not a CLI command/);
|
|
259
|
+
t.assert.match(r.stderr, /problem-map\.md/);
|
|
260
|
+
});
|
|
261
|
+
|
|
243
262
|
test('human-readable (non-JSON) output is concise', async (t) => {
|
|
244
263
|
const r = await runCLI(['status'], env());
|
|
245
264
|
t.assert.strictEqual(r.code, 0);
|
package/test/helpers.js
CHANGED
|
@@ -40,7 +40,10 @@ function cliEnv(scratch, extra = {}) {
|
|
|
40
40
|
CALLX402_CONFIG: path.join(scratch, 'config.json'),
|
|
41
41
|
CALLX402_IDEMPOTENCY_FILE: path.join(scratch, 'idempotency.jsonl'),
|
|
42
42
|
// Hermetic default: local mode unless the test explicitly asks for remote.
|
|
43
|
+
// CALLX402_MODE=local is the legacy remote-server toggle; CALLX402_LOCAL=1
|
|
44
|
+
// forces the v2.0.0 runtime tree path (new default is the hosted rail).
|
|
43
45
|
CALLX402_MODE: 'local',
|
|
46
|
+
CALLX402_LOCAL: '1',
|
|
44
47
|
...extra,
|
|
45
48
|
};
|
|
46
49
|
}
|
|
@@ -101,3 +104,20 @@ async function withEnv(vars, fn) {
|
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
module.exports = { ROOT, CLI, V2_ROOT, ENVELOPE_KEYS, scratchDir, cliEnv, runCLI, assertEnvelope, withEnv };
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* v2 tree availability gate. The v2.0.0 runtime tree is the optional advanced
|
|
110
|
+
* self-hosted mode, not a requirement. Tests that exercise the local subsystem
|
|
111
|
+
* path skip (not fail) when the tree is absent — on a clean machine the CLI
|
|
112
|
+
* routes to the hosted rail by default instead.
|
|
113
|
+
*/
|
|
114
|
+
function v2TreeAvailable() {
|
|
115
|
+
try {
|
|
116
|
+
require('node:fs').accessSync(require('node:path').join(V2_ROOT, 'lib', 'mcp', 'doctor.js'));
|
|
117
|
+
return true;
|
|
118
|
+
} catch { return false; }
|
|
119
|
+
}
|
|
120
|
+
const needsV2Tree = { skip: !v2TreeAvailable() };
|
|
121
|
+
|
|
122
|
+
module.exports.v2TreeAvailable = v2TreeAvailable;
|
|
123
|
+
module.exports.needsV2Tree = needsV2Tree;
|
package/test/integrity.test.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const { test } = require('node:test');
|
|
13
|
+
const { needsV2Tree } = require('./helpers');
|
|
13
14
|
const fs = require('node:fs');
|
|
14
15
|
const path = require('node:path');
|
|
15
16
|
|
|
@@ -25,7 +26,7 @@ function walk(dir, out) {
|
|
|
25
26
|
return out;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
test('zero files changed under the v2.0.0 tree', (t) => {
|
|
29
|
+
test('zero files changed under the v2.0.0 tree', needsV2Tree, (t) => {
|
|
29
30
|
t.assert.ok(fs.existsSync(MARKER), 'integrity marker exists (created before the suite ran)');
|
|
30
31
|
t.assert.ok(fs.existsSync(V2_ROOT), 'v2.0.0 tree exists');
|
|
31
32
|
const markerMs = fs.statSync(MARKER).mtimeMs;
|
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
const { test } = require('node:test');
|
|
17
17
|
const path = require('node:path');
|
|
18
18
|
const { runIntent, runAction } = require('../core/index.js');
|
|
19
|
-
const { scratchDir, withEnv } = require('./helpers');
|
|
20
|
-
|
|
19
|
+
const { scratchDir, withEnv, needsV2Tree } = require('./helpers');
|
|
21
20
|
const scratch = scratchDir('pipeline');
|
|
22
21
|
const hermetic = {
|
|
23
22
|
CALLX402_CONFIG: path.join(scratch, 'config.json'),
|
|
@@ -169,7 +168,7 @@ test('networks/assets/providers constrain planner, router and spendguard', async
|
|
|
169
168
|
});
|
|
170
169
|
});
|
|
171
170
|
|
|
172
|
-
test('real end-to-end run (no doubles, all flags off): honest no-route', async (t) => {
|
|
171
|
+
test('real end-to-end run (no doubles, all flags off): honest no-route', needsV2Tree, async (t) => {
|
|
173
172
|
await withEnv(hermetic, async () => {
|
|
174
173
|
const { result, exitCode } = await runIntent('summarize this for under 1 dollar', { maxBudget: 1 });
|
|
175
174
|
t.assert.strictEqual(exitCode, 0);
|
package/test/remote.test.js
CHANGED
|
@@ -14,8 +14,7 @@ const { test } = require('node:test');
|
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
const { runAction, runIntent } = require('../core/index.js');
|
|
16
16
|
const { createServer } = require('../server/index.js');
|
|
17
|
-
const { scratchDir, cliEnv, runCLI, assertEnvelope, withEnv } = require('./helpers');
|
|
18
|
-
|
|
17
|
+
const { scratchDir, cliEnv, runCLI, assertEnvelope, withEnv, needsV2Tree } = require('./helpers');
|
|
19
18
|
const scratch = scratchDir('remote');
|
|
20
19
|
|
|
21
20
|
/** Start a real callx402 server on an ephemeral port. */
|
|
@@ -148,7 +147,7 @@ test('remote auth: server with token rejects missing/wrong bearer, accepts the r
|
|
|
148
147
|
}
|
|
149
148
|
});
|
|
150
149
|
|
|
151
|
-
test('remote money-safety: settlement UNKNOWN envelope crosses the wire unchanged (exit 5)', async (t) => {
|
|
150
|
+
test('remote money-safety: settlement UNKNOWN envelope crosses the wire unchanged (exit 5)', needsV2Tree, async (t) => {
|
|
152
151
|
// The server runs in-process, so the subsystem flag belongs to the parent
|
|
153
152
|
// env; the CLI subprocess only carries the remote-mode config.
|
|
154
153
|
await withEnv({ PAYLOAD_SETTLEMENT_RESOLVER: '1' }, async () => {
|
package/test/safety.test.js
CHANGED
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
const { test } = require('node:test');
|
|
16
16
|
const path = require('node:path');
|
|
17
17
|
const { runIntent, runAction } = require('../core/index.js');
|
|
18
|
-
const { scratchDir, cliEnv, runCLI, assertEnvelope, withEnv } = require('./helpers');
|
|
19
|
-
|
|
18
|
+
const { scratchDir, cliEnv, runCLI, assertEnvelope, withEnv, needsV2Tree } = require('./helpers');
|
|
20
19
|
const scratch = scratchDir('safety');
|
|
21
20
|
const hermetic = {
|
|
22
21
|
CALLX402_CONFIG: path.join(scratch, 'config.json'),
|
|
@@ -99,7 +98,7 @@ function walkPairs(o, prefix = '') {
|
|
|
99
98
|
return out;
|
|
100
99
|
}
|
|
101
100
|
|
|
102
|
-
test('settlement UNKNOWN: exit 5, fail-closed, no retry/repay instruction', async (t) => {
|
|
101
|
+
test('settlement UNKNOWN: exit 5, fail-closed, no retry/repay instruction', needsV2Tree, async (t) => {
|
|
103
102
|
const r = await runCLI(
|
|
104
103
|
['resolve', '--evidence', '{}', '--json'],
|
|
105
104
|
cliEnv(scratch, { PAYLOAD_SETTLEMENT_RESOLVER: '1' })
|
|
@@ -164,7 +163,7 @@ test('unsafe retry refusal: stored UNKNOWN + --force-retry -> refused, exit 5',
|
|
|
164
163
|
});
|
|
165
164
|
});
|
|
166
165
|
|
|
167
|
-
test('unsafe retry refusal via real CLI + file-backed store', async (t) => {
|
|
166
|
+
test('unsafe retry refusal via real CLI + file-backed store', needsV2Tree, async (t) => {
|
|
168
167
|
const idemFile = path.join(scratch, 'idem-unsafe.jsonl');
|
|
169
168
|
const env = cliEnv(scratch, { CALLX402_IDEMPOTENCY_FILE: idemFile });
|
|
170
169
|
const seeded = {
|
|
@@ -222,7 +221,7 @@ test('timeout: slow subsystem + tiny timeoutMs -> timeout error, exit 1, no hang
|
|
|
222
221
|
});
|
|
223
222
|
});
|
|
224
223
|
|
|
225
|
-
test('timeout via CLI --timeout: monitor --watch is cut off -> exit 1', async (t) => {
|
|
224
|
+
test('timeout via CLI --timeout: monitor --watch is cut off -> exit 1', needsV2Tree, async (t) => {
|
|
226
225
|
// --watch streams forever; --timeout 200ms must win the race and exit 1.
|
|
227
226
|
const r = await runCLI(
|
|
228
227
|
['monitor', '--watch', '--interval', '50', '--timeout', '200'],
|
package/test/sdk.test.js
CHANGED
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
const { test } = require('node:test');
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
const { execFile } = require('node:child_process');
|
|
16
|
-
const { ENVELOPE_KEYS, scratchDir, withEnv } = require('./helpers');
|
|
17
|
-
|
|
16
|
+
const { ENVELOPE_KEYS, scratchDir, withEnv, needsV2Tree } = require('./helpers');
|
|
18
17
|
const scratch = scratchDir('sdk');
|
|
19
18
|
const hermetic = {
|
|
20
19
|
CALLX402_CONFIG: path.join(scratch, 'config.json'),
|
|
@@ -45,7 +44,7 @@ test('JS SDK: callx402({intent}) returns a full envelope (in-process)', async (t
|
|
|
45
44
|
});
|
|
46
45
|
});
|
|
47
46
|
|
|
48
|
-
test('JS SDK: version, status(), and sub-actions', async (t) => {
|
|
47
|
+
test('JS SDK: version, status(), and sub-actions', needsV2Tree, async (t) => {
|
|
49
48
|
await withEnv(hermetic, async () => {
|
|
50
49
|
t.assert.strictEqual(sdk.version, '1.0.0');
|
|
51
50
|
t.assert.strictEqual(require('../sdk/js/index.js').callx402.version, '1.0.0');
|
|
@@ -74,7 +73,7 @@ test('JS SDK: invalid opts throw TypeError (fail fast, no dispatch)', async (t)
|
|
|
74
73
|
});
|
|
75
74
|
});
|
|
76
75
|
|
|
77
|
-
test('JS SDK: action dispatch through runAction', async (t) => {
|
|
76
|
+
test('JS SDK: action dispatch through runAction', needsV2Tree, async (t) => {
|
|
78
77
|
await withEnv(hermetic, async () => {
|
|
79
78
|
const r = await sdk({ action: 'resolve', args: { evidence: '{}' } });
|
|
80
79
|
// Settlement subsystem disabled without its flag: honest failure.
|
package/test/server.test.js
CHANGED
|
@@ -15,8 +15,7 @@ const { test } = require('node:test');
|
|
|
15
15
|
const http = require('node:http');
|
|
16
16
|
const path = require('node:path');
|
|
17
17
|
const { createServer } = require('../server/index.js');
|
|
18
|
-
const { ENVELOPE_KEYS, scratchDir, withEnv } = require('./helpers');
|
|
19
|
-
|
|
18
|
+
const { ENVELOPE_KEYS, scratchDir, withEnv, needsV2Tree } = require('./helpers');
|
|
20
19
|
// The in-process server's core reads the parent env: keep the idempotency
|
|
21
20
|
// store hermetic for the whole file (each test file is its own process).
|
|
22
21
|
const serverScratch = scratchDir('server');
|
|
@@ -59,7 +58,7 @@ function assertEnvelopeShape(t, env, action) {
|
|
|
59
58
|
if (action !== undefined) t.assert.strictEqual(env.action, action);
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
test('GET /health -> 200 with version and subsystem map', async (t) => {
|
|
61
|
+
test('GET /health -> 200 with version and subsystem map', needsV2Tree, async (t) => {
|
|
63
62
|
const { server, port } = await startServer();
|
|
64
63
|
try {
|
|
65
64
|
const r = await request(port, 'GET', '/health');
|
|
@@ -134,7 +133,7 @@ test('unknown route -> 404', async (t) => {
|
|
|
134
133
|
}
|
|
135
134
|
});
|
|
136
135
|
|
|
137
|
-
test('disabled subsystem over HTTP -> 503, honest envelope', async (t) => {
|
|
136
|
+
test('disabled subsystem over HTTP -> 503, honest envelope', needsV2Tree, async (t) => {
|
|
138
137
|
const { server, port } = await startServer();
|
|
139
138
|
try {
|
|
140
139
|
const r = await request(port, 'POST', '/resolve', { body: { evidence: {} } });
|
|
@@ -147,7 +146,7 @@ test('disabled subsystem over HTTP -> 503, honest envelope', async (t) => {
|
|
|
147
146
|
}
|
|
148
147
|
});
|
|
149
148
|
|
|
150
|
-
test('settlement UNKNOWN over HTTP -> 409 with fail-closed envelope', async (t) => {
|
|
149
|
+
test('settlement UNKNOWN over HTTP -> 409 with fail-closed envelope', needsV2Tree, async (t) => {
|
|
151
150
|
await withEnv({ PAYLOAD_SETTLEMENT_RESOLVER: '1' }, async () => {
|
|
152
151
|
const { server, port } = await startServer();
|
|
153
152
|
try {
|