@codespar/cli 0.13.0 → 0.14.1
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 +51 -1
- package/dist/commands/agent.js +13 -7
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/init.js +91 -23
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.js +2 -1
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/mandate-verify.js +75 -27
- package/dist/commands/mandate-verify.js.map +1 -1
- package/dist/config.js +17 -1
- package/dist/config.js.map +1 -1
- package/dist/did.js +262 -41
- package/dist/did.js.map +1 -1
- package/dist/index.js +23 -8
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/templates/bills-agent/LICENSE +21 -0
- package/templates/bills-agent/README.md +21 -0
- package/templates/bills-agent/_gitignore +10 -0
- package/templates/bills-agent/agents/bills-agent/.env.example +7 -0
- package/templates/bills-agent/agents/bills-agent/AGENTS.md +16 -0
- package/templates/bills-agent/agents/bills-agent/CLAUDE.md +16 -0
- package/templates/bills-agent/agents/bills-agent/README.md +89 -0
- package/templates/bills-agent/agents/bills-agent/SYSTEM_PROMPT.md +23 -0
- package/templates/bills-agent/agents/bills-agent/agent.yaml +26 -0
- package/templates/bills-agent/agents/bills-agent/channels/terminal/index.ts +108 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/beneficiary-swap-mandate.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/beneficiary-swap.json +10 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/exfiltration.json +13 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/false-authority.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/false-authority.transcript.jsonl +3 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/fractioning.json +15 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/fractioning.transcript.jsonl +6 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/model-total.json +10 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/model-total.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/prompt-injection.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/prompt-injection.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/evals/adversarial/webhook-replay.json +9 -0
- package/templates/bills-agent/agents/bills-agent/evals/eval.yaml +9 -0
- package/templates/bills-agent/agents/bills-agent/guardrails.json +15 -0
- package/templates/bills-agent/agents/bills-agent/mandate.example.json +46 -0
- package/templates/bills-agent/agents/bills-agent/package.json +24 -0
- package/templates/bills-agent/agents/bills-agent/runbook.md +19 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/beneficiary-not-allowed.json +14 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/cap-exceeded.json +12 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/cap-exceeded.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/escalated-above-threshold.json +20 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/escalated-above-threshold.transcript.jsonl +7 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/happy-path.json +14 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/happy-path.transcript.jsonl +3 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/mandate-revoked.json +21 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/mandate-revoked.transcript.jsonl +9 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/prompt-injection.json +12 -0
- package/templates/bills-agent/agents/bills-agent/scenarios/prompt-injection.transcript.jsonl +2 -0
- package/templates/bills-agent/agents/bills-agent/src/adversarial.ts +177 -0
- package/templates/bills-agent/agents/bills-agent/src/bills.ts +28 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/approve.ts +9 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/check.ts +13 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/consent.ts +26 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/decide.ts +45 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/deny.ts +7 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/eval.ts +37 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/reconcile.ts +38 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/rerun.ts +65 -0
- package/templates/bills-agent/agents/bills-agent/src/commands/resume.ts +30 -0
- package/templates/bills-agent/agents/bills-agent/src/main.ts +185 -0
- package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +125 -0
- package/templates/bills-agent/agents/bills-agent/src/modules/pix-out.ts +73 -0
- package/templates/bills-agent/agents/bills-agent/src/scenarios.ts +196 -0
- package/templates/bills-agent/agents/bills-agent/src/setup.ts +207 -0
- package/templates/bills-agent/agents/bills-agent/test/adversarial.test.ts +40 -0
- package/templates/bills-agent/agents/bills-agent/test/check.test.ts +85 -0
- package/templates/bills-agent/agents/bills-agent/test/cli.test.ts +177 -0
- package/templates/bills-agent/agents/bills-agent/test/scenarios.test.ts +73 -0
- package/templates/bills-agent/agents/bills-agent/tools.json +51 -0
- package/templates/bills-agent/agents/bills-agent/tsconfig.json +11 -0
- package/templates/bills-agent/package.json +34 -0
- package/templates/bills-agent/packages/agent-core/README.md +19 -0
- package/templates/bills-agent/packages/agent-core/package.json +24 -0
- package/templates/bills-agent/packages/agent-core/src/agent.ts +125 -0
- package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +154 -0
- package/templates/bills-agent/packages/agent-core/src/api/client.ts +53 -0
- package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +58 -0
- package/templates/bills-agent/packages/agent-core/src/api/rail.ts +101 -0
- package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +45 -0
- package/templates/bills-agent/packages/agent-core/src/approval.ts +109 -0
- package/templates/bills-agent/packages/agent-core/src/bundle.ts +113 -0
- package/templates/bills-agent/packages/agent-core/src/check.ts +171 -0
- package/templates/bills-agent/packages/agent-core/src/engine.ts +784 -0
- package/templates/bills-agent/packages/agent-core/src/escalate.ts +85 -0
- package/templates/bills-agent/packages/agent-core/src/events.ts +40 -0
- package/templates/bills-agent/packages/agent-core/src/guardrails.ts +45 -0
- package/templates/bills-agent/packages/agent-core/src/hash.ts +49 -0
- package/templates/bills-agent/packages/agent-core/src/ids.ts +10 -0
- package/templates/bills-agent/packages/agent-core/src/index.ts +29 -0
- package/templates/bills-agent/packages/agent-core/src/mandate.ts +100 -0
- package/templates/bills-agent/packages/agent-core/src/manifest.ts +76 -0
- package/templates/bills-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
- package/templates/bills-agent/packages/agent-core/src/providers/replay.ts +63 -0
- package/templates/bills-agent/packages/agent-core/src/providers/types.ts +39 -0
- package/templates/bills-agent/packages/agent-core/src/rail.ts +87 -0
- package/templates/bills-agent/packages/agent-core/src/revocation.ts +30 -0
- package/templates/bills-agent/packages/agent-core/src/secrets.ts +30 -0
- package/templates/bills-agent/packages/agent-core/src/state/store.ts +307 -0
- package/templates/bills-agent/packages/agent-core/src/state-machine.ts +146 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/charge-rail.ts +193 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +57 -0
- package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +124 -0
- package/templates/bills-agent/packages/agent-core/src/tools.ts +55 -0
- package/templates/bills-agent/packages/agent-core/src/types.ts +106 -0
- package/templates/bills-agent/packages/agent-core/test/approval.test.ts +83 -0
- package/templates/bills-agent/packages/agent-core/test/engine.test.ts +524 -0
- package/templates/bills-agent/packages/agent-core/test/escalate.test.ts +42 -0
- package/templates/bills-agent/packages/agent-core/test/helpers.ts +139 -0
- package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +153 -0
- package/templates/bills-agent/packages/agent-core/test/manifest.test.ts +45 -0
- package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +265 -0
- package/templates/bills-agent/packages/agent-core/test/secrets.test.ts +28 -0
- package/templates/bills-agent/packages/agent-core/test/state-machine.test.ts +67 -0
- package/templates/bills-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
- package/templates/bills-agent/packages/agent-core/tsconfig.json +10 -0
- package/templates/bills-agent/tsconfig.base.json +16 -0
- package/templates/bills-agent/vitest.config.ts +12 -0
- package/templates/collections-agent/LICENSE +21 -0
- package/templates/collections-agent/README.md +20 -0
- package/templates/collections-agent/_gitignore +10 -0
- package/templates/collections-agent/agents/collections-agent/.env.example +4 -0
- package/templates/collections-agent/agents/collections-agent/AGENTS.md +19 -0
- package/templates/collections-agent/agents/collections-agent/CLAUDE.md +19 -0
- package/templates/collections-agent/agents/collections-agent/README.md +94 -0
- package/templates/collections-agent/agents/collections-agent/SYSTEM_PROMPT.md +31 -0
- package/templates/collections-agent/agents/collections-agent/agent.yaml +28 -0
- package/templates/collections-agent/agents/collections-agent/channels/terminal/index.ts +178 -0
- package/templates/collections-agent/agents/collections-agent/channels/webhook/index.ts +131 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/beneficiary-swap-mandate.json +11 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/beneficiary-swap.json +12 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/exfiltration.json +19 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/false-authority.json +11 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/false-authority.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/fractioning.json +12 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/fractioning.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/model-total.json +13 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/model-total.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/prompt-injection.json +12 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/prompt-injection.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/evals/adversarial/webhook-replay.json +11 -0
- package/templates/collections-agent/agents/collections-agent/evals/eval.yaml +11 -0
- package/templates/collections-agent/agents/collections-agent/guardrails.json +20 -0
- package/templates/collections-agent/agents/collections-agent/mandate.example.json +40 -0
- package/templates/collections-agent/agents/collections-agent/package.json +29 -0
- package/templates/collections-agent/agents/collections-agent/runbook.md +27 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/beneficiary-not-allowed.json +42 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +3 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/cap-exceeded.json +27 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/cap-exceeded.transcript.jsonl +3 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/charge-expired.json +36 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/charge-expired.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/escalated-above-threshold.json +49 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/escalated-above-threshold.transcript.jsonl +5 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/happy-path.json +60 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/happy-path.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/instalments.json +49 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/instalments.transcript.jsonl +4 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/mandate-revoked.json +42 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/mandate-revoked.transcript.jsonl +6 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/prompt-injection.json +27 -0
- package/templates/collections-agent/agents/collections-agent/scenarios/prompt-injection.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/src/adversarial.ts +190 -0
- package/templates/collections-agent/agents/collections-agent/src/agreements.ts +45 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/approve.ts +10 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/check.ts +13 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/decide.ts +48 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/deny.ts +7 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/eval.ts +39 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/poll.ts +35 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/reconcile.ts +38 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/rerun.ts +55 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/resume.ts +30 -0
- package/templates/collections-agent/agents/collections-agent/src/commands/webhook.ts +36 -0
- package/templates/collections-agent/agents/collections-agent/src/envelope.ts +103 -0
- package/templates/collections-agent/agents/collections-agent/src/main.ts +193 -0
- package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +105 -0
- package/templates/collections-agent/agents/collections-agent/src/poll.ts +94 -0
- package/templates/collections-agent/agents/collections-agent/src/scenarios.ts +252 -0
- package/templates/collections-agent/agents/collections-agent/src/setup.ts +250 -0
- package/templates/collections-agent/agents/collections-agent/test/adversarial.test.ts +45 -0
- package/templates/collections-agent/agents/collections-agent/test/check.test.ts +51 -0
- package/templates/collections-agent/agents/collections-agent/test/cli.test.ts +172 -0
- package/templates/collections-agent/agents/collections-agent/test/envelope.test.ts +53 -0
- package/templates/collections-agent/agents/collections-agent/test/fixtures/accept-1042.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/test/fixtures/accept-1103.transcript.jsonl +2 -0
- package/templates/collections-agent/agents/collections-agent/test/scenarios.test.ts +95 -0
- package/templates/collections-agent/agents/collections-agent/test/webhook.test.ts +100 -0
- package/templates/collections-agent/agents/collections-agent/tools.json +40 -0
- package/templates/collections-agent/agents/collections-agent/tsconfig.json +11 -0
- package/templates/collections-agent/package.json +35 -0
- package/templates/collections-agent/packages/agent-core/README.md +19 -0
- package/templates/collections-agent/packages/agent-core/package.json +24 -0
- package/templates/collections-agent/packages/agent-core/src/agent.ts +125 -0
- package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +154 -0
- package/templates/collections-agent/packages/agent-core/src/api/client.ts +53 -0
- package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +58 -0
- package/templates/collections-agent/packages/agent-core/src/api/rail.ts +101 -0
- package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +45 -0
- package/templates/collections-agent/packages/agent-core/src/approval.ts +109 -0
- package/templates/collections-agent/packages/agent-core/src/bundle.ts +113 -0
- package/templates/collections-agent/packages/agent-core/src/check.ts +171 -0
- package/templates/collections-agent/packages/agent-core/src/engine.ts +784 -0
- package/templates/collections-agent/packages/agent-core/src/escalate.ts +85 -0
- package/templates/collections-agent/packages/agent-core/src/events.ts +40 -0
- package/templates/collections-agent/packages/agent-core/src/guardrails.ts +45 -0
- package/templates/collections-agent/packages/agent-core/src/hash.ts +49 -0
- package/templates/collections-agent/packages/agent-core/src/ids.ts +10 -0
- package/templates/collections-agent/packages/agent-core/src/index.ts +29 -0
- package/templates/collections-agent/packages/agent-core/src/mandate.ts +100 -0
- package/templates/collections-agent/packages/agent-core/src/manifest.ts +76 -0
- package/templates/collections-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
- package/templates/collections-agent/packages/agent-core/src/providers/replay.ts +63 -0
- package/templates/collections-agent/packages/agent-core/src/providers/types.ts +39 -0
- package/templates/collections-agent/packages/agent-core/src/rail.ts +87 -0
- package/templates/collections-agent/packages/agent-core/src/revocation.ts +30 -0
- package/templates/collections-agent/packages/agent-core/src/secrets.ts +30 -0
- package/templates/collections-agent/packages/agent-core/src/state/store.ts +307 -0
- package/templates/collections-agent/packages/agent-core/src/state-machine.ts +146 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/charge-rail.ts +193 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +57 -0
- package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +124 -0
- package/templates/collections-agent/packages/agent-core/src/tools.ts +55 -0
- package/templates/collections-agent/packages/agent-core/src/types.ts +106 -0
- package/templates/collections-agent/packages/agent-core/test/approval.test.ts +83 -0
- package/templates/collections-agent/packages/agent-core/test/engine.test.ts +524 -0
- package/templates/collections-agent/packages/agent-core/test/escalate.test.ts +42 -0
- package/templates/collections-agent/packages/agent-core/test/helpers.ts +139 -0
- package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +153 -0
- package/templates/collections-agent/packages/agent-core/test/manifest.test.ts +45 -0
- package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +265 -0
- package/templates/collections-agent/packages/agent-core/test/secrets.test.ts +28 -0
- package/templates/collections-agent/packages/agent-core/test/state-machine.test.ts +67 -0
- package/templates/collections-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
- package/templates/collections-agent/packages/agent-core/tsconfig.json +10 -0
- package/templates/collections-agent/tsconfig.base.json +16 -0
- package/templates/collections-agent/vitest.config.ts +12 -0
- package/templates/kits.lock.json +41 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contracts that need a real process: `--json` on stdout and nothing
|
|
3
|
+
* else, the live-key refusal, the restart after the issuance followed by
|
|
4
|
+
* `resume` and `poll`, `rerun`, `approve`/`deny`, and `check`.
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { DatabaseSync } from "node:sqlite";
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
|
|
13
|
+
const AGENT_DIR = resolve(import.meta.dirname, "..");
|
|
14
|
+
const NODE = process.execPath;
|
|
15
|
+
const HAPPY = "oi, recebi a mensagem sobre o acordo do pedido 1042";
|
|
16
|
+
|
|
17
|
+
function run(script: string, args: string[], env: Record<string, string>) {
|
|
18
|
+
const result = spawnSync(NODE, ["--disable-warning=ExperimentalWarning", "--import", "tsx", script, ...args], {
|
|
19
|
+
cwd: AGENT_DIR,
|
|
20
|
+
env: { ...process.env, ANTHROPIC_API_KEY: "", CODESPAR_API_KEY: "", ...env },
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
timeout: 60_000,
|
|
23
|
+
});
|
|
24
|
+
return { code: result.status, stdout: result.stdout, stderr: result.stderr };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function scratch(label: string): Record<string, string> {
|
|
28
|
+
const stateDir = mkdtempSync(join(tmpdir(), `collections-${label}-`));
|
|
29
|
+
return { COLLECTIONS_STATE_DIR: stateDir, COLLECTIONS_RUNS_DIR: join(stateDir, "runs") };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("npm start -- --input ... --json", () => {
|
|
33
|
+
it("prints valid JSON on stdout and nothing else; the QR and the people go to stderr", () => {
|
|
34
|
+
const env = scratch("json");
|
|
35
|
+
// The recorded happy-path's first turn is a proposal, no execution: the second turn issues.
|
|
36
|
+
const out = run("src/main.ts", ["--input", HAPPY, "--json"], env);
|
|
37
|
+
expect(out.code).toBe(0);
|
|
38
|
+
const lines = out.stdout.split("\n").filter(Boolean);
|
|
39
|
+
expect(lines).toHaveLength(1);
|
|
40
|
+
const payload = JSON.parse(lines[0]!) as { run_id: string; mode: string; actor: { type: string }; executions: unknown[]; tool_calls: Array<{ name: string }> };
|
|
41
|
+
expect(payload.mode).toBe("human");
|
|
42
|
+
expect(payload.actor.type).toBe("agent");
|
|
43
|
+
expect(payload.executions).toEqual([]);
|
|
44
|
+
expect(payload.tool_calls.map((c) => c.name)).toEqual(["list_agreements"]);
|
|
45
|
+
expect(out.stderr).toContain("replay");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("an accepted proposal, approved with the payer simulated, settles in one process and the QR never reaches stdout", () => {
|
|
49
|
+
const env = scratch("settle");
|
|
50
|
+
const out = run("src/main.ts", ["--input", "fechado, pago à vista", "--transcript", "test/fixtures/accept-1042.transcript.jsonl", "--approve", "--simulate-payer", "--json"], env);
|
|
51
|
+
expect(out.code).toBe(0);
|
|
52
|
+
const lines = out.stdout.split("\n").filter(Boolean);
|
|
53
|
+
expect(lines).toHaveLength(1);
|
|
54
|
+
const payload = JSON.parse(lines[0]!) as { executions: Array<{ state: string; charges: Array<{ charge_id: string; status: string }>; receipt_ids: string[] }>; receipts: string[] };
|
|
55
|
+
expect(payload.executions.map((e) => e.state)).toEqual(["settled"]);
|
|
56
|
+
expect(payload.executions[0]?.charges).toHaveLength(1);
|
|
57
|
+
expect(payload.receipts).toHaveLength(1);
|
|
58
|
+
expect(out.stderr).toContain("copia e cola");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("refuses a key outside csk_test_ before anything else", () => {
|
|
62
|
+
const env = scratch("live");
|
|
63
|
+
const out = run("src/main.ts", ["--input", HAPPY, "--json"], { ...env, CODESPAR_API_KEY: ["csk", "live", "0000000000"].join("_") });
|
|
64
|
+
expect(out.code).toBe(1);
|
|
65
|
+
expect(out.stdout).toBe("");
|
|
66
|
+
expect(out.stderr).toContain("csk_test_");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("section 10: restart after the issuance, then resume and poll", () => {
|
|
71
|
+
it("one receivable, one settlement, never two", () => {
|
|
72
|
+
const env = scratch("restart");
|
|
73
|
+
|
|
74
|
+
const killed = run("src/main.ts", ["--input", "fechado, pago à vista", "--transcript", "test/fixtures/accept-1042.transcript.jsonl", "--approve", "--json"], { ...env, COLLECTIONS_KILL_AFTER_DISPATCH: "1" });
|
|
75
|
+
expect(killed.code).toBe(137);
|
|
76
|
+
expect(killed.stdout).toBe("");
|
|
77
|
+
|
|
78
|
+
const db = new DatabaseSync(join(env["COLLECTIONS_STATE_DIR"]!, "state.db"));
|
|
79
|
+
expect(db.prepare("SELECT state FROM executions").all()).toEqual([{ state: "executing" }]);
|
|
80
|
+
expect((db.prepare("SELECT COUNT(*) AS n FROM stub_rail_attempts").get() as { n: number }).n).toBe(1);
|
|
81
|
+
db.close();
|
|
82
|
+
|
|
83
|
+
// resume: the outbox is `sent`, so nothing is re-issued; the receivable is looked at and found registered, waiting for its payer.
|
|
84
|
+
const resumed = run("src/commands/resume.ts", ["--json"], env);
|
|
85
|
+
expect(resumed.code).toBe(0);
|
|
86
|
+
const r = JSON.parse(resumed.stdout.trim()) as { resumed: Array<{ state: string; reason: string | null; charge_ids: string[] }> };
|
|
87
|
+
expect(r.resumed).toHaveLength(1);
|
|
88
|
+
expect(r.resumed[0]).toMatchObject({ state: "executing", reason: "awaiting_settlement" });
|
|
89
|
+
expect(r.resumed[0]?.charge_ids[0]).toMatch(/^chg_stub_/);
|
|
90
|
+
|
|
91
|
+
// poll: the fixture payer pays on the next look; the record is fetched; the payer is told once.
|
|
92
|
+
const polled = run("src/commands/poll.ts", ["--json"], env);
|
|
93
|
+
expect(polled.code).toBe(0);
|
|
94
|
+
const p = JSON.parse(polled.stdout.trim()) as { polled: Array<{ state: string; timed_out: boolean }> };
|
|
95
|
+
expect(p.polled).toEqual([expect.objectContaining({ state: "settled", timed_out: false })]);
|
|
96
|
+
|
|
97
|
+
const db2 = new DatabaseSync(join(env["COLLECTIONS_STATE_DIR"]!, "state.db"));
|
|
98
|
+
expect((db2.prepare("SELECT COUNT(*) AS n FROM stub_rail_attempts").get() as { n: number }).n).toBe(1);
|
|
99
|
+
expect((db2.prepare("SELECT COUNT(*) AS n FROM events WHERE type = 'commerce.charge.paid'").get() as { n: number }).n).toBe(1);
|
|
100
|
+
expect((db2.prepare("SELECT COUNT(*) AS n FROM events WHERE type = 'message.debtor'").get() as { n: number }).n).toBe(1);
|
|
101
|
+
expect(db2.prepare("SELECT state FROM executions").all()).toEqual([{ state: "settled" }]);
|
|
102
|
+
db2.close();
|
|
103
|
+
|
|
104
|
+
const runsDir = env["COLLECTIONS_RUNS_DIR"]!;
|
|
105
|
+
const receipts = readdirSync(runsDir).flatMap((d) => (existsSync(join(runsDir, d, "receipts")) ? readdirSync(join(runsDir, d, "receipts")) : []));
|
|
106
|
+
expect(receipts).toHaveLength(1);
|
|
107
|
+
|
|
108
|
+
const again = run("src/commands/poll.ts", ["--json"], env);
|
|
109
|
+
expect(JSON.parse(again.stdout.trim())).toEqual({ polled: [] });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("npm run rerun <run-id>", () => {
|
|
114
|
+
it("reproduces a recorded run without network, with the same state sequence, including an expired receivable", () => {
|
|
115
|
+
const env = scratch("rerun");
|
|
116
|
+
// No --simulate-payer: the fixture payer lets it expire at the second look; the recorded run is a failed one.
|
|
117
|
+
const first = run("src/main.ts", ["--input", "pode emitir", "--transcript", "test/fixtures/accept-1103.transcript.jsonl", "--approve", "--json"], { ...env, COLLECTIONS_STUB_PAYER: "expires" });
|
|
118
|
+
expect(first.code).toBe(0);
|
|
119
|
+
const payload = JSON.parse(first.stdout.trim()) as { run_id: string; executions: Array<{ state: string; reason: string | null }> };
|
|
120
|
+
expect(payload.executions[0]).toMatchObject({ state: "failed", reason: "charge_expired" });
|
|
121
|
+
const rerun = run("src/commands/rerun.ts", [payload.run_id, "--json"], env);
|
|
122
|
+
expect(rerun.code).toBe(0);
|
|
123
|
+
const r = JSON.parse(rerun.stdout.trim()) as { same_states: boolean; original: string[]; rerun: string[] };
|
|
124
|
+
expect(r.same_states).toBe(true);
|
|
125
|
+
expect(r.original).toEqual(["awaiting_approval", "approved", "executing", "failed"]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("npm run check", () => {
|
|
130
|
+
it("is green for the shipped agent and prints JSON with --json", () => {
|
|
131
|
+
const out = run("src/commands/check.ts", ["--json"], {});
|
|
132
|
+
expect(out.code).toBe(0);
|
|
133
|
+
const report = JSON.parse(out.stdout.trim()) as { ok: boolean; agent: string; findings: unknown[] };
|
|
134
|
+
expect(report).toMatchObject({ ok: true, agent: "collections-agent" });
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("npm run approve / deny <execution-id>", () => {
|
|
139
|
+
it("decides an execution left awaiting, produces the artifact, issues, and waits for the payer", () => {
|
|
140
|
+
const env = scratch("decide");
|
|
141
|
+
const left = run("src/main.ts", ["--input", "fechado, pago à vista", "--transcript", "test/fixtures/accept-1042.transcript.jsonl", "--json"], env);
|
|
142
|
+
expect(left.code).toBe(0);
|
|
143
|
+
const payload = JSON.parse(left.stdout.trim()) as { executions: Array<{ id: string; state: string }> };
|
|
144
|
+
expect(payload.executions[0]?.state).toBe("awaiting_approval");
|
|
145
|
+
const id = payload.executions[0]!.id;
|
|
146
|
+
|
|
147
|
+
const approved = run("src/commands/approve.ts", [id, "--user", "usr_gerente", "--simulate-payer", "--json"], env);
|
|
148
|
+
expect(approved.code).toBe(0);
|
|
149
|
+
const out = JSON.parse(approved.stdout.trim()) as { state: string; approval_id: string | null; charge_ids: string[]; receipt_ids: string[]; bundle_dir: string };
|
|
150
|
+
expect(out.state).toBe("settled");
|
|
151
|
+
expect(out.approval_id).toMatch(/^apr_/);
|
|
152
|
+
expect(out.charge_ids).toHaveLength(1);
|
|
153
|
+
expect(out.receipt_ids).toHaveLength(1);
|
|
154
|
+
const artifacts = JSON.parse(readFileSync(join(AGENT_DIR, out.bundle_dir, "approval.json"), "utf8")) as Array<{ approver: { type: string; id: string } }>;
|
|
155
|
+
expect(artifacts[0]?.approver).toEqual({ type: "person", id: "usr_gerente", channel: "terminal" });
|
|
156
|
+
|
|
157
|
+
const again = run("src/commands/approve.ts", [id, "--json"], env);
|
|
158
|
+
expect(again.code).toBe(1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("deny leaves a terminal denied execution and issues nothing", () => {
|
|
162
|
+
const env = scratch("deny");
|
|
163
|
+
const left = run("src/main.ts", ["--input", "fechado, pago à vista", "--transcript", "test/fixtures/accept-1042.transcript.jsonl", "--json"], env);
|
|
164
|
+
const id = (JSON.parse(left.stdout.trim()) as { executions: Array<{ id: string }> }).executions[0]!.id;
|
|
165
|
+
const denied = run("src/commands/deny.ts", [id, "--json"], env);
|
|
166
|
+
expect(denied.code).toBe(0);
|
|
167
|
+
expect(JSON.parse(denied.stdout.trim())).toMatchObject({ state: "denied", reason: "denied_by_approver", charge_ids: [] });
|
|
168
|
+
const db = new DatabaseSync(join(env["COLLECTIONS_STATE_DIR"]!, "state.db"));
|
|
169
|
+
expect((db.prepare("SELECT COUNT(*) AS n FROM stub_rail_attempts").get() as { n: number }).n).toBe(0);
|
|
170
|
+
db.close();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { ExecutionItem } from "@codespar/agent-core";
|
|
3
|
+
import { AGREEMENTS } from "../src/agreements.js";
|
|
4
|
+
import { checkEnvelope, floorMinor, type Envelope } from "../src/envelope.js";
|
|
5
|
+
|
|
6
|
+
const envelope: Envelope = { max_discount_pct: 15, max_instalments: 3, due_date_window_days: 90, min_instalment_minor: 5000, collection_hours: "08:00-20:00" };
|
|
7
|
+
const NOW = new Date("2026-09-23T18:00:00Z"); // 15:00 in Sao Paulo
|
|
8
|
+
const TZ = "America/Sao_Paulo";
|
|
9
|
+
const joana = AGREEMENTS[0]!;
|
|
10
|
+
|
|
11
|
+
function items(list: Array<{ amount: number; due_date?: string; payee?: string }>): Pick<{ items: ExecutionItem[]; total: number }, "items" | "total"> {
|
|
12
|
+
const out = list.map((i) => ({ beneficiary: "x", payee: i.payee ?? joana.document, amount: i.amount, currency: "BRL", ...(i.due_date ? { due_date: i.due_date } : {}) }));
|
|
13
|
+
return { items: out, total: out.reduce((s, i) => s + i.amount, 0) };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("the negotiation envelope is deterministic code", () => {
|
|
17
|
+
it("the floor is the principal minus the maximum discount, rounded up", () => {
|
|
18
|
+
expect(floorMinor(joana, envelope)).toBe(102000);
|
|
19
|
+
expect(floorMinor({ ...joana, principal_minor: 100001 }, envelope)).toBe(85001);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("inside: in full with the maximum discount, or three instalments inside the window", () => {
|
|
23
|
+
expect(checkEnvelope(items([{ amount: 102000, due_date: "2026-09-30" }]), envelope, NOW, TZ)).toBeUndefined();
|
|
24
|
+
expect(checkEnvelope(items([{ amount: 40000, due_date: "2026-09-30" }, { amount: 40000, due_date: "2026-10-30" }, { amount: 40000, due_date: "2026-11-30" }]), envelope, NOW, TZ)).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("outside: discount above the ceiling, more than the principal, too many instalments, a tiny instalment", () => {
|
|
28
|
+
expect(checkEnvelope(items([{ amount: 101999, due_date: "2026-09-30" }]), envelope, NOW, TZ)?.reason).toBe("outside_envelope");
|
|
29
|
+
expect(checkEnvelope(items([{ amount: 120001, due_date: "2026-09-30" }]), envelope, NOW, TZ)?.detail).toContain("acima do principal");
|
|
30
|
+
expect(checkEnvelope(items([{ amount: 30000, due_date: "2026-09-30" }, { amount: 30000, due_date: "2026-10-30" }, { amount: 30000, due_date: "2026-11-30" }, { amount: 30000, due_date: "2026-12-15" }]), envelope, NOW, TZ)?.detail).toContain("parcelas");
|
|
31
|
+
expect(checkEnvelope(items([{ amount: 118000, due_date: "2026-09-30" }, { amount: 2000, due_date: "2026-10-30" }]), envelope, NOW, TZ)?.detail).toContain("minimo");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("due dates: in the past, beyond the window, out of order, missing", () => {
|
|
35
|
+
expect(checkEnvelope(items([{ amount: 120000, due_date: "2026-09-22" }]), envelope, NOW, TZ)?.detail).toContain("ja passou");
|
|
36
|
+
expect(checkEnvelope(items([{ amount: 120000, due_date: "2026-12-23" }]), envelope, NOW, TZ)?.detail).toContain("fora da janela");
|
|
37
|
+
expect(checkEnvelope(items([{ amount: 120000, due_date: "2026-12-22" }]), envelope, NOW, TZ)).toBeUndefined();
|
|
38
|
+
expect(checkEnvelope(items([{ amount: 60000, due_date: "2026-10-30" }, { amount: 60000, due_date: "2026-09-30" }]), envelope, NOW, TZ)?.detail).toContain("antes da anterior");
|
|
39
|
+
expect(checkEnvelope(items([{ amount: 120000 }]), envelope, NOW, TZ)?.detail).toContain("sem vencimento");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("collection hours are refused in both modes, whatever the proposal", () => {
|
|
43
|
+
const night = new Date("2026-09-24T01:00:00Z"); // 22:00 in Sao Paulo
|
|
44
|
+
const verdict = checkEnvelope(items([{ amount: 120000, due_date: "2026-09-30" }]), envelope, night, TZ);
|
|
45
|
+
expect(verdict?.reason).toBe("outside_hours");
|
|
46
|
+
const morning = new Date("2026-09-24T11:00:00Z"); // 08:00 in Sao Paulo, the window opens
|
|
47
|
+
expect(checkEnvelope(items([{ amount: 120000, due_date: "2026-09-30" }]), envelope, morning, TZ)).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("one execution, one agreement", () => {
|
|
51
|
+
expect(checkEnvelope(items([{ amount: 60000, due_date: "2026-09-30" }, { amount: 60000, due_date: "2026-10-30", payee: AGREEMENTS[1]!.document }]), envelope, NOW, TZ)?.detail).toContain("more than one debtor");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"kind": "assistant_step", "tool_calls": [{"id": "tc_1", "name": "codespar_charge", "input": {"action": "create", "agreement": "acordo-1042", "instalments": [{"amount_minor": 108000, "due_date": "2026-09-30"}], "total_minor": 108000}}]}
|
|
2
|
+
{"kind": "assistant_step", "reply": "Recebemos, acordo quitado. Obrigada, Joana!"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"kind": "assistant_step", "tool_calls": [{"id": "tc_1", "name": "codespar_charge", "input": {"action": "create", "agreement": "acordo-1103", "instalments": [{"amount_minor": 40500, "due_date": "2026-09-25"}], "total_minor": 40500}}]}
|
|
2
|
+
{"kind": "assistant_step", "reply": "A cobranca venceu sem pagamento. Se quiser, emito uma nova."}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { AGREEMENTS } from "../src/agreements.js";
|
|
6
|
+
import { checkScenario, listScenarios, loadScenario, runScenario, type ScenarioRun } from "../src/scenarios.js";
|
|
7
|
+
|
|
8
|
+
const runsDir = mkdtempSync(join(tmpdir(), "collections-runs-"));
|
|
9
|
+
const DOCUMENTS = AGREEMENTS.map((a) => a.document);
|
|
10
|
+
|
|
11
|
+
describe("section 12: scenario packs, on the replay provider and the stub rail", () => {
|
|
12
|
+
const required = ["happy-path", "charge-expired", "prompt-injection", "cap-exceeded", "beneficiary-not-allowed", "escalated-above-threshold", "mandate-revoked", "instalments"];
|
|
13
|
+
|
|
14
|
+
it("ships every scenario the spec asks for, plus instalments", () => {
|
|
15
|
+
for (const name of required) expect(listScenarios()).toContain(name);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
for (const name of required) {
|
|
19
|
+
const scenario = loadScenario(name);
|
|
20
|
+
for (const mode of scenario.modes) {
|
|
21
|
+
it(`${name} [${mode}] ends in the states the pack declares`, async () => {
|
|
22
|
+
const run = await runScenario(scenario, { mode, runsDir });
|
|
23
|
+
const check = checkScenario(scenario, run);
|
|
24
|
+
expect(check.failures).toEqual([]);
|
|
25
|
+
expect(check.ok).toBe(true);
|
|
26
|
+
// Every event of the bundle carries an actor; no event carries a debtor's document in the clear.
|
|
27
|
+
const raw = readFileSync(join(run.bundle_dir, "events.jsonl"), "utf8");
|
|
28
|
+
const events = raw.split("\n").filter(Boolean).map((l) => JSON.parse(l) as { actor?: unknown; type: string });
|
|
29
|
+
expect(events.length).toBeGreaterThan(0);
|
|
30
|
+
for (const e of events) expect(e.actor).toBeDefined();
|
|
31
|
+
// Every record carries an actor and no raw document; the snapshot masks the book.
|
|
32
|
+
for (const file of readdirSync(join(run.bundle_dir, "receipts"))) {
|
|
33
|
+
const receipt = JSON.parse(readFileSync(join(run.bundle_dir, "receipts", file), "utf8")) as { actor?: unknown; kind?: string; chain: string | null; payment: { payee: string | null; money_moved: boolean; sandbox: boolean } };
|
|
34
|
+
expect(receipt.actor).toBeDefined();
|
|
35
|
+
expect(receipt.kind).toBe("charge");
|
|
36
|
+
expect(receipt.chain).toBeNull();
|
|
37
|
+
expect(receipt.payment.money_moved).toBe(false);
|
|
38
|
+
expect(receipt.payment.sandbox).toBe(true);
|
|
39
|
+
expect(receipt.payment.payee ?? "").toContain("***");
|
|
40
|
+
}
|
|
41
|
+
const snapshot = readFileSync(join(run.bundle_dir, "mandate.snapshot.json"), "utf8");
|
|
42
|
+
for (const doc of DOCUMENTS) expect(snapshot).not.toContain(doc);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
it("happy-path: the same records in human and in mandate; the person decided in human, the envelope in mandate", async () => {
|
|
48
|
+
const scenario = loadScenario("happy-path");
|
|
49
|
+
const human = await runScenario(scenario, { mode: "human", runsDir });
|
|
50
|
+
const mandate = await runScenario(scenario, { mode: "mandate", runsDir });
|
|
51
|
+
expect(normalizeReceipts(human)).toEqual(normalizeReceipts(mandate));
|
|
52
|
+
const approvals = (run: ScenarioRun) => JSON.parse(readFileSync(join(run.bundle_dir, "approval.json"), "utf8")) as Array<{ approver: { type: string }; items_hash: string; items: Array<{ due_date?: string }> }>;
|
|
53
|
+
expect(approvals(human)[0]?.approver.type).toBe("person");
|
|
54
|
+
expect(approvals(mandate)[0]?.approver.type).toBe("mandate");
|
|
55
|
+
expect(approvals(human)[0]?.items_hash).toBe(approvals(mandate)[0]?.items_hash);
|
|
56
|
+
expect(approvals(human)[0]?.items[0]?.due_date).toBe("2026-09-30");
|
|
57
|
+
// The payer saw the instrument once and was told once, in each mode.
|
|
58
|
+
for (const run of [human, mandate]) {
|
|
59
|
+
const events = readFileSync(join(run.bundle_dir, "events.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l) as { type: string; payload?: { payable?: boolean } });
|
|
60
|
+
expect(events.filter((e) => e.type === "charge.instrument" && e.payload?.payable === true)).toHaveLength(1);
|
|
61
|
+
expect(events.filter((e) => e.type === "message.debtor")).toHaveLength(1);
|
|
62
|
+
expect(events.filter((e) => e.type === "commerce.charge.paid")).toHaveLength(1);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("nothing reaches executing without an approval artifact carrying its items_hash", async () => {
|
|
67
|
+
for (const name of required) {
|
|
68
|
+
const scenario = loadScenario(name);
|
|
69
|
+
for (const mode of scenario.modes) {
|
|
70
|
+
const run = await runScenario(scenario, { mode, runsDir });
|
|
71
|
+
const approvalPath = join(run.bundle_dir, "approval.json");
|
|
72
|
+
const artifacts = existsSync(approvalPath) ? (JSON.parse(readFileSync(approvalPath, "utf8")) as Array<{ execution_id: string; items_hash: string }>) : [];
|
|
73
|
+
for (const e of run.executions) {
|
|
74
|
+
if (!e.trail.includes("executing")) continue;
|
|
75
|
+
expect(artifacts.some((a) => a.execution_id === e.id)).toBe(true);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("instalments: three receivables, three idempotency keys, one execution, one message", async () => {
|
|
82
|
+
const run = await runScenario(loadScenario("instalments"), { mode: "human", runsDir });
|
|
83
|
+
expect(run.executions).toHaveLength(1);
|
|
84
|
+
expect(new Set(run.executions[0]!.charge_ids).size).toBe(3);
|
|
85
|
+
expect(run.charges_issued).toBe(3);
|
|
86
|
+
expect(run.debtor_messages).toBe(1);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
function normalizeReceipts(run: ScenarioRun): unknown[] {
|
|
91
|
+
return readdirSync(join(run.bundle_dir, "receipts"))
|
|
92
|
+
.map((f) => JSON.parse(readFileSync(join(run.bundle_dir, "receipts", f), "utf8")) as { state: string; kind?: string; payment: { amount_minor: number; payee: string | null; sandbox: boolean } })
|
|
93
|
+
.map((r) => ({ state: r.state, kind: r.kind, amount: r.payment.amount_minor, payee: r.payment.payee, sandbox: r.payment.sandbox }))
|
|
94
|
+
.sort((a, b) => a.amount - b.amount);
|
|
95
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The webhook channel closes the cycle from a trigger delivery: signature
|
|
3
|
+
* verified when a secret is set, duplicates dropped, out-of-order ignored,
|
|
4
|
+
* the payer told once, the paid record fetched.
|
|
5
|
+
*/
|
|
6
|
+
import { createHmac } from "node:crypto";
|
|
7
|
+
import { mkdtempSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { announceOutcome } from "../channels/terminal/index.js";
|
|
12
|
+
import { createWebhookHandler, verifySignature } from "../channels/webhook/index.js";
|
|
13
|
+
import { RUNS_DIR, setup } from "../src/setup.js";
|
|
14
|
+
|
|
15
|
+
function issued(mode: "human" | "mandate" = "mandate") {
|
|
16
|
+
let clock = new Date("2026-09-23T18:00:00Z");
|
|
17
|
+
const s = setup({ mode, rail: "stub", provider: "replay", transcript: "unused", stateDir: mkdtempSync(join(tmpdir(), "collections-webhook-")), runsDir: mkdtempSync(join(tmpdir(), "collections-webhook-runs-")), now: () => (clock = new Date(clock.getTime() + 1000)), say: () => undefined });
|
|
18
|
+
s.payer.behave("never");
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function delivery(id: string, type: string, chargeId: string): string {
|
|
23
|
+
return JSON.stringify({ id, type, source: "celcoin", occurred_at: "2026-09-23T18:00:30.000Z", data: { payment_id: chargeId, amount_minor: 108000, currency: "BRL", simulated: true, settled_against: "sandbox_fixture" } });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sign(secret: string, body: string, t: number): string {
|
|
27
|
+
return `t=${t},v1=${createHmac("sha256", secret).update(`${t}.${body}`, "utf8").digest("hex")}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("channels/webhook", () => {
|
|
31
|
+
it("paid twice, created late, expired after paid: one settled, one message, one record", async () => {
|
|
32
|
+
const s = issued();
|
|
33
|
+
try {
|
|
34
|
+
const d = await s.engine.draft({ items: [{ payee: "acordo-1042", amount: 108000, due_date: "2026-09-30" }] });
|
|
35
|
+
if (!d.ok) throw new Error("refused");
|
|
36
|
+
const e = await s.engine.execute(d.execution.id);
|
|
37
|
+
expect(e.state).toBe("executing");
|
|
38
|
+
const chargeId = e.outcomes[0]!.transaction_id!;
|
|
39
|
+
const told: string[] = [];
|
|
40
|
+
const handle = createWebhookHandler({ engine: s.engine, onClosed: (x) => void announceOutcome(x, s, (l) => told.push(l)) });
|
|
41
|
+
expect(await handle({}, delivery("evt_1", "commerce.charge.paid", chargeId))).toEqual({ status: 200, body: { applied: true, reason: "settled" } });
|
|
42
|
+
expect(await handle({}, delivery("evt_1", "commerce.charge.paid", chargeId))).toEqual({ status: 200, body: { applied: false, reason: "duplicate event id" } });
|
|
43
|
+
expect(await handle({}, delivery("evt_2", "commerce.charge.created", chargeId))).toMatchObject({ status: 200, body: { applied: false } });
|
|
44
|
+
expect(await handle({}, delivery("evt_3", "commerce.charge.expired", chargeId))).toMatchObject({ status: 200, body: { applied: false, reason: "execution already settled" } });
|
|
45
|
+
expect(await handle({}, delivery("evt_4", "commerce.charge.paid", "chg_not_ours"))).toMatchObject({ status: 200, body: { applied: false, reason: "unknown attempt" } });
|
|
46
|
+
expect(await handle({}, "{not json")).toMatchObject({ status: 400 });
|
|
47
|
+
expect(s.engine.get(e.id)!.state).toBe("settled");
|
|
48
|
+
expect(told).toHaveLength(1);
|
|
49
|
+
expect(told[0]).toContain("acordo quitado");
|
|
50
|
+
expect(s.bundle.listReceipts()).toEqual([`${chargeId}.json`]);
|
|
51
|
+
expect(s.store.listEvents({ run_id: s.runId }).filter((ev) => ev.type === "message.debtor")).toHaveLength(1);
|
|
52
|
+
} finally {
|
|
53
|
+
s.close();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("with a trigger secret: a missing, stale or wrong signature is 401 and moves nothing", async () => {
|
|
58
|
+
const s = issued();
|
|
59
|
+
try {
|
|
60
|
+
const d = await s.engine.draft({ items: [{ payee: "acordo-1042", amount: 108000, due_date: "2026-09-30" }] });
|
|
61
|
+
if (!d.ok) throw new Error("refused");
|
|
62
|
+
const e = await s.engine.execute(d.execution.id);
|
|
63
|
+
const chargeId = e.outcomes[0]!.transaction_id!;
|
|
64
|
+
const now = new Date("2026-09-23T18:01:00Z");
|
|
65
|
+
const handle = createWebhookHandler({ engine: s.engine, secret: "whsec_test", clock: () => now });
|
|
66
|
+
const body = delivery("evt_s1", "commerce.charge.paid", chargeId);
|
|
67
|
+
expect((await handle({}, body)).status).toBe(401);
|
|
68
|
+
expect((await handle({ "x-codespar-signature": sign("whsec_other", body, Math.floor(now.getTime() / 1000)) }, body)).status).toBe(401);
|
|
69
|
+
expect((await handle({ "x-codespar-signature": sign("whsec_test", body, Math.floor(now.getTime() / 1000) - 3600) }, body)).status).toBe(401);
|
|
70
|
+
expect(s.engine.get(e.id)!.state).toBe("executing");
|
|
71
|
+
expect(await handle({ "x-codespar-signature": sign("whsec_test", body, Math.floor(now.getTime() / 1000)) }, body)).toEqual({ status: 200, body: { applied: true, reason: "settled" } });
|
|
72
|
+
expect(verifySignature("whsec_test", "garbage", body, now, 300)).toMatchObject({ ok: false });
|
|
73
|
+
} finally {
|
|
74
|
+
s.close();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("expired by webhook closes failed with charge_expired and tells the payer once", async () => {
|
|
79
|
+
const s = issued("human");
|
|
80
|
+
try {
|
|
81
|
+
const d = await s.engine.draft({ items: [{ payee: "acordo-1103", amount: 40500, due_date: "2026-09-25" }] });
|
|
82
|
+
if (!d.ok) throw new Error("refused");
|
|
83
|
+
const approved = s.engine.approve(d.execution.id, { id: "usr_operator", channel: "terminal" });
|
|
84
|
+
const e = await s.engine.execute(approved.id);
|
|
85
|
+
const chargeId = e.outcomes[0]!.transaction_id!;
|
|
86
|
+
const told: string[] = [];
|
|
87
|
+
const handle = createWebhookHandler({ engine: s.engine, onClosed: (x) => void announceOutcome(x, s, (l) => told.push(l)) });
|
|
88
|
+
expect(await handle({}, delivery("evt_x1", "commerce.charge.expired", chargeId))).toEqual({ status: 200, body: { applied: true, reason: "failed" } });
|
|
89
|
+
expect(await handle({}, delivery("evt_x2", "commerce.charge.paid", chargeId))).toMatchObject({ body: { applied: false } });
|
|
90
|
+
expect(s.engine.get(e.id)).toMatchObject({ state: "failed", reason: "charge_expired" });
|
|
91
|
+
expect(told).toHaveLength(1);
|
|
92
|
+
expect(told[0]).toContain("venceu");
|
|
93
|
+
expect(s.bundle.listReceipts()).toEqual([]);
|
|
94
|
+
} finally {
|
|
95
|
+
s.close();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
void RUNS_DIR;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta_tools": [
|
|
3
|
+
{
|
|
4
|
+
"name": "codespar_charge",
|
|
5
|
+
"effect": "charge",
|
|
6
|
+
"description": "Propose the receivable(s) of an agreement the payer accepted: one instalment per bolepix (cobranca com vencimento: the payer settles by Pix or by boleto). This does NOT issue anything: it creates an execution in `drafted` that the code checks against the collection policy and the negotiation envelope (discount ceiling, instalment count, due-date window, collection hours) and, depending on the approval mode, asks the operator or issues. The code builds the real call (`POST /v1/charges`, `method: boleto`, the debtor's name and document from the agreement, `idempotency_key` per instalment) and presents the QR and the copy-and-paste; you never see or pass a document. action=status reads back the state of a receivable this run issued.",
|
|
7
|
+
"input_schema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"action": { "type": "string", "enum": ["create", "status"], "description": "create (the default) proposes the receivables; status reads one back." },
|
|
11
|
+
"agreement": { "type": "string", "description": "The agreement alias from list_agreements (acordo-1042). Never a name, a document or a Pix key typed in the chat." },
|
|
12
|
+
"instalments": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"minItems": 1,
|
|
15
|
+
"description": "One entry per instalment, in order. A single entry is a payment in full.",
|
|
16
|
+
"items": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"amount_minor": { "type": "integer", "minimum": 1, "description": "BRL cents." },
|
|
20
|
+
"due_date": { "type": "string", "description": "YYYY-MM-DD. Inside the envelope's window." }
|
|
21
|
+
},
|
|
22
|
+
"required": ["amount_minor", "due_date"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"total_minor": { "type": "integer", "description": "Optional. What you believe the total is. The code recomputes it and yours is only recorded." },
|
|
26
|
+
"execution_id": { "type": "string", "description": "action=status: the execution to read." }
|
|
27
|
+
},
|
|
28
|
+
"required": ["action"]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"local_tools": [
|
|
33
|
+
{
|
|
34
|
+
"name": "list_agreements",
|
|
35
|
+
"effect": "read",
|
|
36
|
+
"description": "The open agreements this merchant collects: alias, debtor's first name, principal in BRL cents, origin, and the envelope you may negotiate inside (maximum discount, maximum instalments, due-date window). Read-only. Documents are not returned.",
|
|
37
|
+
"input_schema": { "type": "object", "properties": {} }
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "The merchant's agent that collects: agrees terms with the payer inside a negotiation envelope, issues one bolepix per instalment with an idempotency key, presents the QR and the copy-and-paste in the conversation, and closes the cycle on commerce.charge.paid or commerce.charge.expired.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"workspaces": [
|
|
9
|
+
"packages/agent-core",
|
|
10
|
+
"agents/collections-agent"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "npm start --workspace=agents/collections-agent --",
|
|
14
|
+
"check": "npm run check --workspaces --if-present --",
|
|
15
|
+
"test": "vitest run --pool=forks --maxWorkers=1",
|
|
16
|
+
"typecheck": "tsc --noEmit -p packages/agent-core/tsconfig.json && tsc --noEmit -p agents/collections-agent/tsconfig.json",
|
|
17
|
+
"approve": "npm run approve --workspace=agents/collections-agent --",
|
|
18
|
+
"deny": "npm run deny --workspace=agents/collections-agent --",
|
|
19
|
+
"eval": "npm run eval --workspace=agents/collections-agent --",
|
|
20
|
+
"poll": "npm run poll --workspace=agents/collections-agent --",
|
|
21
|
+
"reconcile": "npm run reconcile --workspace=agents/collections-agent --",
|
|
22
|
+
"rerun": "npm run rerun --workspace=agents/collections-agent --",
|
|
23
|
+
"resume": "npm run resume --workspace=agents/collections-agent --",
|
|
24
|
+
"webhook": "npm run webhook --workspace=agents/collections-agent --"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "22.20.4",
|
|
28
|
+
"tsx": "4.20.5",
|
|
29
|
+
"typescript": "5.9.2",
|
|
30
|
+
"vitest": "3.2.4"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.13"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @codespar/agent-core
|
|
2
|
+
|
|
3
|
+
The primitives every CodeSpar starter-kit agent inherits (spec v5.1.1, section 4): the execution state machine, the approval artifact, the `agent.yaml` manifest, `escalate_above`, `actor`, local state in SQLite, the proof bundle, the rail contract and the two providers.
|
|
4
|
+
|
|
5
|
+
## The state machine, and what the types do and do not prove
|
|
6
|
+
|
|
7
|
+
`transition(execution, to, input)` accepts only a `to` that the table in `state-machine.ts` lists for `execution.state`. On a **narrowed** value (`Execution<"drafted">`, `Execution<"executing">`, ...) a transition outside the table is a compile error, and `test/transitions.type-test.ts` keeps that true with `@ts-expect-error` lines that `tsc` refuses to leave unused. An **un-narrowed** `Execution` (the union, which is what a row read back from SQLite is) compiles for any target; the engine narrows with casts at its call sites. So the guard that always holds is the runtime one: `transition()` throws `IllegalTransitionError` on any pair the table does not list, whatever the static type said. The type test proves the table is closed; the runtime check enforces it.
|
|
8
|
+
|
|
9
|
+
## The one policy, three gates
|
|
10
|
+
|
|
11
|
+
`ExecutionEngine.policy()` runs at draft, at a human's approval and again immediately before `executing`, inside the transaction that writes the outbox row. Mandate status, allowlist, per-transaction cap, the window cap (counting settled, in flight AND awaiting executions as reservations), `escalate_above` and the artifact's `items_hash` are all re-checked at the last gate. A person's approval satisfies `escalate_above` and nothing else.
|
|
12
|
+
|
|
13
|
+
## Reconcile never re-dispatches
|
|
14
|
+
|
|
15
|
+
An execution left in `executing` is looked up on the rail, attempt by attempt. A recorded settled or failed outcome closes it; `in_flight`, `uncertain` or unknown leaves it in `executing` with `reason: rail_uncertain` for a human. The single exception is an outbox row still `pending` (the state changed, no call was ever made): those attempts go out once, under the same ids.
|
|
16
|
+
|
|
17
|
+
## Stubs
|
|
18
|
+
|
|
19
|
+
`stubs/mandate-status.ts` (the section 4.7 status source for runs without a key: revocation, pause and the organization kill switch, over the local state.db) and `stubs/rail.ts` (the sandbox rail) are stand-ins for the CI, the scenarios and `rerun`, marked as such. With a test key the engine reads the mandate status from the API instead (`api/mandate-status.ts`, `GET /v1/mandates/{id}`), fail-closed: anything but `active` refuses `executing`, and a read that does not answer is `mandate_status_unavailable`, never "assume active". The approval artifact is signed with a local development key (`approval.ts`); the API does not sign approval lists.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codespar/agent-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The primitives every CodeSpar starter-kit agent inherits: the execution state machine, the approval artifact, the agent.yaml manifest, escalate_above, actor, local state and providers.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.ts",
|
|
11
|
+
"./providers/anthropic": "./src/providers/anthropic.ts",
|
|
12
|
+
"./providers/replay": "./src/providers/replay.ts"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
16
|
+
"test": "vitest run --pool=forks --maxWorkers=1 --root ../.. packages/agent-core"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@anthropic-ai/sdk": "0.128.0",
|
|
20
|
+
"@codespar/sdk": "0.16.4",
|
|
21
|
+
"yaml": "2.9.1",
|
|
22
|
+
"zod": "3.25.76"
|
|
23
|
+
}
|
|
24
|
+
}
|