diff --git a/extension/src/rules/__tests__/countdown-timer-redact.test.ts b/extension/src/rules/__tests__/countdown-timer-redact.test.ts
index ee73cc7..1231969 100644
--- a/extension/src/rules/__tests__/countdown-timer-redact.test.ts
+++ b/extension/src/rules/__tests__/countdown-timer-redact.test.ts
@@ -50,6 +50,18 @@ describe("matchesTimerPattern", () => {
expect(matchesTimerPattern("15 minutes remaining")).toBe(true);
});
+ it("matches urgency suffix synonyms (#203 item 21)", () => {
+ expect(matchesTimerPattern("5 hours to claim")).toBe(true);
+ expect(matchesTimerPattern("45 minutes to save")).toBe(true);
+ });
+
+ it("matches expiry-lead phrasings (#203 item 21)", () => {
+ expect(matchesTimerPattern("Sale ends in 3h")).toBe(true);
+ expect(matchesTimerPattern("Offer expires in 45 minutes")).toBe(true);
+ expect(matchesTimerPattern("Closes in 2d")).toBe(true);
+ expect(matchesTimerPattern("Ends in 30 seconds")).toBe(true);
+ });
+
it("does not match arbitrary numbers", () => {
expect(matchesTimerPattern("Item #123456")).toBe(false);
expect(matchesTimerPattern("Save 30%")).toBe(false);
@@ -125,6 +137,31 @@ describe("countdownTimerRedactRule", () => {
expect(document.querySelector(`.${PLACEHOLDER_CLASS}`)).not.toBeNull();
});
+ it("hides an expiry-lead countdown that decreased (#203 item 21)", () => {
+ document.body.innerHTML = `
Sale ends in 2h 15m
`;
+ countdownTimerRedactRule.apply(document.body);
+
+ const timer = document.querySelector("#t");
+ if (timer) {
+ timer.textContent = "Sale ends in 2h 14m";
+ }
+
+ jest.advanceTimersByTime(SNAPSHOT_DELAY_MS);
+
+ expect(document.querySelector("#t")).toBeNull();
+ expect(document.querySelector(`.${PLACEHOLDER_CLASS}`)).not.toBeNull();
+ });
+
+ it("leaves a static expiry-lead badge alone (decrement gate)", () => {
+ document.body.innerHTML = `Expires in 30 days
`;
+ countdownTimerRedactRule.apply(document.body);
+
+ jest.advanceTimersByTime(SNAPSHOT_DELAY_MS);
+
+ expect(document.querySelector("#t")).not.toBeNull();
+ expect(document.querySelector(`.${PLACEHOLDER_CLASS}`)).toBeNull();
+ });
+
it("leaves static clock-shaped text alone", () => {
document.body.innerHTML = `Posted at 12:34`;
countdownTimerRedactRule.apply(document.body);
diff --git a/extension/src/rules/__tests__/scarcity-redact.test.ts b/extension/src/rules/__tests__/scarcity-redact.test.ts
index efd7f55..2bcb365 100644
--- a/extension/src/rules/__tests__/scarcity-redact.test.ts
+++ b/extension/src/rules/__tests__/scarcity-redact.test.ts
@@ -46,6 +46,21 @@ describe("matchesScarcityPattern", () => {
"8 purchased in the past day",
"5 have it in their cart",
"3 in carts",
+ // Synonym evasion bypasses (#203 item 21).
+ "Just 3 left",
+ "Just 2 in stock",
+ "3 items left",
+ "5 units remaining",
+ "2 pieces left",
+ "While supplies last",
+ "While stocks last",
+ "Selling quickly",
+ "Going quickly",
+ "Flying off the shelves",
+ "Going off shelves",
+ "12 added to cart in the last hour",
+ "8 shoppers added this to their bag",
+ "5 others added to wishlist",
])("matches urgency phrasing: %s", (text) => {
expect(matchesScarcityPattern(text)).toBe(true);
});
@@ -67,6 +82,11 @@ describe("matchesScarcityPattern", () => {
"Ships in 2 days",
"Save 30%",
"Free shipping",
+ // "Just" alone is not an urgency signal.
+ "Just released",
+ "Just for you",
+ // Bare "added to cart" without a count is a UI confirmation, not scarcity.
+ "Added to cart",
])("leaves non-urgency text alone: %s", (text) => {
expect(matchesScarcityPattern(text)).toBe(false);
});
diff --git a/extension/src/rules/countdown-timer-redact.ts b/extension/src/rules/countdown-timer-redact.ts
index d733ece..a77fcfd 100644
--- a/extension/src/rules/countdown-timer-redact.ts
+++ b/extension/src/rules/countdown-timer-redact.ts
@@ -36,13 +36,20 @@ const COLON_PATTERN = /(?