Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/types/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codespar/types",
"version": "0.10.11",
"version": "0.10.12",
"description": "Shared session interface contract for codespar runtimes",
"type": "module",
"main": "./dist/index.js",
Expand Down
4 changes: 4 additions & 0 deletions packages/types/src/testing/demo-scenarios-consistency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import type { DemoScenario } from "./demo-scenario.js";
import { SERVICE_INVOICE_SCENARIO } from "./demo-scenarios/service-invoice.js";
import { INSTALLMENT_NEGOTIATION_SCENARIO } from "./demo-scenarios/installment-negotiation.js";
import { PAYMENT_REJECTION_SCENARIO } from "./demo-scenarios/payment-rejection.js";
import { CUSTOMER_DATA_REJECTION_SCENARIO } from "./demo-scenarios/customer-data-rejection.js";
import { MERCHANT_BLOCKED_SCENARIO } from "./demo-scenarios/merchant-blocked.js";
import { SHARED_META_TOOL_DEFINITIONS } from "../meta-tool-definitions.js";
import type { SharedMetaToolDefinition } from "../meta-tool-definitions.js";

Expand All @@ -28,6 +30,8 @@ const ALL_SCENARIOS: DemoScenario[] = [
SERVICE_INVOICE_SCENARIO,
INSTALLMENT_NEGOTIATION_SCENARIO,
PAYMENT_REJECTION_SCENARIO,
CUSTOMER_DATA_REJECTION_SCENARIO,
MERCHANT_BLOCKED_SCENARIO,
];

const META_TOOL_NAME = /^codespar_[a-z_]+$/;
Expand Down
145 changes: 145 additions & 0 deletions packages/types/src/testing/demo-scenarios/customer-data-rejection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import type { DemoScenario } from "../demo-scenario.js";

/* Payment-failure triage — customer-data category, at the meta-tool abstraction.
*
* The agent attempts a Pix payment; `codespar_pay` returns a *successful* result
* whose business outcome is a decline with `category: "customer_data"` and
* `error_code: "INVALID_CPF_CNPJ"`. The irreplaceable judgment is reading that
* category: a customer-data problem is recoverable by asking the customer for a
* correction, so the agent notifies the customer (`codespar_notify`), and on the
* corrected turn retries `codespar_pay` (now confirmed) and issues the NF-e
* (`codespar_invoice`). No human escalation — the customer can fix the input.
*
* The agent never picks a provider: `codespar_pay` owns provider routing
* internally. What the agent routes is the *remediation* (ask the customer vs.
* escalate to a human), which is exactly the category-reading judgment a fixed
* lookup table gets wrong when a new rejection code shows up in production.
*
* The first `codespar_pay` mock is a SUCCESSFUL dispatch whose payload is a
* decline — a rejected payment is a successful API call returning a declined
* result, so the meta-tool trace still reports `status: "success"`. The agent
* reads the declined output and drives the correction.
*/
export const CUSTOMER_DATA_REJECTION_SCENARIO: DemoScenario = {
name: "customer-data-rejection",
servers: ["asaas", "nuvem-fiscal", "z-api"],
mocks: {
// Stateful array: first call is declined for invalid CPF/CNPJ, the retry
// with the corrected document succeeds.
codespar_pay: [
{
status: "rejected",
category: "customer_data",
error_code: "INVALID_CPF_CNPJ",
reason: "Documento (CPF/CNPJ) do pagador inválido ou não encontrado",
},
{ id: "pay_demo_triage_001", status: "confirmed", method: "pix" },
],
codespar_notify: { messageId: "msg_demo_triage_001", sent: true },
codespar_invoice: {
id: "nfe_demo_triage_001",
status: "autorizada",
pdf_url: "https://example.test/nfe_demo_triage_001.pdf",
},
},
turns: [
{
message:
"Quero pagar R$ 2.500 via Pix do meu pedido. Meu CPF é 111.111.111-11.",
expectMetaTools: ["codespar_pay", "codespar_notify"],
},
{
message: "Foi mal, o CPF correto é 390.533.447-05.",
expectMetaTools: ["codespar_pay", "codespar_invoice"],
},
],
aimockFixtures: {
fixtures: [
{
match: { userMessage: "R$ 2.500", turnIndex: 0, hasToolResult: false },
response: {
toolCalls: [
{
name: "codespar_pay",
arguments: {
amount: 250000,
currency: "BRL",
method: "pix",
description: "Pagamento do pedido via Pix",
consumer_id: "111.111.111-11",
},
},
],
finishReason: "tool_calls",
},
},
{
match: { userMessage: "R$ 2.500", turnIndex: 1, hasToolResult: true },
response: {
toolCalls: [
{
name: "codespar_notify",
arguments: {
channel: "whatsapp",
to: "+5511987654321",
message:
"O CPF informado é inválido, então não consegui concluir o pagamento. Pode me enviar o CPF correto que eu refaço a cobrança?",
},
},
],
finishReason: "tool_calls",
},
},
{
match: { userMessage: "R$ 2.500", turnIndex: 2, hasToolResult: true },
response: {
content:
"O CPF informado não passou na validação. Me envia o CPF correto que eu refaço o pagamento na hora.",
finishReason: "stop",
},
},
{
match: { userMessage: "CPF correto", turnIndex: 0, hasToolResult: false },
response: {
toolCalls: [
{
name: "codespar_pay",
arguments: {
amount: 250000,
currency: "BRL",
method: "pix",
description: "Pagamento do pedido via Pix (CPF corrigido)",
consumer_id: "390.533.447-05",
},
},
],
finishReason: "tool_calls",
},
},
{
match: { userMessage: "CPF correto", turnIndex: 1, hasToolResult: true },
response: {
toolCalls: [
{
name: "codespar_invoice",
arguments: {
type: "nfe",
recipient: { name: "Cliente Demo", document: "39053344705" },
items: [{ description: "Pedido pago via Pix", amount: 2500.0 }],
},
},
],
finishReason: "tool_calls",
},
},
{
match: { userMessage: "CPF correto", turnIndex: 2, hasToolResult: true },
response: {
content:
"Prontinho! Pagamento aprovado com o CPF corrigido e NF-e emitida. Qualquer coisa é só chamar.",
finishReason: "stop",
},
},
],
},
};
84 changes: 84 additions & 0 deletions packages/types/src/testing/demo-scenarios/merchant-blocked.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { DemoScenario } from "../demo-scenario.js";

