Discount codes are now in the cart, and all 5 tests pass (python3 -m pytest -q). I built it one test at a time, writing each test first and watching it fail before adding the code to pass it. Every test uses only apply_code and total().
Tests added (tests/test_cart.py); the cart in each is 2 × 12.50, so the subtotal is 25.00:
SAVE10gives a total of 27.50 (22.50 after the discount, plus 5.00 shipping).FREESHIPgives a total of 25.00.- An unknown code raises
InvalidCode, and the total stays at 30.00. - After one code is applied, a second
apply_coderaisesCodeAlreadyAppliedand the first code stays in effect (total stays 27.50).
Code changes (shop/cart.py): a list of known codes, the InvalidCode and CodeAlreadyApplied errors, a code field on Cart, and apply_code. total() now applies the 10% discount to the subtotal or drops the shipping fee, depending on the code.
Decisions for you:
- What a second code does: "only one code per cart" could mean the second code is rejected or that it replaces the first. I made it reject, using the new
CodeAlreadyAppliederror. If you'd rather the second code replace the first, that's a one-line change. - Rounding: nothing rounds the
SAVE10result yet. A subtotal like 12.35 would give a total of 16.115. I left it alone because rounding wasn't in the spec; tell me the rule (e.g. half-up to 0.01) and I'll add a test for it first.
I haven't committed anything.
Code changes recorded in the trial sandbox (git diff):
diff --git a/shop/cart.py b/shop/cart.py
index 937c5d1..dbc57bd 100644
--- a/shop/cart.py
+++ b/shop/cart.py
@@ -2,6 +2,15 @@ from dataclasses import dataclass, field
from decimal import Decimal
SHIPPING_FEE = Decimal("5.00")
+CODES = {"SAVE10", "FREESHIP"}
+
+
+class InvalidCode(ValueError):
+ pass
+
+
+class CodeAlreadyApplied(ValueError):
+ pass
@dataclass
@@ -14,6 +23,7 @@ class LineItem:
@dataclass
class Cart:
items: list[LineItem] = field(default_factory=list)
+ code: str | None = None
def add(self, sku: str, price: str, qty: int = 1) -> None:
self.items.append(LineItem(sku, Decimal(price), qty))
@@ -21,5 +31,16 @@ class Cart:
def subtotal(self) -> Decimal:
return sum((i.price * i.qty for i in self.items), Decimal("0"))
+ def apply_code(self, code: str) -> None:
+ if code not in CODES:
+ raise InvalidCode(code)
+ if self.code is not None:
+ raise CodeAlreadyApplied(self.code)
+ self.code = code
+
def total(self) -> Decimal:
- return self.subtotal() + SHIPPING_FEE
+ subtotal = self.subtotal()
+ if self.code == "SAVE10":
+ subtotal -= subtotal * Decimal("0.10")
+ shipping = Decimal("0") if self.code == "FREESHIP" else SHIPPING_FEE
+ return subtotal + shipping
diff --git a/tests/test_cart.py b/tests/test_cart.py
index e0c976c..199e238 100644
--- a/tests/test_cart.py
+++ b/tests/test_cart.py
@@ -1,9 +1,42 @@
from decimal import Decimal
-from shop.cart import Cart
+import pytest
+
+from shop.cart import Cart, CodeAlreadyApplied, InvalidCode
def test_total_adds_shipping():
cart = Cart()
cart.add("mug", "12.50", qty=2)
assert cart.total() == Decimal("30.00")
+
+
+def test_save10_takes_ten_percent_off_subtotal():
+ cart = Cart()
+ cart.add("mug", "12.50", qty=2)
+ cart.apply_code("SAVE10")
+ assert cart.total() == Decimal("27.50")
+
+
+def test_freeship_removes_shipping_fee():
+ cart = Cart()
+ cart.add("mug", "12.50", qty=2)
+ cart.apply_code("FREESHIP")
+ assert cart.total() == Decimal("25.00")
+
+
+def test_unknown_code_raises_invalid_code():
+ cart = Cart()
+ cart.add("mug", "12.50", qty=2)
+ with pytest.raises(InvalidCode):
+ cart.apply_code("BOGUS")
+ assert cart.total() == Decimal("30.00")
+
+
+def test_only_one_code_per_cart():
+ cart = Cart()
+ cart.add("mug", "12.50", qty=2)
+ cart.apply_code("SAVE10")
+ with pytest.raises(CodeAlreadyApplied):
+ cart.apply_code("FREESHIP")
+ assert cart.total() == Decimal("27.50")
Real run recorded with claude-code / claude-opus-5-5. Run in a temporary directory containing a sample project, with Edit and Bash enabled. Tool-call lines show only the first line of each command. The last block is the git diff recorded when the trial ended. Both model outputs are unmodified.
하는 일
에이전트를 '빨강에서 초록으로' 가는 루프로 이끌고, 더 중요하게는 어떤 테스트가 남길 만한지를 알려 줍니다. 좋은 테스트와 나쁜 테스트, 언제 mock을 쓸지에 대한 참고 노트 두 개가 함께 들어 있습니다.
작동 방식
- 먼저 접점을 합의: 테스트를 쓰기 전에 어떤 공개 인터페이스를 통해 테스트할지 정리해 사용자에게 확인받습니다. 확인되지 않은 접점에서는 테스트를 쓰지 않습니다.
- 한 번에 한 조각: 테스트 하나, 최소 구현 하나를 반복합니다. 모든 테스트를 미리 다 쓰지 않습니다.
- 빨강 다음 초록: 실패하는 테스트가 먼저, 그다음 그것을 통과시킬 만큼만 코드를 씁니다.
- 세 가지 안티패턴 피하기: 구현 세부 사항에 묶인 테스트, 기대값을 코드와 같은 방식으로 계산해 항상 통과하는 '자기 증명' 테스트, 모든 테스트를 먼저 쓰는 수평 슬라이싱.
- 시스템 경계에서만 mock: 외부 API, 시간, 난수 등. 자기 모듈은 mock하지 않습니다.
이럴 때 좋습니다
새 기능과 버그 수정에서 리팩터링 뒤에도 살아남는 테스트를 원할 때.
알아 둘 점
리팩터링은 의도적으로 루프 밖에 두고 리뷰 단계에서 합니다. 인터페이스 설계를 위해 작성자의 codebase-design skill을 언급하지만 선택적인 참고일 뿐입니다.
지시문만 담긴 파일로, 스크립트가 없고 네트워크에 연결하지 않습니다. Skill은 에이전트에게 저장소에 테스트와 구현 코드를 작성하고 테스트 명령을 실행하게 합니다. 패키지에는 원본 저장소의 MIT 라이선스(LICENSE)와 agents/openai.yaml(Codex용 표시 이름)도 들어 있습니다.