@alexkroman1/aai-cli 5.13.2 → 6.1.0
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/dist/{_agent-C8Jk-ZcP.mjs → _agent-vGW5SBnX.mjs} +23 -5
- package/dist/_agent.d.ts +18 -0
- package/dist/{_api-client-BX8Qv51n.mjs → _api-client-LgLksMhN.mjs} +45 -8
- package/dist/_api-client.d.ts +39 -0
- package/dist/{_bundler-BuIyDdvs.mjs → _bundler-BapCUUJN.mjs} +42 -14
- package/dist/_bundler.d.ts +43 -5
- package/dist/_cli-common.d.ts +67 -3
- package/dist/{_config-CzwAWOWr.mjs → _config-D_s09e7g.mjs} +36 -18
- package/dist/_deploy.d.ts +2 -5
- package/dist/_dev-restart.d.ts +6 -1
- package/dist/{_dev-server-Ch7K8e8i.mjs → _dev-server-DQ1t9sbw.mjs} +94 -38
- package/dist/_dev-server.d.ts +30 -2
- package/dist/_e2e-test-utils.d.ts +80 -0
- package/dist/_fault-mode.d.ts +175 -0
- package/dist/{_init-CbkrE2wq.mjs → _init-CpISqoR8.mjs} +3 -3
- package/dist/{_output-CC300DzW.mjs → _output-CKkmWs7i.mjs} +8 -1
- package/dist/_output.d.ts +9 -2
- package/dist/{_server-common-61fL__39.mjs → _server-common-6e0QI4mq.mjs} +1 -1
- package/dist/{_slug-api-D30zPmsn.mjs → _slug-api-D2heILuC.mjs} +2 -2
- package/dist/{_studio-D2ciIhbi.mjs → _studio-zAJycZ8j.mjs} +3 -3
- package/dist/{_templates-B1kt3BGt.mjs → _templates-iDGjGiPf.mjs} +4 -6
- package/dist/{_typecheck-gate-4v9UBZHh.mjs → _typecheck-gate-BXvUNnfs.mjs} +1 -1
- package/dist/{_utils-0DHbcqUb.mjs → _utils-B8QmtFhK.mjs} +21 -7
- package/dist/_utils.d.ts +18 -2
- package/dist/{_vite-env-Dg_QlVv0.mjs → _vite-env-BNveawd1.mjs} +31 -1
- package/dist/_vite-env.d.ts +30 -0
- package/dist/build-BO1Ni6oJ.mjs +62 -0
- package/dist/build.d.ts +19 -0
- package/dist/cli-workflow.d.ts +10 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +406 -224
- package/dist/{client-bundler-C9qXL8A2.mjs → client-bundler-B8Q9F8gJ.mjs} +3 -22
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-DCRX3d96.mjs → delete-4I0uNR4a.mjs} +6 -7
- package/dist/delete.d.ts +2 -8
- package/dist/{deploy-DjsEV-40.mjs → deploy-DJ3HRcia.mjs} +13 -14
- package/dist/{dev-CvPH5dbF.mjs → dev-C6SWaAZP.mjs} +3 -3
- package/dist/{eject-LgBRfnbH.mjs → eject-Zrzk5KV3.mjs} +41 -15
- package/dist/eject.d.ts +30 -3
- package/dist/{init-BppxMR8r.mjs → init-BTRGiPQr.mjs} +61 -26
- package/dist/init.d.ts +11 -0
- package/dist/{login-BQQaxJhL.mjs → login-k0Z3VQu9.mjs} +5 -5
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +498 -91
- package/dist/scaffold/package.json +11 -8
- package/dist/scaffold/pnpm-workspace.yaml +32 -0
- package/dist/scaffold/server.mjs +53 -57
- package/dist/{secret-Ds9dvqAj.mjs → secret-DzSQx1Ds.mjs} +6 -3
- package/dist/{storage-DbrQSHFk.mjs → storage-C-Do0mIA.mjs} +18 -6
- package/dist/{studio-GSFd4s5O.mjs → studio-BGVmCA2G.mjs} +10 -10
- package/dist/templates/code-interpreter/agent.ts +0 -2
- package/dist/templates/dispatch-center/agent.test.ts +21 -30
- package/dist/templates/dispatch-center/agent.ts +4 -31
- package/dist/templates/dispatch-center/client.tsx +41 -28
- package/dist/templates/dispatch-center/shared.ts +73 -66
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +13 -15
- package/dist/templates/dispatch-center/tools/incident_create.ts +42 -45
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +60 -62
- package/dist/templates/dispatch-center/tools/incident_get.ts +10 -6
- package/dist/templates/dispatch-center/tools/incident_triage.ts +44 -46
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +40 -42
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +3 -6
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +21 -24
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +53 -55
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +3 -5
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +40 -43
- package/dist/templates/embedded-assets/agent.test.ts +83 -0
- package/dist/templates/embedded-assets/agent.ts +1 -48
- package/dist/templates/embedded-assets/shared.ts +17 -0
- package/dist/templates/embedded-assets/system-prompt.md +13 -0
- package/dist/templates/embedded-assets/tools/list_topics.ts +9 -0
- package/dist/templates/embedded-assets/tools/search_knowledge.ts +26 -0
- package/dist/templates/health-assistant/agent.test.ts +169 -0
- package/dist/templates/health-assistant/agent.ts +1 -166
- package/dist/templates/health-assistant/fda.ts +100 -0
- package/dist/templates/health-assistant/tools/check_drug_interaction.ts +59 -0
- package/dist/templates/health-assistant/tools/medication_lookup.ts +31 -0
- package/dist/templates/infocom-adventure/agent.test.ts +205 -0
- package/dist/templates/infocom-adventure/agent.ts +3 -106
- package/dist/templates/infocom-adventure/client.tsx +32 -28
- package/dist/templates/infocom-adventure/shared.ts +36 -16
- package/dist/templates/infocom-adventure/tools/game_state_drop.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_flag.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_get.ts +16 -0
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_restart.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_score.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_take.ts +13 -0
- package/dist/templates/link-digest/agent.test.ts +223 -0
- package/dist/templates/link-digest/agent.ts +79 -0
- package/dist/templates/link-digest/client.tsx +146 -0
- package/dist/templates/link-digest/workflows/digest.ts +248 -0
- package/dist/templates/math-buddy/agent.ts +0 -2
- package/dist/templates/night-owl/agent.test.ts +80 -0
- package/dist/templates/night-owl/agent.ts +1 -68
- package/dist/templates/night-owl/system-prompt.md +10 -0
- package/dist/templates/night-owl/tools/recommend.ts +64 -0
- package/dist/templates/personal-finance/agent.ts +0 -2
- package/dist/templates/pizza-ordering/agent.test.ts +59 -58
- package/dist/templates/pizza-ordering/agent.ts +10 -159
- package/dist/templates/pizza-ordering/client.tsx +4 -4
- package/dist/templates/pizza-ordering/shared.ts +44 -27
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +31 -0
- package/dist/templates/pizza-ordering/tools/place_order.ts +22 -0
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +22 -0
- package/dist/templates/pizza-ordering/tools/set_customer_name.ts +13 -0
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +28 -0
- package/dist/templates/pizza-ordering/tools/view_order.ts +21 -0
- package/dist/templates/plan-and-execute/agent.test.ts +411 -0
- package/dist/templates/plan-and-execute/agent.ts +23 -0
- package/dist/templates/plan-and-execute/client.tsx +115 -0
- package/dist/templates/plan-and-execute/graph.ts +178 -0
- package/dist/templates/plan-and-execute/prompts.ts +110 -0
- package/dist/templates/plan-and-execute/shared.ts +161 -0
- package/dist/templates/plan-and-execute/system-prompt.md +18 -0
- package/dist/templates/plan-and-execute/tools/plan_status.ts +20 -0
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +63 -0
- package/dist/templates/plan-and-execute/tools/start_plan.ts +51 -0
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +101 -0
- package/dist/templates/recap-workflow/agent.test.ts +762 -0
- package/dist/templates/recap-workflow/agent.ts +95 -0
- package/dist/templates/recap-workflow/shared.ts +43 -0
- package/dist/templates/recap-workflow/tools/cancel_recap.ts +23 -0
- package/dist/templates/recap-workflow/tools/keep_transcript.ts +31 -0
- package/dist/templates/recap-workflow/tools/recap_progress.ts +27 -0
- package/dist/templates/recap-workflow/tools/recap_status.ts +46 -0
- package/dist/templates/recap-workflow/tools/request_recap.ts +43 -0
- package/dist/templates/recap-workflow/workflows/recap.ts +544 -0
- package/dist/templates/recap-workflow/workflows/tokens.ts +26 -0
- package/dist/templates/redline/agent.test.ts +222 -0
- package/dist/templates/redline/agent.ts +89 -0
- package/dist/templates/redline/client.tsx +184 -0
- package/dist/templates/redline/workflows/prompts.ts +68 -0
- package/dist/templates/redline/workflows/redline.ts +258 -0
- package/dist/templates/research-workflow/agent.test.ts +525 -0
- package/dist/templates/research-workflow/agent.ts +94 -0
- package/dist/templates/research-workflow/shared.ts +35 -0
- package/dist/templates/research-workflow/tools/file_it_now.ts +22 -0
- package/dist/templates/research-workflow/tools/request_research.ts +33 -0
- package/dist/templates/research-workflow/tools/research_progress.ts +26 -0
- package/dist/templates/research-workflow/tools/research_status.ts +40 -0
- package/dist/templates/research-workflow/workflows/prompts.ts +143 -0
- package/dist/templates/research-workflow/workflows/research.ts +558 -0
- package/dist/templates/retail/agent.test.ts +319 -256
- package/dist/templates/retail/agent.ts +6 -46
- package/dist/templates/retail/authenticate.ts +2 -2
- package/dist/templates/retail/client.tsx +45 -24
- package/dist/templates/retail/registry.test.ts +40 -25
- package/dist/templates/retail/resolve.test.ts +17 -16
- package/dist/templates/retail/resolve.ts +32 -67
- package/dist/templates/retail/shared.test.ts +21 -19
- package/dist/templates/retail/shared.ts +61 -23
- package/dist/templates/retail/store.test.ts +47 -52
- package/dist/templates/retail/store.ts +92 -79
- package/dist/templates/retail/swap.test.ts +20 -17
- package/dist/templates/retail/swap.ts +7 -14
- package/dist/templates/retail/tools/cancel_pending_order.ts +7 -11
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +26 -14
- package/dist/templates/retail/tools/find_user_id_by_email.ts +5 -9
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +5 -7
- package/dist/templates/retail/tools/get_item_details.ts +7 -8
- package/dist/templates/retail/tools/get_order_details.ts +7 -8
- package/dist/templates/retail/tools/get_product_details.ts +6 -8
- package/dist/templates/retail/tools/get_user_details.ts +6 -8
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
- package/dist/templates/retail/tools/modify_pending_order_address.ts +6 -10
- package/dist/templates/retail/tools/modify_pending_order_items.ts +8 -20
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +7 -13
- package/dist/templates/retail/tools/modify_user_address.ts +6 -10
- package/dist/templates/retail/tools/return_delivered_order_items.ts +7 -13
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
- package/dist/templates/solo-rpg/agent.test.ts +169 -54
- package/dist/templates/solo-rpg/agent.ts +4 -22
- package/dist/templates/solo-rpg/client.tsx +19 -6
- package/dist/templates/solo-rpg/shared.ts +20 -20
- package/dist/templates/solo-rpg/tools/action_roll.ts +3 -5
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +3 -5
- package/dist/templates/solo-rpg/tools/check_state.ts +7 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +3 -3
- package/dist/templates/solo-rpg/tools/oracle.ts +19 -12
- package/dist/templates/solo-rpg/tools/save_game.ts +11 -8
- package/dist/templates/solo-rpg/tools/setup_character.ts +3 -3
- package/dist/templates/solo-rpg/tools/update_state.ts +13 -13
- package/dist/templates/support-line/agent.test.ts +343 -0
- package/dist/templates/support-line/agent.ts +27 -0
- package/dist/templates/support-line/client.tsx +122 -0
- package/dist/templates/support-line/graph.ts +224 -0
- package/dist/templates/support-line/knowledge.json +65 -0
- package/dist/templates/support-line/prompts.ts +95 -0
- package/dist/templates/support-line/shared.ts +240 -0
- package/dist/templates/support-line/system-prompt.md +17 -0
- package/dist/templates/support-line/tools/answer_question.ts +72 -0
- package/dist/templates/support-line/tools/list_topics.ts +22 -0
- package/dist/templates/support-line/tools/log_ticket.ts +30 -0
- package/dist/templates/transcription-workflow/agent.test.ts +563 -0
- package/dist/templates/transcription-workflow/agent.ts +92 -0
- package/dist/templates/transcription-workflow/client.tsx +234 -0
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +416 -0
- package/dist/templates/transcription-workflow/workflows/wav.ts +314 -0
- package/dist/templates/travel-concierge/agent.test.ts +338 -0
- package/dist/templates/travel-concierge/agent.ts +29 -0
- package/dist/templates/travel-concierge/client.tsx +161 -0
- package/dist/templates/travel-concierge/routing.ts +138 -0
- package/dist/templates/travel-concierge/shared.ts +547 -0
- package/dist/templates/travel-concierge/system-prompt.md +24 -0
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +20 -0
- package/dist/templates/travel-concierge/tools/book_excursion.ts +18 -0
- package/dist/templates/travel-concierge/tools/book_hotel.ts +20 -0
- package/dist/templates/travel-concierge/tools/cancel_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +16 -0
- package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +7 -0
- package/dist/templates/travel-concierge/tools/confirm_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +35 -0
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +33 -0
- package/dist/templates/travel-concierge/tools/search_excursions.ts +37 -0
- package/dist/templates/travel-concierge/tools/search_flights.ts +43 -0
- package/dist/templates/travel-concierge/tools/search_hotels.ts +36 -0
- package/dist/templates/travel-concierge/tools/to_car_rental_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_excursion_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_flight_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_hotel_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/update_ticket.ts +22 -0
- package/dist/templates/web-researcher/agent.ts +0 -2
- package/dist/templates/web-researcher/system-prompt.md +10 -0
- package/dist/{test-2LWBcXvY.mjs → test-B6ZBxSpk.mjs} +2 -2
- package/dist/typecheck.mjs +17 -15
- package/dist/worker-bundler-CIdDhZ0c.mjs +398 -0
- package/dist/worker-bundler.d.ts +14 -0
- package/dist/worker-bundler.mjs +1 -108
- package/dist/workflow-B-O97AqI.mjs +125 -0
- package/dist/workflow-bundler.d.ts +133 -0
- package/dist/workflow.d.ts +67 -0
- package/package.json +13 -9
- package/dist/build-DQJ1ZSm9.mjs +0 -39
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
2
|
import seedJson from "./seed.json";
|
|
3
|
-
import type { RetailState
|
|
4
|
-
import { buildScriptBullets, DEMO_PERSONAS, storeView } from "./shared.ts";
|
|
3
|
+
import type { RetailState } from "./shared.ts";
|
|
4
|
+
import { buildScriptBullets, DEMO_PERSONAS, emptyRetailState, storeView } from "./shared.ts";
|
|
5
|
+
import { seedStore } from "./store.ts";
|
|
5
6
|
|
|
6
7
|
function makeState(authenticatedUserId: string | null): RetailState {
|
|
7
8
|
return {
|
|
8
|
-
store:
|
|
9
|
+
store: seedStore(),
|
|
9
10
|
authenticatedUserId,
|
|
10
11
|
callSeq: 3,
|
|
11
12
|
activity: [{ seq: 3, tool: "get_order_details", summary: "read #W5866402", at: 0 }],
|
|
@@ -15,20 +16,21 @@ function makeState(authenticatedUserId: string | null): RetailState {
|
|
|
15
16
|
|
|
16
17
|
describe("storeView", () => {
|
|
17
18
|
test("projects nothing identifying before authentication", () => {
|
|
18
|
-
const view = storeView(
|
|
19
|
+
const view = storeView(makeState(null));
|
|
19
20
|
expect(view.customer).toBeNull();
|
|
20
21
|
expect(view.orders).toEqual([]);
|
|
21
22
|
});
|
|
22
23
|
|
|
23
|
-
test("an
|
|
24
|
-
|
|
24
|
+
test("an untouched session projects an empty view, not undefined", () => {
|
|
25
|
+
// The value `client.tsx` hoists as its pre-first-tool-call fallback.
|
|
26
|
+
const view = storeView(emptyRetailState());
|
|
25
27
|
expect(view.customer).toBeNull();
|
|
26
28
|
expect(view.orders).toEqual([]);
|
|
27
29
|
expect(view.callSeq).toBe(0);
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
test("projects the authenticated customer and only their orders", () => {
|
|
31
|
-
const view = storeView(
|
|
33
|
+
const view = storeView(makeState("olivia_ito_3591"));
|
|
32
34
|
expect(view.customer?.name).toBe("Olivia Ito");
|
|
33
35
|
expect(view.customer?.email).toBe("olivia.ito5204@example.com");
|
|
34
36
|
expect(view.orders.map((o) => o.orderId).sort((a, b) => a.localeCompare(b))).toEqual(
|
|
@@ -40,13 +42,13 @@ describe("storeView", () => {
|
|
|
40
42
|
|
|
41
43
|
test("projects live gift-card balances, since refunds move them", () => {
|
|
42
44
|
const state = makeState("olivia_ito_3591");
|
|
43
|
-
const view = storeView(
|
|
45
|
+
const view = storeView(state);
|
|
44
46
|
const card = view.customer?.paymentMethods.find((m) => m.id === "gift_card_7794233");
|
|
45
47
|
expect(card?.balance).toBe(56);
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
test("no other customer's email, address, or payment method reaches the client", () => {
|
|
49
|
-
const view = storeView(
|
|
51
|
+
const view = storeView(makeState("olivia_ito_3591"));
|
|
50
52
|
const json = JSON.stringify(view);
|
|
51
53
|
for (const leaked of [
|
|
52
54
|
"aarav.anderson9752@example.com",
|
|
@@ -64,14 +66,14 @@ describe("storeView", () => {
|
|
|
64
66
|
});
|
|
65
67
|
|
|
66
68
|
test("carries callSeq, activity and focus through", () => {
|
|
67
|
-
const view = storeView(
|
|
69
|
+
const view = storeView(makeState("olivia_ito_3591"));
|
|
68
70
|
expect(view.callSeq).toBe(3);
|
|
69
71
|
expect(view.activity).toHaveLength(1);
|
|
70
72
|
expect(view.focus.orderId).toBe("#W5866402");
|
|
71
73
|
});
|
|
72
74
|
|
|
73
75
|
test("order totals sum the payments, not the items", () => {
|
|
74
|
-
const view = storeView(
|
|
76
|
+
const view = storeView(makeState("olivia_ito_3591"));
|
|
75
77
|
const delivered = view.orders.find((o) => o.orderId === "#W5866402");
|
|
76
78
|
expect(delivered?.total).toBe(3203.78);
|
|
77
79
|
});
|
|
@@ -83,7 +85,7 @@ describe("DEMO_PERSONAS", () => {
|
|
|
83
85
|
});
|
|
84
86
|
|
|
85
87
|
test("every persona's email, name and zip match its seed record", () => {
|
|
86
|
-
const users = Object.values((
|
|
88
|
+
const users = Object.values(seedStore().users);
|
|
87
89
|
for (const persona of DEMO_PERSONAS) {
|
|
88
90
|
const user = users.find((u) => u.email === persona.email);
|
|
89
91
|
expect.soft(user, `no seeded customer has email ${persona.email}`).toBeDefined();
|
|
@@ -105,12 +107,12 @@ describe("swapOptions", () => {
|
|
|
105
107
|
test("empty when no order is focused", () => {
|
|
106
108
|
const state = makeState("olivia_ito_3591");
|
|
107
109
|
state.focus = {};
|
|
108
|
-
expect(storeView(
|
|
110
|
+
expect(storeView(state).swapOptions).toEqual([]);
|
|
109
111
|
});
|
|
110
112
|
|
|
111
113
|
test("lists available alternatives for the focused order's items", () => {
|
|
112
114
|
// makeState focuses #W5866402 — Espresso Machine + Sneakers.
|
|
113
|
-
const view = storeView(
|
|
115
|
+
const view = storeView(makeState("olivia_ito_3591"));
|
|
114
116
|
expect(view.swapOptions).toHaveLength(2);
|
|
115
117
|
const espresso = view.swapOptions.find((s) => s.itemId === "6242772310");
|
|
116
118
|
expect(espresso?.itemName).toBe("Espresso Machine");
|
|
@@ -121,7 +123,7 @@ describe("swapOptions", () => {
|
|
|
121
123
|
test("never offers an unavailable variant as a target", () => {
|
|
122
124
|
const state = makeState("aarav_anderson_8794");
|
|
123
125
|
state.focus = { orderId: "#W4316152" };
|
|
124
|
-
const view = storeView(
|
|
126
|
+
const view = storeView(state);
|
|
125
127
|
const ids = view.swapOptions.flatMap((s) => s.alternatives.map((a) => a.itemId));
|
|
126
128
|
// 6454334990 is a Tea Kettle variant with available: false.
|
|
127
129
|
expect(ids).not.toContain("6454334990");
|
|
@@ -130,7 +132,7 @@ describe("swapOptions", () => {
|
|
|
130
132
|
test("caps both axes so the payload stays bounded", () => {
|
|
131
133
|
const state = makeState("aarav_anderson_8794");
|
|
132
134
|
state.focus = { orderId: "#W9311069" }; // five items
|
|
133
|
-
const view = storeView(
|
|
135
|
+
const view = storeView(state);
|
|
134
136
|
expect(view.swapOptions.length).toBeLessThanOrEqual(3);
|
|
135
137
|
for (const option of view.swapOptions) {
|
|
136
138
|
expect(option.alternatives.length).toBeLessThanOrEqual(4);
|
|
@@ -140,7 +142,7 @@ describe("swapOptions", () => {
|
|
|
140
142
|
test("a focus on another customer's order projects nothing", () => {
|
|
141
143
|
const state = makeState("olivia_ito_3591");
|
|
142
144
|
state.focus = { orderId: "#W4316152" }; // aarav's
|
|
143
|
-
const view = storeView(
|
|
145
|
+
const view = storeView(state);
|
|
144
146
|
expect(view.swapOptions).toEqual([]);
|
|
145
147
|
expect(JSON.stringify(view)).not.toContain("#W4316152");
|
|
146
148
|
});
|
|
@@ -153,8 +155,8 @@ describe("buildScriptBullets", () => {
|
|
|
153
155
|
expect(bullets.join(" ")).toContain(DEMO_PERSONAS[0]?.zip ?? "");
|
|
154
156
|
});
|
|
155
157
|
|
|
156
|
-
test("
|
|
157
|
-
expect(buildScriptBullets(
|
|
158
|
+
test("an empty state yields the pre-auth bullets", () => {
|
|
159
|
+
expect(buildScriptBullets(emptyRetailState()).length).toBeGreaterThan(0);
|
|
158
160
|
});
|
|
159
161
|
|
|
160
162
|
test("a customer with pending orders is offered cancel and address change", () => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { DeepReadonly } from "@alexkroman1/aai";
|
|
2
|
+
|
|
1
3
|
// ─── Store types ─────────────────────────────────────────────────────────────
|
|
2
4
|
// Field names are tau2's snake_case verbatim, because `seed.json` is tau2's
|
|
3
5
|
// records unmodified. Renaming them here would mean transforming 107 KB of
|
|
@@ -129,7 +131,36 @@ export interface RetailState {
|
|
|
129
131
|
focus: { orderId?: string; productId?: string };
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
|
|
134
|
+
/**
|
|
135
|
+
* The store as a READ hands it out: deep-frozen, and typed to say so.
|
|
136
|
+
*
|
|
137
|
+
* The projection and its helpers take this rather than {@link RetailState},
|
|
138
|
+
* which is the widening a deep-readonly slot forces and the reason it is worth
|
|
139
|
+
* doing: a mutable state still satisfies it, so `client.tsx` still projects
|
|
140
|
+
* `emptyRetailState()` and every `retailTool` body still passes its draft,
|
|
141
|
+
* while a projection helper that WOULD have mutated stops compiling instead of
|
|
142
|
+
* throwing at its first call.
|
|
143
|
+
*/
|
|
144
|
+
export type FrozenRetailState = DeepReadonly<RetailState>;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A pristine, EMPTY store — no seed data.
|
|
148
|
+
*
|
|
149
|
+
* `store.ts`'s `createDefaultState` builds the real seeded state on top of
|
|
150
|
+
* this, so the shape is declared once. It lives here, apart from that, because
|
|
151
|
+
* `client.tsx` needs a `RetailState` for its pre-first-call fallback and must
|
|
152
|
+
* not import `store.ts`: that module pulls the 107 KB `seed.json`, which would
|
|
153
|
+
* then land in the browser bundle.
|
|
154
|
+
*/
|
|
155
|
+
export function emptyRetailState(): RetailState {
|
|
156
|
+
return {
|
|
157
|
+
store: { users: {}, orders: {}, products: {} },
|
|
158
|
+
authenticatedUserId: null,
|
|
159
|
+
callSeq: 0,
|
|
160
|
+
activity: [],
|
|
161
|
+
focus: {},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
133
164
|
|
|
134
165
|
// ─── View types — exactly what crosses the wire ──────────────────────────────
|
|
135
166
|
|
|
@@ -249,13 +280,13 @@ export const DEMO_PERSONAS: readonly DemoPersona[] = [
|
|
|
249
280
|
|
|
250
281
|
// ─── Projection ──────────────────────────────────────────────────────────────
|
|
251
282
|
|
|
252
|
-
function paymentLabel(method: PaymentMethod): string {
|
|
283
|
+
function paymentLabel(method: DeepReadonly<PaymentMethod>): string {
|
|
253
284
|
if (method.source === "gift_card") return "Gift card";
|
|
254
285
|
if (method.source === "paypal") return "PayPal";
|
|
255
286
|
return `${method.brand} ending ${method.last_four}`;
|
|
256
287
|
}
|
|
257
288
|
|
|
258
|
-
function paymentMethodView(method: PaymentMethod): PaymentMethodView {
|
|
289
|
+
function paymentMethodView(method: DeepReadonly<PaymentMethod>): PaymentMethodView {
|
|
259
290
|
return {
|
|
260
291
|
id: method.id,
|
|
261
292
|
source: method.source,
|
|
@@ -267,7 +298,7 @@ function paymentMethodView(method: PaymentMethod): PaymentMethodView {
|
|
|
267
298
|
/** Net of payments minus refunds — what the customer has actually paid. Items
|
|
268
299
|
* can't be summed instead: a modified order's items no longer match what was
|
|
269
300
|
* charged. */
|
|
270
|
-
export function orderTotal(order: Order): number {
|
|
301
|
+
export function orderTotal(order: DeepReadonly<Order>): number {
|
|
271
302
|
const net = order.payment_history.reduce(
|
|
272
303
|
(sum, p) => sum + (p.transaction_type === "payment" ? p.amount : -p.amount),
|
|
273
304
|
0,
|
|
@@ -275,7 +306,7 @@ export function orderTotal(order: Order): number {
|
|
|
275
306
|
return Math.round(net * 100) / 100;
|
|
276
307
|
}
|
|
277
308
|
|
|
278
|
-
function orderView(order: Order): OrderView {
|
|
309
|
+
function orderView(order: DeepReadonly<Order>): OrderView {
|
|
279
310
|
return {
|
|
280
311
|
orderId: order.order_id,
|
|
281
312
|
status: order.status,
|
|
@@ -295,7 +326,10 @@ function orderView(order: Order): OrderView {
|
|
|
295
326
|
* an exchange sayable. Only available variants, only the focused order, and
|
|
296
327
|
* capped on both axes so the payload stays bounded.
|
|
297
328
|
*/
|
|
298
|
-
function swapOptionsFor(
|
|
329
|
+
function swapOptionsFor(
|
|
330
|
+
state: FrozenRetailState,
|
|
331
|
+
order: DeepReadonly<Order> | undefined,
|
|
332
|
+
): SwapOptionView[] {
|
|
299
333
|
if (!order) return [];
|
|
300
334
|
return order.items.slice(0, MAX_SWAP_ITEMS).map((item) => {
|
|
301
335
|
const product = state.store.products[item.product_id];
|
|
@@ -321,16 +355,20 @@ function swapOptionsFor(state: RetailState, order: Order | undefined): SwapOptio
|
|
|
321
355
|
*
|
|
322
356
|
* Only the authenticated customer is projected. The other five seeded
|
|
323
357
|
* customers' emails, addresses, and payment methods never leave the server,
|
|
324
|
-
* which is why `syncState` takes a projection rather than a boolean.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
358
|
+
* which is why `syncState` takes a projection rather than a boolean.
|
|
359
|
+
*
|
|
360
|
+
* Takes the state itself, not the slot: `retailSlot.projection` supplies a
|
|
361
|
+
* real one even before the first tool call, so the optional chaining this
|
|
362
|
+
* used to carry — which read as security gating and was not — is gone. The
|
|
363
|
+
* gating that IS security is still here, on `user`. */
|
|
364
|
+
export function storeView(state: FrozenRetailState): StoreView {
|
|
365
|
+
const userId = state.authenticatedUserId;
|
|
366
|
+
const user = userId ? state.store.users[userId] : undefined;
|
|
367
|
+
const focusedOrderId = state.focus.orderId;
|
|
330
368
|
// Gated on `user`: an order is only ever projected for the customer on the
|
|
331
369
|
// call, so a stale focus from before authentication cannot leak one.
|
|
332
370
|
const focusedOrder =
|
|
333
|
-
|
|
371
|
+
user && focusedOrderId && user.orders.includes(focusedOrderId)
|
|
334
372
|
? state.store.orders[focusedOrderId]
|
|
335
373
|
: undefined;
|
|
336
374
|
|
|
@@ -346,7 +384,7 @@ export function storeView(slot: StateSlot): StoreView {
|
|
|
346
384
|
: null,
|
|
347
385
|
orders: user
|
|
348
386
|
? user.orders
|
|
349
|
-
.map((id) => state
|
|
387
|
+
.map((id) => state.store.orders[id])
|
|
350
388
|
.filter((o): o is Order => o !== undefined)
|
|
351
389
|
.map(orderView)
|
|
352
390
|
: [],
|
|
@@ -356,13 +394,13 @@ export function storeView(slot: StateSlot): StoreView {
|
|
|
356
394
|
// though its details were already withheld from `orders`/`swapOptions`.
|
|
357
395
|
focus: {
|
|
358
396
|
...(focusedOrder && focusedOrderId ? { orderId: focusedOrderId } : {}),
|
|
359
|
-
...(state
|
|
397
|
+
...(state.focus.productId ? { productId: state.focus.productId } : {}),
|
|
360
398
|
},
|
|
361
|
-
swapOptions:
|
|
362
|
-
callSeq: state
|
|
363
|
-
activity: state
|
|
399
|
+
swapOptions: swapOptionsFor(state, focusedOrder),
|
|
400
|
+
callSeq: state.callSeq,
|
|
401
|
+
activity: state.activity.slice(-MAX_ACTIVITY),
|
|
364
402
|
scriptBullets: buildScriptBullets(state),
|
|
365
|
-
productCount: Object.keys(state
|
|
403
|
+
productCount: Object.keys(state.store.products).length,
|
|
366
404
|
};
|
|
367
405
|
}
|
|
368
406
|
|
|
@@ -375,13 +413,13 @@ const MAX_BULLETS = 6;
|
|
|
375
413
|
* that offers actions the data can't support — "return an item" to someone
|
|
376
414
|
* with no delivered order — is worse than no list.
|
|
377
415
|
*/
|
|
378
|
-
export function buildScriptBullets(state:
|
|
379
|
-
const userId = state
|
|
380
|
-
const user = userId ? state
|
|
416
|
+
export function buildScriptBullets(state: FrozenRetailState): string[] {
|
|
417
|
+
const userId = state.authenticatedUserId;
|
|
418
|
+
const user = userId ? state.store.users[userId] : undefined;
|
|
381
419
|
|
|
382
420
|
// Pre-auth: only the two ways in. The personas panel (a constant, rendered
|
|
383
421
|
// straight from DEMO_PERSONAS) covers WHICH customer to be.
|
|
384
|
-
if (!
|
|
422
|
+
if (!user) {
|
|
385
423
|
const first = DEMO_PERSONAS[0];
|
|
386
424
|
return [
|
|
387
425
|
`"My email is ${first?.email ?? ""}"`,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { isToolFailure, type ToolContext } from "@alexkroman1/aai";
|
|
2
|
+
import { createToolContext } from "@alexkroman1/aai/testing";
|
|
2
3
|
import { describe, expect, test } from "vitest";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import {
|
|
@@ -10,31 +11,22 @@ import {
|
|
|
10
11
|
findProduct,
|
|
11
12
|
findUser,
|
|
12
13
|
findVariant,
|
|
13
|
-
getState,
|
|
14
|
-
isError,
|
|
15
14
|
isGiftCard,
|
|
16
15
|
money,
|
|
17
16
|
requireOwnOrder,
|
|
17
|
+
retailSlot,
|
|
18
18
|
retailTool,
|
|
19
19
|
} from "./store.ts";
|
|
20
20
|
|
|
21
|
-
let sessionCounter = 0;
|
|
22
|
-
|
|
23
21
|
function makeCtx(): ToolContext {
|
|
24
|
-
return
|
|
25
|
-
sessionId: `test-session-${++sessionCounter}`,
|
|
26
|
-
send: () => {},
|
|
27
|
-
env: {},
|
|
28
|
-
state: {},
|
|
29
|
-
messages: [],
|
|
30
|
-
} as unknown as ToolContext;
|
|
22
|
+
return createToolContext();
|
|
31
23
|
}
|
|
32
24
|
|
|
33
25
|
describe("session state", () => {
|
|
34
|
-
test("
|
|
26
|
+
test("the slot seeds lazily and returns the same object on re-entry", () => {
|
|
35
27
|
const ctx = makeCtx();
|
|
36
|
-
const a =
|
|
37
|
-
const b =
|
|
28
|
+
const a = retailSlot.get(ctx);
|
|
29
|
+
const b = retailSlot.get(ctx);
|
|
38
30
|
expect(a).toBe(b);
|
|
39
31
|
expect(a.authenticatedUserId).toBeNull();
|
|
40
32
|
expect(a.callSeq).toBe(0);
|
|
@@ -42,14 +34,15 @@ describe("session state", () => {
|
|
|
42
34
|
});
|
|
43
35
|
|
|
44
36
|
test("each session gets its own deep copy — a mutation cannot leak across sessions", () => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
37
|
+
retailSlot.update(makeCtx(), (first) => {
|
|
38
|
+
const order = first.store.orders["#W9300146"];
|
|
39
|
+
if (!order) throw new Error("fixture missing");
|
|
40
|
+
order.status = "cancelled";
|
|
41
|
+
const giftCard = first.store.users.aarav_anderson_8794?.payment_methods.gift_card_7245904;
|
|
42
|
+
if (giftCard?.source === "gift_card") giftCard.balance = 0;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const second = retailSlot.get(makeCtx());
|
|
53
46
|
expect(second.store.orders["#W9300146"]?.status).toBe("pending");
|
|
54
47
|
const fresh = second.store.users.aarav_anderson_8794?.payment_methods.gift_card_7245904;
|
|
55
48
|
expect(fresh?.source === "gift_card" && fresh.balance).toBe(17);
|
|
@@ -67,52 +60,52 @@ describe("money", () => {
|
|
|
67
60
|
describe("lookups", () => {
|
|
68
61
|
test("findUser resolves and reports a miss by id", () => {
|
|
69
62
|
const state = createDefaultState();
|
|
70
|
-
expect(
|
|
63
|
+
expect(isToolFailure(findUser(state, "olivia_ito_3591"))).toBe(false);
|
|
71
64
|
const miss = findUser(state, "nobody_1");
|
|
72
|
-
expect(
|
|
65
|
+
expect(isToolFailure(miss) && miss.error).toContain("nobody_1");
|
|
73
66
|
});
|
|
74
67
|
|
|
75
68
|
test("findOrder resolves and reports a miss", () => {
|
|
76
69
|
const state = createDefaultState();
|
|
77
70
|
const order = findOrder(state, "#W5866402");
|
|
78
|
-
expect(
|
|
79
|
-
expect(
|
|
80
|
-
expect(
|
|
71
|
+
expect(isToolFailure(order)).toBe(false);
|
|
72
|
+
expect(isToolFailure(order) ? null : order.status).toBe("delivered");
|
|
73
|
+
expect(isToolFailure(findOrder(state, "#W0000000"))).toBe(true);
|
|
81
74
|
});
|
|
82
75
|
|
|
83
76
|
test("findProduct and findVariant resolve within one product", () => {
|
|
84
77
|
const state = createDefaultState();
|
|
85
78
|
const product = findProduct(state, "9832717871");
|
|
86
|
-
if (
|
|
79
|
+
if (isToolFailure(product)) throw new Error(product.error);
|
|
87
80
|
expect(product.name).toBe("Tea Kettle");
|
|
88
81
|
const variant = findVariant(product, "3909406921");
|
|
89
|
-
expect(
|
|
82
|
+
expect(isToolFailure(variant) ? null : variant.price).toBe(98.25);
|
|
90
83
|
// A real item id, but of a different product.
|
|
91
|
-
expect(
|
|
84
|
+
expect(isToolFailure(findVariant(product, "4725166838"))).toBe(true);
|
|
92
85
|
});
|
|
93
86
|
|
|
94
87
|
test("findItem scans every product and returns both product and variant", () => {
|
|
95
88
|
const state = createDefaultState();
|
|
96
89
|
const found = findItem(state, "3909406921");
|
|
97
|
-
if (
|
|
90
|
+
if (isToolFailure(found)) throw new Error(found.error);
|
|
98
91
|
expect(found.product.product_id).toBe("9832717871");
|
|
99
92
|
expect(found.variant.price).toBe(98.25);
|
|
100
|
-
expect(
|
|
93
|
+
expect(isToolFailure(findItem(state, "0000000000"))).toBe(true);
|
|
101
94
|
});
|
|
102
95
|
|
|
103
96
|
test("findPaymentMethod resolves on the owning user only", () => {
|
|
104
97
|
const state = createDefaultState();
|
|
105
98
|
const olivia = findUser(state, "olivia_ito_3591");
|
|
106
|
-
if (
|
|
107
|
-
expect(
|
|
99
|
+
if (isToolFailure(olivia)) throw new Error(olivia.error);
|
|
100
|
+
expect(isToolFailure(findPaymentMethod(olivia, "gift_card_7794233"))).toBe(false);
|
|
108
101
|
// aarav's card is not olivia's.
|
|
109
|
-
expect(
|
|
102
|
+
expect(isToolFailure(findPaymentMethod(olivia, "gift_card_7245904"))).toBe(true);
|
|
110
103
|
});
|
|
111
104
|
|
|
112
105
|
test("isGiftCard narrows", () => {
|
|
113
106
|
const state = createDefaultState();
|
|
114
107
|
const olivia = findUser(state, "olivia_ito_3591");
|
|
115
|
-
if (
|
|
108
|
+
if (isToolFailure(olivia)) throw new Error(olivia.error);
|
|
116
109
|
const card = olivia.payment_methods.gift_card_7794233;
|
|
117
110
|
const paypal = olivia.payment_methods.paypal_8049766;
|
|
118
111
|
expect(card && isGiftCard(card)).toBe(true);
|
|
@@ -124,15 +117,15 @@ describe("ownership and authentication guards", () => {
|
|
|
124
117
|
test("authenticatedUser errors before authentication", () => {
|
|
125
118
|
const state = createDefaultState();
|
|
126
119
|
const result = authenticatedUser(state);
|
|
127
|
-
expect(
|
|
120
|
+
expect(isToolFailure(result) && result.error.toLowerCase()).toContain("find_user_id_by_email");
|
|
128
121
|
});
|
|
129
122
|
|
|
130
123
|
test("requireOwnOrder refuses another customer's order without revealing it", () => {
|
|
131
124
|
const state = createDefaultState();
|
|
132
125
|
state.authenticatedUserId = "olivia_ito_3591";
|
|
133
126
|
const result = requireOwnOrder(state, "#W4316152"); // aarav's
|
|
134
|
-
expect(
|
|
135
|
-
if (!
|
|
127
|
+
expect(isToolFailure(result)).toBe(true);
|
|
128
|
+
if (!isToolFailure(result)) throw new Error("expected refusal");
|
|
136
129
|
expect(result.error).not.toContain("aarav");
|
|
137
130
|
expect(result.error).not.toContain("Tea Kettle");
|
|
138
131
|
});
|
|
@@ -144,8 +137,8 @@ describe("ownership and authentication guards", () => {
|
|
|
144
137
|
const missingId = "#W0000000";
|
|
145
138
|
const foreign = requireOwnOrder(state, foreignId);
|
|
146
139
|
const missing = requireOwnOrder(state, missingId);
|
|
147
|
-
expect(
|
|
148
|
-
if (!(
|
|
140
|
+
expect(isToolFailure(foreign) && isToolFailure(missing)).toBe(true);
|
|
141
|
+
if (!(isToolFailure(foreign) && isToolFailure(missing))) throw new Error("expected refusals");
|
|
149
142
|
// Structural, not literal, equality: the messages legitimately differ by
|
|
150
143
|
// the id the caller themselves passed in (that's not information about
|
|
151
144
|
// the store — they already knew it), so comparing byte-for-byte would be
|
|
@@ -161,7 +154,7 @@ describe("ownership and authentication guards", () => {
|
|
|
161
154
|
const state = createDefaultState();
|
|
162
155
|
state.authenticatedUserId = "olivia_ito_3591";
|
|
163
156
|
const result = requireOwnOrder(state, "#W5866402");
|
|
164
|
-
expect(
|
|
157
|
+
expect(isToolFailure(result) ? null : result.order_id).toBe("#W5866402");
|
|
165
158
|
});
|
|
166
159
|
});
|
|
167
160
|
|
|
@@ -196,7 +189,7 @@ describe("retailTool", () => {
|
|
|
196
189
|
const ctx = makeCtx();
|
|
197
190
|
await echo.execute({ value: "a" }, ctx);
|
|
198
191
|
await echo.execute({ value: "b" }, ctx);
|
|
199
|
-
const state =
|
|
192
|
+
const state = retailSlot.get(ctx);
|
|
200
193
|
expect(state.callSeq).toBe(2);
|
|
201
194
|
expect(state.activity.map((a) => a.summary)).toEqual(["echoed a", "echoed b"]);
|
|
202
195
|
expect(state.activity.map((a) => a.tool)).toEqual(["echo", "echo"]);
|
|
@@ -205,28 +198,30 @@ describe("retailTool", () => {
|
|
|
205
198
|
test("a repeated identical call still changes the projection", async () => {
|
|
206
199
|
const ctx = makeCtx();
|
|
207
200
|
await echo.execute({ value: "same" }, ctx);
|
|
208
|
-
const first =
|
|
201
|
+
const first = retailSlot.get(ctx).callSeq;
|
|
209
202
|
await echo.execute({ value: "same" }, ctx);
|
|
210
|
-
expect(
|
|
203
|
+
expect(retailSlot.get(ctx).callSeq).toBeGreaterThan(first);
|
|
211
204
|
});
|
|
212
205
|
|
|
213
206
|
test("defaults to requiring authentication and does not run execute when blocked", async () => {
|
|
214
207
|
const ctx = makeCtx();
|
|
215
208
|
const result = await gated.execute({}, ctx);
|
|
216
|
-
expect(
|
|
209
|
+
expect(isToolFailure(result) && result.error).toContain("find_user_id_by_email");
|
|
217
210
|
});
|
|
218
211
|
|
|
219
212
|
test("a blocked call is still logged and still bumps callSeq", async () => {
|
|
220
213
|
const ctx = makeCtx();
|
|
221
214
|
await gated.execute({}, ctx);
|
|
222
|
-
const state =
|
|
215
|
+
const state = retailSlot.get(ctx);
|
|
223
216
|
expect(state.callSeq).toBe(1);
|
|
224
217
|
expect(state.activity[0]?.summary).toContain("blocked");
|
|
225
218
|
});
|
|
226
219
|
|
|
227
220
|
test("runs once authenticated", async () => {
|
|
228
221
|
const ctx = makeCtx();
|
|
229
|
-
|
|
222
|
+
retailSlot.update(ctx, (state) => {
|
|
223
|
+
state.authenticatedUserId = "olivia_ito_3591";
|
|
224
|
+
});
|
|
230
225
|
const result = await gated.execute({}, ctx);
|
|
231
226
|
expect(result).toEqual({ ok: true });
|
|
232
227
|
});
|
|
@@ -234,13 +229,13 @@ describe("retailTool", () => {
|
|
|
234
229
|
test("an error result is logged as an error, not through summary()", async () => {
|
|
235
230
|
const ctx = makeCtx();
|
|
236
231
|
await failing.execute({}, ctx);
|
|
237
|
-
expect(
|
|
232
|
+
expect(retailSlot.get(ctx).activity[0]?.summary).toBe("error: nope");
|
|
238
233
|
});
|
|
239
234
|
|
|
240
235
|
test("activity is capped so a long call cannot grow the payload", async () => {
|
|
241
236
|
const ctx = makeCtx();
|
|
242
237
|
for (let i = 0; i < 15; i++) await echo.execute({ value: String(i) }, ctx);
|
|
243
|
-
const state =
|
|
238
|
+
const state = retailSlot.get(ctx);
|
|
244
239
|
expect(state.callSeq).toBe(15);
|
|
245
240
|
expect(state.activity).toHaveLength(10);
|
|
246
241
|
expect(state.activity[0]?.summary).toBe("echoed 5");
|
|
@@ -249,7 +244,7 @@ describe("retailTool", () => {
|
|
|
249
244
|
test("concurrent calls serialize — no lost increments", async () => {
|
|
250
245
|
const ctx = makeCtx();
|
|
251
246
|
await Promise.all(Array.from({ length: 8 }, (_, i) => echo.execute({ value: String(i) }, ctx)));
|
|
252
|
-
const state =
|
|
247
|
+
const state = retailSlot.get(ctx);
|
|
253
248
|
expect(state.callSeq).toBe(8);
|
|
254
249
|
expect(new Set(state.activity.map((a) => a.seq)).size).toBe(state.activity.length);
|
|
255
250
|
});
|