/* Payment-failure triage — non-recoverable category, at the meta-tool abstraction.
*
* The agent attempts a Pix payment; `codespar_pay` returns a *successful* result
* whose business outcome is a decline with `category: "non_recoverable"` and
* `error_code: "MERCHANT_BLOCKED"`. The irreplaceable judgment is reading that
* category: a blocked merchant account is NOT something the customer can fix, so
* the only correct remediation is to notify a human (`codespar_notify` to an
* internal ops channel) and stop. The agent does NOT retry the payment, does NOT
* ask the customer for anything, and does NOT issue an invoice.
*
* This is the discriminating half of the triage: the same surface
* (`codespar_pay` returns a decline with a category) routes to a completely
* different remediation than the customer-data case. A fixed ask-the-customer
* flow gets this wrong — it would pester the customer about data that is fine
* while the real fix is a human unblocking the merchant account.
*/
export const MERCHANT_BLOCKED_SCENARIO: DemoScenario = {
name: "merchant-blocked",
servers: ["asaas", "z-api"],
mocks: {
// Single rejected result — non-recoverable, so there is no retry element.
codespar_pay: {
status: "rejected",
category: "non_recoverable",
error_code: "MERCHANT_BLOCKED",
reason: "Conta do lojista bloqueada pelo adquirente",
},
codespar_notify: { messageId: "msg_demo_triage_ops_001", sent: true },
},
turns: [
{
message: "Quero pagar R$ 800 via Pix do pedido #4471.",
expectMetaTools: ["codespar_pay", "codespar_notify"],
},
],
aimockFixtures: {
fixtures: [
{
match: { userMessage: "pedido #4471", turnIndex: 0, hasToolResult: false },
response: {
toolCalls: [
{
name: "codespar_pay",
arguments: {
amount: 80000,
currency: "BRL",
method: "pix",
description: "Pagamento do pedido #4471 via Pix",
},
},
],
finishReason: "tool_calls",
},
},
{
match: { userMessage: "pedido #4471", turnIndex: 1, hasToolResult: true },
response: {
toolCalls: [
{
name: "codespar_notify",
arguments: {
channel: "slack",
to: "#payments-ops",
message:
"Pagamento do pedido #4471 bloqueado: conta do lojista MERCHANT_BLOCKED no adquirente. Não é recuperável pelo cliente — escalando para o time resolver.",
},
},
],
finishReason: "tool_calls",
},
},
{
match: { userMessage: "pedido #4471", turnIndex: 2, hasToolResult: true },
response: {
content:
"Não consegui concluir esse pagamento: a conta do lojista está bloqueada no adquirente, o que não dá para resolver pelo seu lado. Já escalei para o time de pagamentos cuidar disso.",
finishReason: "stop",
},
},
],
},
};
4 changes: 4 additions & 0 deletions packages/types/src/testing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ export * from "./demo-scenario.js";
export { SERVICE_INVOICE_SCENARIO } from "./demo-scenarios/service-invoice.js";
export { INSTALLMENT_NEGOTIATION_SCENARIO } from "./demo-scenarios/installment-negotiation.js";
export { PAYMENT_REJECTION_SCENARIO } from "./demo-scenarios/payment-rejection.js";
export { CUSTOMER_DATA_REJECTION_SCENARIO } from "./demo-scenarios/customer-data-rejection.js";
export { MERCHANT_BLOCKED_SCENARIO } from "./demo-scenarios/merchant-blocked.js";
export { DEMO_SCENARIO_MANIFEST } from "./scenario-manifest.js";
export type { DemoScenarioName } from "./scenario-manifest.js";

