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
6 changes: 4 additions & 2 deletions domains/doordash.com/remove_cart_item.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@
if (projectedBefore.truncated || new TextEncoder().encode(JSON.stringify(projectedBefore)).byteLength > 170000) return fail("blocked", "The complete cart exceeds safe bounds. Nothing was changed.");
const lines = before.orders[0].orderItems;
if (!Array.isArray(lines) || lines.length < 2 || lines.length > 100 || lines.some(l => !l || !ident(l.id))) return fail("blocked", "Last-line removal or an incomplete/unsupported cart is not supported.");
if (lines.some(l => l.isPrescriptionItem !== false || !ident(l.item?.id) || String(l.item?.storeId) !== input.expected_store_id || !Array.isArray(l.options) || l.options.some(o => !o || !ident(o.id)) || typeof l.quantity !== "number" || !Number.isFinite(l.quantity) || l.quantity <= 0)) return fail("blocked", "Prescription, incomplete or conflicting cart lines are not supported. Nothing was changed.");
if (lines.some(l => l.isPrescriptionItem !== false || !ident(l.item?.id) || (l.item?.storeId != null && String(l.item.storeId) !== input.expected_store_id) || !Array.isArray(l.options) || l.options.some(o => !o || !ident(o.id)) || typeof l.quantity !== "number" || !Number.isFinite(l.quantity) || l.quantity <= 0)) return fail("blocked", "Prescription, incomplete or conflicting cart lines are not supported. Nothing was changed.");
if (lines.filter(l => String(l.id) === input.line_id).length !== 1) return fail("not_found", "That exact line is not present exactly once in this cart.");
if (await revision(before) !== input.expected_revision) return fail("stale_state", "The cart changed after get_cart. Read it again; no write was sent.");
await readAccount();
Expand Down Expand Up @@ -221,7 +221,9 @@
// The site may reprice the remaining lines (promotions, bundles); their identity, quantity and options must not change.
const repriced = new Map((Array.isArray(after.orders?.[0]?.orderItems) ? after.orders[0].orderItems : []).map(l => [String(l?.id), l]));
const kept = l => { const now = repriced.get(String(l.id)); return { ...l, singlePrice: now?.singlePrice, priceOfTotalQuantity: now?.priceOfTotalQuantity, priceDisplayString: now?.priceDisplayString }; };
const expected = { ...before, subtotal: after.subtotal, total: after.total, orders: before.orders.map(o => ({ ...o, orderItems: o.orderItems.filter(l => String(l.id) !== input.line_id).map(kept), lineItems: after.orders?.[0]?.lineItems })) };
// The site ids the order after its first line, so removing that line renames the order to a remaining line.
const renamed = o => String(o.id) === input.line_id && o.orderItems.some(l => String(l.id) !== input.line_id && String(l.id) === String(after.orders?.[0]?.id)) ? after.orders[0].id : o.id;
const expected = { ...before, subtotal: after.subtotal, total: after.total, orders: before.orders.map(o => ({ ...o, id: renamed(o), orderItems: o.orderItems.filter(l => String(l.id) !== input.line_id).map(kept), lineItems: after.orders?.[0]?.lineItems })) };
// The order of the remaining lines is not part of their identity.
const byLineId = c => ({ ...c, orders: Array.isArray(c.orders) ? c.orders.map(o => (o && Array.isArray(o.orderItems) ? { ...o, orderItems: [...o.orderItems].sort((x, y) => String(x?.id).localeCompare(String(y?.id))) } : o)) : c.orders });
if (JSON.stringify(canonical(byLineId(expected))) !== JSON.stringify(canonical(byLineId(after)))) return fail("outcome_unknown", "The exact removal with unchanged other lines was not confirmed. Inspect manually; do not retry.", true);
Expand Down
2 changes: 1 addition & 1 deletion registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -1591,7 +1591,7 @@
"url_patterns_reason": "Narrowed to www.doordash.com, the consumer marketplace: these tools call its same-origin GraphQL endpoint with the page's session; other doordash.com hosts are separate products such as the merchant portal and help center.",
"status": "draft",
"last_verified": null,
"notes": "Exact non-last ordinary personal cart-line removal with account/store/revision, complete non-prescription line checks, an uncertain-write guard and observed postconditions. One attempt, no automatic retries. The guard refuses further removals from the cart until a fresh get_cart revision shows the cart changed; the postcondition lets the site reprice or reorder the remaining lines but not change their identity, quantity or options. Independent Kernel/local-extension QA passed on 2026-09-30 for earlier source; the guard and postcondition changed since and need fresh independent QA. All mutation success is fixture-only, never live-verified. Cross-tab races are not atomically excluded; final order/payment execution is absent."
"notes": "Exact non-last ordinary personal cart-line removal with account/store/revision, complete non-prescription line checks, an uncertain-write guard and observed postconditions. One attempt, no automatic retries. The guard refuses further removals from the cart until a fresh get_cart revision shows the cart changed; the postcondition lets the site reprice or reorder the remaining lines but not change their identity, quantity or options. Independent Kernel/local-extension QA passed on 2026-09-30 for earlier source; the guard and postcondition changed since and need fresh independent QA. Live on 2026-10-27, DoorDash returned item.storeId null for every line of a signed-in two-line cart, so the earlier per-line store check refused every removal as conflicting; a null line store id is now accepted and the cart's restaurant id remains the store check. DoorDash also ids the cart's order after its first line, so removing that line renames the order to a remaining line; the postcondition accepts only that rename. Live check of this source on 2026-10-28 (not independent QA), signed in, three-line cart: removing the first of three and then the second of two each returned removed with the other lines kept; stale revision and wrong store returned stale_state, unknown cart and line not_found, wrong account wrong_account, and the last line was refused, none with a write; the cart was emptied afterwards. Cross-tab races are not atomically excluded; final order/payment execution is absent."
},
{
"domain": "doordash.com",
Expand Down
18 changes: 17 additions & 1 deletion tests/doordash-cart.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const sources = Object.fromEntries(["get_cart", "get_checkout_summary", "remove_
const plain = value => JSON.parse(JSON.stringify(value));
const email = "shopper@example.invalid";
const account = () => ({ email, isGuest: false, defaultAddress: { city: "Testville", state: "ZZ", zipCode: "00000", countryCode: "US", entryCode: "secret-entry" } });
const item = (id, quantity = 1) => ({ id, quantity, singlePrice: 700, priceOfTotalQuantity: 700 * quantity, priceDisplayString: "$7.00", cartItemStatusType: "CART_ITEM_STATUS_TYPE_UNSPECIFIED", isPrescriptionItem: false, options: [{ id: "extra-1", name: "Tofu", quantity: 1, price: 100 }], item: { id: "menu-" + id, name: "Fabricated bowl", storeId: "1001" } });
const item = (id, quantity = 1) => ({ id, quantity, singlePrice: 700, priceOfTotalQuantity: 700 * quantity, priceDisplayString: "$7.00", cartItemStatusType: "CART_ITEM_STATUS_TYPE_UNSPECIFIED", isPrescriptionItem: false, options: [{ id: "extra-1", name: "Tofu", quantity: 1, price: 100 }], item: { id: "menu-" + id, name: "Fabricated bowl", storeId: null } });
const cart = () => ({ id: "cart-1", groupCart: false, isBundle: false, isCatering: false, isMerchantShipping: false, isPrescriptionDelivery: false, isConsumerPickup: false, currencyCode: "USD", subtotal: 1400, total: 1700, restaurant: { id: "1001", name: "Fabricated Kitchen" }, orders: [{ id: "order-1", isSubCartFinalized: false, orderItems: [item("line-1"), item("line-2")], lineItems: [{ label: "Service fee", finalMoney: { unitAmount: 300, displayString: "$3.00" } }] }] });

function conforms(s, v) {
Expand Down Expand Up @@ -245,6 +245,22 @@ test("remaining lines listed in another order still confirm the removal", async
assert.equal(out.status, "ok"); assert.deepEqual(out.remaining_cart.lines.map(l => l.line_id), ["line-3", "line-2"]); assert.equal(h.store.size, 0);
});

test("removing the line the order is named after accepts the order renamed to a remaining line", async () => {
const c = cart(); c.orders[0].id = "line-1";
const h = harness({ carts: [c], afterWrite: c => { c.orders[0].id = c.orders[0].orderItems[0].id; } });
const out = await h.run("remove_cart_item", await removalInput(h));
assert.equal(out.status, "ok"); assert.deepEqual(out.remaining_cart.lines.map(l => l.line_id), ["line-2"]); assert.equal(h.store.size, 0);
});

for (const [label, orderId, renamedTo] of [["to an id that is not a remaining line", "line-1", "order-9"], ["when it was not named after the removed line", "order-1", "line-2"]]) {
test(`an order renamed ${label} is not confirmed`, async () => {
const c = cart(); c.orders[0].id = orderId;
const h = harness({ carts: [c], afterWrite: c => { c.orders[0].id = renamedTo; } });
const out = await h.run("remove_cart_item", await removalInput(h));
assert.equal(out.error_kind, "outcome_unknown"); assert.equal(h.store.size, 1);
});
}

test("an uncertain write blocks the unchanged cart but not a fresh revision of a changed one", async () => {
const h = harness({ noRemoval: true }); const input = await removalInput(h);
assert.equal((await h.run("remove_cart_item", input)).error_kind, "outcome_unknown"); assert.equal(h.store.size, 1);
Expand Down
Loading