@alexkroman1/aai-cli 5.3.0 → 5.5.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/README.md +51 -0
- package/dist/{_agent-D8zBb3M5.mjs → _agent-DMyOab9_.mjs} +2 -2
- package/dist/{_bundler-DFS4xxqE.mjs → _bundler-Cjaxa2wi.mjs} +1 -1
- package/dist/{_config-D3F9km8X.mjs → _config-5AEqhh-O.mjs} +2 -2
- package/dist/{_dev-server-BBUWo0sb.mjs → _dev-server-vV05Fnki.mjs} +3 -3
- package/dist/{_init-DU-sXH6S.mjs → _init-BZ9t_Kz-.mjs} +3 -56
- package/dist/{_slug-api-19R5kZ0U.mjs → _slug-api-DaqQJHk8.mjs} +1 -1
- package/dist/_templates-Bv8CR800.mjs +69 -0
- package/dist/_templates.d.ts +6 -0
- package/dist/{build-Xq1xxoSg.mjs → build-D_PgQOD4.mjs} +1 -1
- package/dist/cli.mjs +59 -17
- package/dist/{client-bundler-BWDkUeEP.mjs → client-bundler-yiWoXrgb.mjs} +3 -0
- package/dist/client-bundler.d.ts +9 -0
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-kSXFLxek.mjs → delete-DXilFBb1.mjs} +1 -1
- package/dist/{deploy-Ci0X1gp2.mjs → deploy-1eaXcfUw.mjs} +3 -3
- package/dist/{dev-g7sGt8qX.mjs → dev-gVNdGFYY.mjs} +1 -1
- package/dist/{init-BjeK8crW.mjs → init-DoU4_txp.mjs} +3 -3
- package/dist/login-C59ZHzuO.mjs +109 -0
- package/dist/login.d.ts +34 -0
- package/dist/scaffold/CLAUDE.md +190 -120
- package/dist/scaffold/package.json +3 -3
- package/dist/{secret-DOva9OGk.mjs → secret-CGAIAbUx.mjs} +1 -1
- package/dist/{storage-vM6HjDZZ.mjs → storage-CnhOayhm.mjs} +1 -1
- package/dist/templates/code-interpreter/agent.ts +1 -2
- package/dist/templates/dispatch-center/agent.test.ts +3 -3
- package/dist/templates/dispatch-center/agent.ts +7 -7
- package/dist/templates/dispatch-center/client.tsx +7 -2
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_create.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_get.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_triage.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +1 -1
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +1 -1
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +1 -1
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +1 -1
- package/dist/templates/embedded-assets/agent.ts +2 -3
- package/dist/templates/health-assistant/agent.ts +3 -4
- package/dist/templates/infocom-adventure/agent.ts +10 -12
- package/dist/templates/infocom-adventure/client.tsx +5 -5
- package/dist/templates/math-buddy/agent.ts +7 -6
- package/dist/templates/night-owl/agent.ts +2 -3
- package/dist/templates/personal-finance/agent.ts +1 -2
- package/dist/templates/pipeline-simple/agent.test.ts +13 -13
- package/dist/templates/pipeline-simple/agent.ts +5 -6
- package/dist/templates/pizza-ordering/agent.test.ts +13 -11
- package/dist/templates/pizza-ordering/agent.ts +5 -6
- package/dist/templates/retail/address.ts +29 -0
- package/dist/templates/retail/agent.test.ts +1030 -0
- package/dist/templates/retail/agent.ts +71 -0
- package/dist/templates/retail/authenticate.ts +34 -0
- package/dist/templates/retail/client.tsx +459 -0
- package/dist/templates/retail/refund.ts +19 -0
- package/dist/templates/retail/registry.test.ts +182 -0
- package/dist/templates/retail/resolve.test.ts +158 -0
- package/dist/templates/retail/resolve.ts +158 -0
- package/dist/templates/retail/seed.json +4260 -0
- package/dist/templates/retail/seed.test.ts +224 -0
- package/dist/templates/retail/shared.test.ts +192 -0
- package/dist/templates/retail/shared.ts +430 -0
- package/dist/templates/retail/store.test.ts +256 -0
- package/dist/templates/retail/store.ts +243 -0
- package/dist/templates/retail/swap.test.ts +186 -0
- package/dist/templates/retail/swap.ts +138 -0
- package/dist/templates/retail/system-prompt.md +111 -0
- package/dist/templates/retail/tools/cancel_pending_order.ts +82 -0
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +83 -0
- package/dist/templates/retail/tools/find_user_id_by_email.ts +32 -0
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +38 -0
- package/dist/templates/retail/tools/get_item_details.ts +29 -0
- package/dist/templates/retail/tools/get_order_details.ts +41 -0
- package/dist/templates/retail/tools/get_product_details.ts +33 -0
- package/dist/templates/retail/tools/get_user_details.ts +44 -0
- package/dist/templates/retail/tools/list_all_product_types.ts +24 -0
- package/dist/templates/retail/tools/modify_pending_order_address.ts +46 -0
- package/dist/templates/retail/tools/modify_pending_order_items.ts +102 -0
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +103 -0
- package/dist/templates/retail/tools/modify_user_address.ts +38 -0
- package/dist/templates/retail/tools/return_delivered_order_items.ts +101 -0
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +22 -0
- package/dist/templates/simple/agent.test.ts +18 -0
- package/dist/templates/simple/agent.ts +3 -0
- package/dist/templates/solo-rpg/agent.test.ts +2 -2
- package/dist/templates/solo-rpg/agent.ts +1 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +1 -1
- package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/oracle.ts +1 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
- package/dist/templates/solo-rpg/tools/update_state.ts +1 -1
- package/dist/templates/web-researcher/agent.ts +1 -2
- package/dist/typecheck.d.ts +18 -0
- package/dist/typecheck.mjs +8 -0
- package/dist/worker-bundler.d.ts +9 -1
- package/dist/worker-bundler.mjs +8 -0
- package/package.json +3 -3
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import seedJson from "./seed.json";
|
|
4
|
+
|
|
5
|
+
const AddressSchema = z.object({
|
|
6
|
+
address1: z.string(),
|
|
7
|
+
address2: z.string(),
|
|
8
|
+
city: z.string(),
|
|
9
|
+
state: z.string(),
|
|
10
|
+
country: z.string(),
|
|
11
|
+
zip: z.string(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const PaymentMethodSchema = z.discriminatedUnion("source", [
|
|
15
|
+
z.object({ source: z.literal("gift_card"), id: z.string(), balance: z.number() }),
|
|
16
|
+
z.object({ source: z.literal("paypal"), id: z.string() }),
|
|
17
|
+
z.object({
|
|
18
|
+
source: z.literal("credit_card"),
|
|
19
|
+
id: z.string(),
|
|
20
|
+
brand: z.string(),
|
|
21
|
+
last_four: z.string(),
|
|
22
|
+
}),
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const VariantSchema = z.object({
|
|
26
|
+
item_id: z.string(),
|
|
27
|
+
options: z.record(z.string(), z.string()),
|
|
28
|
+
available: z.boolean(),
|
|
29
|
+
price: z.number(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const SeedSchema = z.object({
|
|
33
|
+
products: z.record(
|
|
34
|
+
z.string(),
|
|
35
|
+
z.object({
|
|
36
|
+
name: z.string(),
|
|
37
|
+
product_id: z.string(),
|
|
38
|
+
variants: z.record(z.string(), VariantSchema),
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
41
|
+
users: z.record(
|
|
42
|
+
z.string(),
|
|
43
|
+
z.object({
|
|
44
|
+
user_id: z.string(),
|
|
45
|
+
name: z.object({ first_name: z.string(), last_name: z.string() }),
|
|
46
|
+
address: AddressSchema,
|
|
47
|
+
email: z.string(),
|
|
48
|
+
payment_methods: z.record(z.string(), PaymentMethodSchema),
|
|
49
|
+
orders: z.array(z.string()),
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
orders: z.record(
|
|
53
|
+
z.string(),
|
|
54
|
+
z.object({
|
|
55
|
+
order_id: z.string(),
|
|
56
|
+
user_id: z.string(),
|
|
57
|
+
address: AddressSchema,
|
|
58
|
+
items: z.array(
|
|
59
|
+
z.object({
|
|
60
|
+
name: z.string(),
|
|
61
|
+
product_id: z.string(),
|
|
62
|
+
item_id: z.string(),
|
|
63
|
+
price: z.number(),
|
|
64
|
+
options: z.record(z.string(), z.string()),
|
|
65
|
+
}),
|
|
66
|
+
),
|
|
67
|
+
status: z.string(),
|
|
68
|
+
fulfillments: z
|
|
69
|
+
.array(z.object({ tracking_id: z.array(z.string()), item_ids: z.array(z.string()) }))
|
|
70
|
+
.optional(),
|
|
71
|
+
payment_history: z.array(
|
|
72
|
+
z.object({
|
|
73
|
+
transaction_type: z.enum(["payment", "refund"]),
|
|
74
|
+
amount: z.number(),
|
|
75
|
+
payment_method_id: z.string(),
|
|
76
|
+
}),
|
|
77
|
+
),
|
|
78
|
+
}),
|
|
79
|
+
),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Parsed ONCE here so `store.ts` can cast at zero runtime cost. This test is
|
|
83
|
+
// what makes that cast honest.
|
|
84
|
+
const seed = SeedSchema.parse(seedJson);
|
|
85
|
+
|
|
86
|
+
describe("seed shape", () => {
|
|
87
|
+
test("matches the tau2 schema", () => {
|
|
88
|
+
expect(Object.keys(seed.products)).toHaveLength(50);
|
|
89
|
+
expect(Object.keys(seed.users)).toHaveLength(6);
|
|
90
|
+
expect(Object.keys(seed.orders)).toHaveLength(22);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("every order belongs to a seeded user and is listed on that user", () => {
|
|
94
|
+
for (const order of Object.values(seed.orders)) {
|
|
95
|
+
const user = seed.users[order.user_id];
|
|
96
|
+
expect(user, `order ${order.order_id} has no seeded user`).toBeDefined();
|
|
97
|
+
expect(user?.orders).toContain(order.order_id);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("every user's listed orders are all seeded", () => {
|
|
102
|
+
for (const user of Object.values(seed.users)) {
|
|
103
|
+
for (const orderId of user.orders) {
|
|
104
|
+
expect(seed.orders[orderId], `${user.user_id} lists unseeded ${orderId}`).toBeDefined();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("every ordered item resolves to a product variant with a matching price", () => {
|
|
110
|
+
for (const order of Object.values(seed.orders)) {
|
|
111
|
+
for (const item of order.items) {
|
|
112
|
+
const variant = seed.products[item.product_id]?.variants[item.item_id];
|
|
113
|
+
expect(
|
|
114
|
+
variant,
|
|
115
|
+
`${order.order_id}: ${item.item_id} missing from its product`,
|
|
116
|
+
).toBeDefined();
|
|
117
|
+
expect(variant?.price).toBe(item.price);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("every payment_method_id in an order resolves on its user", () => {
|
|
123
|
+
for (const order of Object.values(seed.orders)) {
|
|
124
|
+
const user = seed.users[order.user_id];
|
|
125
|
+
for (const payment of order.payment_history) {
|
|
126
|
+
expect(
|
|
127
|
+
user?.payment_methods[payment.payment_method_id],
|
|
128
|
+
`${order.order_id}: unknown method ${payment.payment_method_id}`,
|
|
129
|
+
).toBeDefined();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("names + zips are unique, so find_user_id_by_name_zip is unambiguous", () => {
|
|
135
|
+
const keys = Object.values(seed.users).map((u) =>
|
|
136
|
+
`${u.name.first_name}|${u.name.last_name}|${u.address.zip}`.toLowerCase(),
|
|
137
|
+
);
|
|
138
|
+
expect(new Set(keys).size).toBe(keys.length);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("emails are unique", () => {
|
|
142
|
+
const emails = Object.values(seed.users).map((u) => u.email.toLowerCase());
|
|
143
|
+
expect(new Set(emails).size).toBe(emails.length);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("seed coverage — each datum backs a specific test in agent.test.ts", () => {
|
|
148
|
+
test("every status the tools branch on is present", () => {
|
|
149
|
+
const statuses = new Set(Object.values(seed.orders).map((o) => o.status));
|
|
150
|
+
for (const s of ["pending", "processed", "delivered", "cancelled"]) {
|
|
151
|
+
expect(statuses, `no ${s} order seeded`).toContain(s);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("aarav_anderson has one gift card at $17.00 and no other method", () => {
|
|
156
|
+
const methods = seed.users.aarav_anderson_8794?.payment_methods ?? {};
|
|
157
|
+
expect(Object.keys(methods)).toEqual(["gift_card_7245904"]);
|
|
158
|
+
expect(methods.gift_card_7245904).toMatchObject({ source: "gift_card", balance: 17 });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("harper_brown holds no gift card, so a return must go to the original method", () => {
|
|
162
|
+
const methods = Object.values(seed.users.harper_brown_7363?.payment_methods ?? {});
|
|
163
|
+
expect(methods.some((m) => m.source === "gift_card")).toBe(false);
|
|
164
|
+
expect(methods.length).toBeGreaterThanOrEqual(2);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("olivia_ito holds all three payment-method kinds", () => {
|
|
168
|
+
const sources = Object.values(seed.users.olivia_ito_3591?.payment_methods ?? {})
|
|
169
|
+
.map((m) => m.source)
|
|
170
|
+
.sort();
|
|
171
|
+
expect(sources).toEqual(["credit_card", "gift_card", "paypal"]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("olivia_ito has three pending orders, so ordinal shorthand must disambiguate", () => {
|
|
175
|
+
const pending = (seed.users.olivia_ito_3591?.orders ?? []).filter(
|
|
176
|
+
(id) => seed.orders[id]?.status === "pending",
|
|
177
|
+
);
|
|
178
|
+
expect(pending).toHaveLength(3);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("#W4316152 carries a duplicate item id", () => {
|
|
182
|
+
const items = seed.orders["#W4316152"]?.items.map((i) => i.item_id) ?? [];
|
|
183
|
+
expect(items).toEqual(["7292993796", "7292993796"]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("#W1840144 carries a duplicate item id on a non-gift-card order", () => {
|
|
187
|
+
const order = seed.orders["#W1840144"];
|
|
188
|
+
const dupes = (order?.items ?? []).filter((i) => i.item_id === "8590708195");
|
|
189
|
+
expect(dupes).toHaveLength(2);
|
|
190
|
+
expect(order?.payment_history[0]?.payment_method_id).toBe("paypal_2306935");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("the exchange arithmetic every test asserts is reachable", () => {
|
|
194
|
+
const cases: [string, string, string, number][] = [
|
|
195
|
+
// product_id, from item, to item, expected diff
|
|
196
|
+
["1762337868", "1304426904", "4725166838", 36.32], // exceeds aarav's $17
|
|
197
|
+
["9832717871", "7292993796", "3909406921", 3.45], // fits
|
|
198
|
+
["4354588079", "6242772310", "6200867091", -40.86], // refund direction
|
|
199
|
+
["4760268021", "8997785118", "6017636844", -382.03], // pending modify refund
|
|
200
|
+
["4760268021", "6017636844", "9844888101", 167.37], // exceeds anya's $51 card
|
|
201
|
+
];
|
|
202
|
+
for (const [productId, from, to, diff] of cases) {
|
|
203
|
+
const variants = seed.products[productId]?.variants;
|
|
204
|
+
const a = variants?.[from];
|
|
205
|
+
const b = variants?.[to];
|
|
206
|
+
expect(a, `${productId}: ${from} trimmed away`).toBeDefined();
|
|
207
|
+
expect(b, `${productId}: ${to} trimmed away`).toBeDefined();
|
|
208
|
+
expect(b?.available, `${to} must be available to be an exchange target`).toBe(true);
|
|
209
|
+
expect(Math.round(((b?.price ?? 0) - (a?.price ?? 0)) * 100) / 100).toBe(diff);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("an unavailable variant survives trimming for the 'not available' path", () => {
|
|
214
|
+
expect(seed.products["9832717871"]?.variants["6454334990"]?.available).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("every product keeps at least two variants, so an exchange always has a target", () => {
|
|
218
|
+
for (const [pid, product] of Object.entries(seed.products)) {
|
|
219
|
+
expect(Object.keys(product.variants).length, `${pid} has too few variants`).toBeGreaterThan(
|
|
220
|
+
1,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import seedJson from "./seed.json";
|
|
3
|
+
import type { RetailState, Store } from "./shared.ts";
|
|
4
|
+
import { buildScriptBullets, DEMO_PERSONAS, storeView } from "./shared.ts";
|
|
5
|
+
|
|
6
|
+
function makeState(authenticatedUserId: string | null): RetailState {
|
|
7
|
+
return {
|
|
8
|
+
store: structuredClone(seedJson) as unknown as Store,
|
|
9
|
+
authenticatedUserId,
|
|
10
|
+
callSeq: 3,
|
|
11
|
+
activity: [{ seq: 3, tool: "get_order_details", summary: "read #W5866402", at: 0 }],
|
|
12
|
+
focus: { orderId: "#W5866402" },
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("storeView", () => {
|
|
17
|
+
test("projects nothing identifying before authentication", () => {
|
|
18
|
+
const view = storeView({ retail: makeState(null) });
|
|
19
|
+
expect(view.customer).toBeNull();
|
|
20
|
+
expect(view.orders).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("an absent state slot degrades to an empty view", () => {
|
|
24
|
+
const view = storeView({});
|
|
25
|
+
expect(view.customer).toBeNull();
|
|
26
|
+
expect(view.orders).toEqual([]);
|
|
27
|
+
expect(view.callSeq).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("projects the authenticated customer and only their orders", () => {
|
|
31
|
+
const view = storeView({ retail: makeState("olivia_ito_3591") });
|
|
32
|
+
expect(view.customer?.name).toBe("Olivia Ito");
|
|
33
|
+
expect(view.customer?.email).toBe("olivia.ito5204@example.com");
|
|
34
|
+
expect(view.orders.map((o) => o.orderId).sort((a, b) => a.localeCompare(b))).toEqual(
|
|
35
|
+
["#W3657213", "#W5353646", "#W5442520", "#W5866402", "#W7941031"].sort((a, b) =>
|
|
36
|
+
a.localeCompare(b),
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("projects live gift-card balances, since refunds move them", () => {
|
|
42
|
+
const state = makeState("olivia_ito_3591");
|
|
43
|
+
const view = storeView({ retail: state });
|
|
44
|
+
const card = view.customer?.paymentMethods.find((m) => m.id === "gift_card_7794233");
|
|
45
|
+
expect(card?.balance).toBe(56);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("no other customer's email, address, or payment method reaches the client", () => {
|
|
49
|
+
const view = storeView({ retail: makeState("olivia_ito_3591") });
|
|
50
|
+
const json = JSON.stringify(view);
|
|
51
|
+
for (const leaked of [
|
|
52
|
+
"aarav.anderson9752@example.com",
|
|
53
|
+
"harper.brown3965@example.com",
|
|
54
|
+
"emma.smith3991@example.com",
|
|
55
|
+
"gift_card_7245904",
|
|
56
|
+
"paypal_2306935",
|
|
57
|
+
"#W4316152",
|
|
58
|
+
"#W1840144",
|
|
59
|
+
]) {
|
|
60
|
+
expect(json, `projection leaked ${leaked}`).not.toContain(leaked);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("carries callSeq, activity and focus through", () => {
|
|
65
|
+
const view = storeView({ retail: makeState("olivia_ito_3591") });
|
|
66
|
+
expect(view.callSeq).toBe(3);
|
|
67
|
+
expect(view.activity).toHaveLength(1);
|
|
68
|
+
expect(view.focus.orderId).toBe("#W5866402");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("order totals sum the payments, not the items", () => {
|
|
72
|
+
const view = storeView({ retail: makeState("olivia_ito_3591") });
|
|
73
|
+
const delivered = view.orders.find((o) => o.orderId === "#W5866402");
|
|
74
|
+
expect(delivered?.total).toBe(3203.78);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("DEMO_PERSONAS", () => {
|
|
79
|
+
test("lists every seeded customer", () => {
|
|
80
|
+
expect(DEMO_PERSONAS).toHaveLength(Object.keys(seedJson.users).length);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("every persona's email, name and zip match its seed record", () => {
|
|
84
|
+
const users = Object.values((structuredClone(seedJson) as unknown as Store).users);
|
|
85
|
+
for (const persona of DEMO_PERSONAS) {
|
|
86
|
+
const user = users.find((u) => u.email === persona.email);
|
|
87
|
+
expect(user, `no seeded customer has email ${persona.email}`).toBeDefined();
|
|
88
|
+
expect(`${user?.name.first_name} ${user?.name.last_name}`).toBe(persona.name);
|
|
89
|
+
expect(user?.address.zip).toBe(persona.zip);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("every persona carries a hint, so a user can pick a path deliberately", () => {
|
|
94
|
+
for (const persona of DEMO_PERSONAS) {
|
|
95
|
+
expect(persona.hint.length, persona.name).toBeGreaterThan(20);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("swapOptions", () => {
|
|
101
|
+
test("empty when no order is focused", () => {
|
|
102
|
+
const state = makeState("olivia_ito_3591");
|
|
103
|
+
state.focus = {};
|
|
104
|
+
expect(storeView({ retail: state }).swapOptions).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("lists available alternatives for the focused order's items", () => {
|
|
108
|
+
// makeState focuses #W5866402 — Espresso Machine + Sneakers.
|
|
109
|
+
const view = storeView({ retail: makeState("olivia_ito_3591") });
|
|
110
|
+
expect(view.swapOptions).toHaveLength(2);
|
|
111
|
+
const espresso = view.swapOptions.find((s) => s.itemId === "6242772310");
|
|
112
|
+
expect(espresso?.itemName).toBe("Espresso Machine");
|
|
113
|
+
expect(espresso?.alternatives.length).toBeGreaterThan(0);
|
|
114
|
+
expect(espresso?.alternatives.map((a) => a.itemId)).not.toContain("6242772310");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("never offers an unavailable variant as a target", () => {
|
|
118
|
+
const state = makeState("aarav_anderson_8794");
|
|
119
|
+
state.focus = { orderId: "#W4316152" };
|
|
120
|
+
const view = storeView({ retail: state });
|
|
121
|
+
const ids = view.swapOptions.flatMap((s) => s.alternatives.map((a) => a.itemId));
|
|
122
|
+
// 6454334990 is a Tea Kettle variant with available: false.
|
|
123
|
+
expect(ids).not.toContain("6454334990");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("caps both axes so the payload stays bounded", () => {
|
|
127
|
+
const state = makeState("aarav_anderson_8794");
|
|
128
|
+
state.focus = { orderId: "#W9311069" }; // five items
|
|
129
|
+
const view = storeView({ retail: state });
|
|
130
|
+
expect(view.swapOptions.length).toBeLessThanOrEqual(3);
|
|
131
|
+
for (const option of view.swapOptions) {
|
|
132
|
+
expect(option.alternatives.length).toBeLessThanOrEqual(4);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("a focus on another customer's order projects nothing", () => {
|
|
137
|
+
const state = makeState("olivia_ito_3591");
|
|
138
|
+
state.focus = { orderId: "#W4316152" }; // aarav's
|
|
139
|
+
const view = storeView({ retail: state });
|
|
140
|
+
expect(view.swapOptions).toEqual([]);
|
|
141
|
+
expect(JSON.stringify(view)).not.toContain("#W4316152");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("buildScriptBullets", () => {
|
|
146
|
+
test("pre-auth bullets name both lookup paths using the first persona", () => {
|
|
147
|
+
const bullets = buildScriptBullets(makeState(null));
|
|
148
|
+
expect(bullets.join(" ")).toContain(DEMO_PERSONAS[0]?.email ?? "");
|
|
149
|
+
expect(bullets.join(" ")).toContain(DEMO_PERSONAS[0]?.zip ?? "");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("undefined state yields the pre-auth bullets", () => {
|
|
153
|
+
expect(buildScriptBullets(undefined).length).toBeGreaterThan(0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("a customer with pending orders is offered cancel and address change", () => {
|
|
157
|
+
const bullets = buildScriptBullets(makeState("olivia_ito_3591")).join(" ").toLowerCase();
|
|
158
|
+
expect(bullets).toContain("cancel");
|
|
159
|
+
expect(bullets).toContain("address");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("a customer with a delivered order is offered a return naming a real item", () => {
|
|
163
|
+
const bullets = buildScriptBullets(makeState("olivia_ito_3591")).join(" ");
|
|
164
|
+
expect(bullets.toLowerCase()).toContain("return");
|
|
165
|
+
expect(bullets).toMatch(/Espresso Machine|Sneakers/);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("the exchange bullet names a real alternative option, not 'a different option'", () => {
|
|
169
|
+
const state = makeState("olivia_ito_3591");
|
|
170
|
+
const bullets = buildScriptBullets(state);
|
|
171
|
+
const exchange = bullets.find((b) => b.toLowerCase().includes("exchange"));
|
|
172
|
+
expect(exchange).toBeDefined();
|
|
173
|
+
// It must quote option values that really exist on that product.
|
|
174
|
+
const espresso = state.store.products["4354588079"];
|
|
175
|
+
const optionWords = Object.values(espresso?.variants ?? {})
|
|
176
|
+
.filter((v) => v.available)
|
|
177
|
+
.flatMap((v) => Object.values(v.options));
|
|
178
|
+
expect(optionWords.some((word) => exchange?.includes(word))).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("no return bullet when the customer has no delivered order", () => {
|
|
182
|
+
// anya_garcia_3271 has cancelled + processed + pending, no delivered.
|
|
183
|
+
const bullets = buildScriptBullets(makeState("anya_garcia_3271")).join(" ").toLowerCase();
|
|
184
|
+
expect(bullets).not.toContain("return");
|
|
185
|
+
expect(bullets).not.toContain("exchange");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("bullets stay short enough to render in a sidebar", () => {
|
|
189
|
+
const bullets = buildScriptBullets(makeState("olivia_ito_3591"));
|
|
190
|
+
expect(bullets.length).toBeLessThanOrEqual(6);
|
|
191
|
+
});
|
|
192
|
+
});
|