// Re-export the contract descriptors from the testing entrypoint too, so a
// conformance-test author imports the suite and the descriptors it drives
Expand Down
38 changes: 38 additions & 0 deletions packages/types/src/testing/scenario-manifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Lockstep guard: the demo-scenario manifest's `version` must equal the
* package's published version, and its `scenarios` must name real, shipped
* scenarios whose `name` matches.
*
* Why this matters: a consumer asserts its `@codespar/types` pin equals
* `DEMO_SCENARIO_MANIFEST.version`. If a publish bumps the package version but
* forgets to bump the manifest, the manifest would point at a version nobody
* pins — silently disarming the version-alignment check. This test fails the
* publish before that can happen.
*
* package.json is read at runtime (not statically imported) so it stays outside
* the package's `rootDir`/build graph.
*/

import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { DEMO_SCENARIO_MANIFEST } from "./scenario-manifest.js";
import { CUSTOMER_DATA_REJECTION_SCENARIO } from "./demo-scenarios/customer-data-rejection.js";
import { MERCHANT_BLOCKED_SCENARIO } from "./demo-scenarios/merchant-blocked.js";

const pkg = JSON.parse(
readFileSync(new URL("../../package.json", import.meta.url), "utf8"),
) as { version: string };

describe("demo scenario manifest", () => {
it("version is in lockstep with the package version", () => {
expect(DEMO_SCENARIO_MANIFEST.version).toBe(pkg.version);
});

it("names exactly the shipped triage scenarios", () => {
const shipped = [
CUSTOMER_DATA_REJECTION_SCENARIO.name,
MERCHANT_BLOCKED_SCENARIO.name,
].sort();
expect([...DEMO_SCENARIO_MANIFEST.scenarios].sort()).toEqual(shipped);
});
});
32 changes: 32 additions & 0 deletions packages/types/src/testing/scenario-manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* ── Demo scenario manifest ───────────────────────────────────────
*
* The published contract for the demo scenarios this package ships: the
* canonical set of scenario `name`s plus the package version. A test that drives
* these scenarios asserts against it so an example and the published scenarios
* cannot silently drift apart:
*
* - Completeness: a consumer asserts the set of scenarios it actually drives
* equals `scenarios` exactly. Publish a new scenario and forget to cover it,
* and the assertion fails.
*
* - Version-alignment: a consumer asserts its `@codespar/types` dependency is
* an exact pin equal to `version` — a caret/tilde or a stale pin fails, so a
* consumer always tracks the exact published scenario set.
*
* `version` is kept in lockstep with the package version by
* `scenario-manifest.test.ts`, which fails CI if they drift — so a publish that
* forgets to bump the manifest cannot ship.
* ─────────────────────────────────────────────────────────────── */

/** The demo-scenario contract: the published package version plus the scenario
* names a consumer of these scenarios must drive completely. */
export const DEMO_SCENARIO_MANIFEST = {
/** Must equal this package's published version (see the lockstep test). */
version: "0.10.12",
/** Scenario `name`s a consumer must drive, exactly. */
scenarios: ["customer-data-rejection", "merchant-blocked"],
} as const;

/** A manifest scenario name. */
export type DemoScenarioName =
(typeof DEMO_SCENARIO_MANIFEST)["scenarios"][number];
Loading