Expose PChome rescore review metrics
This commit is contained in:
@@ -74,6 +74,7 @@ ALERT_WEBHOOK_PASSWORD=your_secure_webhook_password_here
|
||||
AUTO_FIX_ENABLED=true
|
||||
|
||||
# --- GitLab CI/CD ---
|
||||
GITLAB_ENABLED=false
|
||||
GITLAB_URL=http://192.168.0.110:8929
|
||||
GITLAB_TOKEN=your_gitlab_token_here
|
||||
GITLAB_PROJECT_ID=1
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.450 補 PChome 覆核 fast-count UI 語意與重算可採用指標:預設全量覆核頁跳過 exact count 時,模板會顯示「約」作為快取總數提示;搜尋、分類、單一狀態仍是精準總數。`fetch_competitor_coverage()` 同步輸出 `rescore_accepted_count`,讓 Dashboard、daily/growth 與 OpenClaw 摘要能把「重算可採用待審」從一般覆核隊列拆出來。
|
||||
- V10.449 修正 PChome 覆核 exact count 條件:只有預設「全部覆核、無搜尋、無分類」頁跳過 exact count;只要有搜尋詞、分類篩選或單一 review status,就保留精準總數,避免分頁資訊失準。
|
||||
- V10.448 讓 PChome 覆核「全部」頁跳過 exact count:`review_status=all` 使用 shared overview cache 的待處理總數作為分頁總數提示,只查當頁 50 筆;單一狀態分流仍保留 exact count,降低全量覆核頁互動成本。
|
||||
- V10.447 反轉 PChome 覆核頁查詢方向:review queue page 先從最新 `competitor_match_attempts` 的可覆核狀態縮小候選,再 join ACTIVE 商品與最新價,並用 `NOT EXISTS` 排除已有有效 identity_v2 正式價;避免每次「全部覆核」先掃全站 ACTIVE 商品。
|
||||
|
||||
@@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.449"
|
||||
SYSTEM_VERSION = "V10.450"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **最後更新**: 2026-05-24 (台北時間)
|
||||
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 備援預設關閉
|
||||
> **適用版本**: V10.449
|
||||
> **適用版本**: V10.450
|
||||
|
||||
---
|
||||
|
||||
@@ -81,6 +81,7 @@ SQL漏斗(~300筆)
|
||||
- 排程閉環:`run_pchome_match_backfill_task` 每日 10:30 執行,補抓 PChome 待比對商品、寫入歷史價格,再重算 `strategy='product_pick'` 清單。
|
||||
- PChome / MOMO 競價摘要出口 `services/competitor_intel_repository.py` 使用 30 分鐘共享快取(`COMPETITOR_INTEL_CACHE_TTL_SECONDS` 可調),避免 `/growth_analysis`、`/daily_sales`、PPT/AI 報表每次請求重跑昂貴覆蓋率與價差趨勢查詢;`run_competitor_price_feeder_task` 與 PChome backfill 完成後會主動清除快取。快取只包摘要輸出,不改 matcher 的高信心門檻與 identity_v2 準確性規則。
|
||||
- 商品看板第一屏:`/` 的 V2 看板直接以 `products`、`price_records`、`competitor_prices`、`competitor_match_attempts`、`competitor_match_reviews`、`ai_price_recommendations` 顯示比對覆蓋率、PChome 優勢、MOMO 威脅、AI 挑品、待比對優先清單與 PChome 覆核隊列;`filter=ai_picks` 可查看 50 品 AI 挑品列表,`filter=pchome_review` 可直接查看需人工處理的比價覆核 SKU,並以 DB 分頁支援 search/category/status 後的完整隊列,不得只截前 50 筆。覆核狀態篩選必須至少包含全部、需單位價、已排除、低信心、價格過期、找不到同款與人工閉環,讓人工可依 matcher 診斷類型分批處理。列內顯示候選 PChome 商品、候選價、match score、單位價換算摘要、人工動作與 matcher 診斷原因標籤(品牌不符、商品線不符、容量差異、組合差異、需單位價、價差極端等),不得只顯示籠統「待比對」。`/api/export/excel/pchome-review` 必須匯出同一套覆核隊列、人工處置、候選 PChome、單位價比較與原始診斷,讓人工覆核、簡報與後續 AI 分析共用同一份證據。`/api/pchome-review/<sku>/decision` 是人工閉環入口:`accept_identity` 才可把候選寫入 `competitor_prices` 與 `competitor_price_history` 並打上 `manual_review/manual_accept/identity_v2`;`reject_identity`、`unit_price_required` 與 `needs_research` 只寫 `competitor_match_reviews` 並追加 manual attempt,不得把不同販售組合或否決候選灌入正式價差。PChome feeder 後續搜尋同一候選時必須讀取 `competitor_match_reviews`:已否決候選寫 `manual_rejected` 並跳過正式寫入,且必須繼續評估下一個候選,不能讓已否決候選長期阻塞同 SKU;已標記單位價候選寫 `manual_unit_price_required`;已要求補搜尋候選寫 `manual_needs_research` 並停留在覆核隊列;已採用候選可保守補到最低門檻並保留 `manual_review/manual_accept` 標籤。搜尋候選池只有強同款分數達 `0.90` 才可提前停止,避免 0.76 灰區候選卡掉後續更精準搜尋詞。人工 `reject_identity`、`unit_price_required`、`needs_research` 若命中當前正式候選,必須將同候選 `competitor_prices` 過期,不得繼續顯示正式總價差。商品列表必須將 `manual_rejected`、`manual_unit_price_required`、`manual_needs_research` 顯示為明確人工閉環狀態,不可回落成籠統「待比對」。`fetch_competitor_coverage()` 必須輸出人工採用、人工否決、人工單位價與採用率,daily/growth/PPT 共用 payload 必須顯示人工閉環成效,避免只呈現待審數。商品看板深度快取同時寫入 `data/dashboard_full_cache.pkl`,供多個 Gunicorn worker 共用,避免部署後各 worker 重複重建 7,000+ 商品統計造成開頁變慢;所有資料異動與 AI 挑品重算都透過 `clear_dashboard_cache()` 同步清除記憶體與共享快取,手動重算 API 會立即預熱商品看板快取,避免第一位使用者承擔重建成本。
|
||||
- PChome re-score 回收線:`rescore_accepted_current` 只能表示最新版 matcher 判定「可人工採用」,不可直接寫入正式 `competitor_prices`;`fetch_competitor_coverage()` 必須輸出 `rescore_accepted_count`,Dashboard、daily/growth 與 OpenClaw 競品摘要都要把「重算可採用待審」獨立呈現,避免和一般低信心/單位價覆核混在一起。
|
||||
|
||||
| 角色 | 模型 | 主機 | 成本 | 每日限額 |
|
||||
|------|------|------|------|---------|
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
- 2026-05-21 追記:同步 browse.sh 診斷計畫寫入 `competitor_match_attempts` 後的 `services/competitor_price_feeder.py` 行數;此處只更新 inventory,不變更模組化決策。
|
||||
- 2026-05-24 追記:同步背景 PChome 近門檻身份回收與 focused identity 系列更新後的 `services/marketplace_product_matcher.py` 行數;此處只更新 inventory,不變更商品比對行為。
|
||||
- 2026-05-24 追記:同步 111 fallback circuit breaker、NemoTron 決策信封與 Telegram template governance 後的 `run_scheduler.py`、`services/ollama_service.py`、`services/nemoton_dispatcher_service.py`、`services/telegram_templates.py` 行數;此處只更新 inventory,不變更模組化決策。
|
||||
- 2026-05-24 追記:同步 PChome 覆核頁 fast-count、輕量 render 與重算可採用指標後的 `routes/dashboard_routes.py` 行數;此處只更新 inventory,不變更 dashboard 行為。
|
||||
|
||||
## 達到或超過 800 行檔案清單
|
||||
|
||||
@@ -63,7 +64,7 @@
|
||||
| 3681 | `routes/admin_observability_routes.py` | P0 觀測台巨型 Blueprint | `services/observability_query_service.py` / `services/observability_action_service.py` / route glue |
|
||||
| 1796 | `routes/ai_routes.py` | P1 AI Blueprint | route glue / AI orchestration service / prompt builders |
|
||||
| 2154 | `services/nemoton_dispatcher_service.py` | P1 NemoTron service | NIM client / tool-call parser / action dispatcher |
|
||||
| 2026 | `routes/dashboard_routes.py` | P1 Dashboard Blueprint | competitor decision overview / dashboard query service;首頁資料整併需抽 service |
|
||||
| 2535 | `routes/dashboard_routes.py` | P1 Dashboard Blueprint | competitor decision overview / dashboard query service;首頁資料整併需抽 service |
|
||||
| 1485 | `routes/vendor_routes.py` | P1 Vendor Blueprint | route glue / stockout mutation/email;V2 page query、stockout list/batches API query、vendor list/detail query 已抽到 `services/vendor_stockout_query_service.py` |
|
||||
| 1390 | `services/telegram_bot_service.py` | P1 Telegram service | command handlers / message formatters / bot client |
|
||||
| 1237 | `app.py` | P1 bootstrap | 保持只做 app setup;繼續往 app_factory / extension setup 抽;Phase 42 只做 metadata table name 對齊 |
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
## 📅 詳細更新日誌 (考古存檔)
|
||||
|
||||
### 2026-05-24:PChome 近門檻身份回收第二輪
|
||||
- **V10.450 PChome 覆核 fast-count UI 語意與重算可採用指標**: 預設全量覆核頁跳過 exact count 時,模板會以「約」標記快取總數,避免操作員把快取總數誤認為即時計算;搜尋、分類與單一狀態分流仍保留精準總數。`fetch_competitor_coverage()` 同步輸出 `rescore_accepted_count`,Dashboard、daily/growth 與 OpenClaw 摘要會把「重算可採用待審」獨立顯示,不再只混在一般覆核隊列。
|
||||
- **V10.449 PChome 覆核 exact count 條件修正**: 只有預設「全部覆核、無搜尋、無分類」頁跳過 exact count;若使用搜尋詞、分類篩選或單一 review status,仍保留精準總數,避免操作員分頁資訊失準。
|
||||
- **V10.448 PChome 覆核全量頁跳過 exact count**: `review_status=all` 改用 shared overview cache 的待處理總數作為分頁總數提示,當頁只查 50 筆;單一狀態分流仍保留 exact count,避免每次操作全量覆核頁都為總筆數重掃整個 review queue。
|
||||
- **V10.447 PChome 覆核頁查詢方向反轉**: review queue page 改由最新 `competitor_match_attempts` 的可覆核狀態先縮小候選,再 join ACTIVE 商品與最新價,並用 `NOT EXISTS` 排除已有有效 `identity_v2` 正式 PChome 價格;避免「全部覆核」每次先掃全站 ACTIVE 商品後才過濾,提高核心比價覆核頁可操作性。
|
||||
|
||||
@@ -676,6 +676,11 @@ def _merge_competitor_review_context(overview, review_context):
|
||||
overview.update({
|
||||
'review_queue_count': int(coverage.get('actionable_review_count') or len(review_queue) or 0),
|
||||
'unit_comparable_count': int(coverage.get('unit_comparable_count') or 0),
|
||||
'rescore_accepted_count': int(
|
||||
coverage.get('rescore_accepted_count')
|
||||
or review_status_counts.get('rescore_accepted')
|
||||
or 0
|
||||
),
|
||||
'review_status_counts': review_status_counts,
|
||||
'review_queue': review_queue[:3],
|
||||
})
|
||||
@@ -819,6 +824,7 @@ def _load_competitor_decision_overview(session, latest_items=None):
|
||||
'pending_priority': [],
|
||||
'review_queue_count': 0,
|
||||
'unit_comparable_count': 0,
|
||||
'rescore_accepted_count': 0,
|
||||
'review_queue': [],
|
||||
}
|
||||
|
||||
@@ -1688,6 +1694,7 @@ def _load_cached_competitor_overview_for_review(now_taipei, review_queue, review
|
||||
if not overview.get('review_queue'):
|
||||
overview['review_queue'] = list(review_queue[:3])
|
||||
overview.setdefault('unit_comparable_count', 0)
|
||||
overview.setdefault('rescore_accepted_count', 0)
|
||||
return overview
|
||||
|
||||
|
||||
@@ -1727,6 +1734,7 @@ def _render_pchome_review_dashboard(
|
||||
)
|
||||
review_queue = review_page.get('items') or []
|
||||
review_queue_total = int(review_page.get('total') or 0)
|
||||
review_total_is_estimated = False
|
||||
if review_queue_total < 0:
|
||||
review_queue_total = int(
|
||||
(overview_hint.get('review_status_counts') or {}).get('all')
|
||||
@@ -1734,6 +1742,7 @@ def _render_pchome_review_dashboard(
|
||||
or len(review_queue)
|
||||
or 0
|
||||
)
|
||||
review_total_is_estimated = True
|
||||
review_queue_map = {
|
||||
str(row.get('sku') or ''): row
|
||||
for row in review_queue
|
||||
@@ -1774,6 +1783,7 @@ def _render_pchome_review_dashboard(
|
||||
current_category=category_filter,
|
||||
current_filter=filter_type,
|
||||
current_review_status=review_status,
|
||||
review_total_is_estimated=review_total_is_estimated,
|
||||
review_status_options=review_status_options,
|
||||
search_query=search_query,
|
||||
current_sort=sort_by,
|
||||
|
||||
@@ -403,7 +403,7 @@ def _cached_payload(cache_key: str, producer, ttl_seconds: int = COMPETITOR_INTE
|
||||
|
||||
def fetch_competitor_coverage(engine) -> dict:
|
||||
return _cached_payload(
|
||||
f"coverage:v4:floor={PCHOME_MATCH_SCORE_FLOOR}:manual_reviews=1",
|
||||
f"coverage:v5:floor={PCHOME_MATCH_SCORE_FLOOR}:manual_reviews=1:rescore=1",
|
||||
lambda: _fetch_competitor_coverage_uncached(engine),
|
||||
)
|
||||
|
||||
@@ -422,6 +422,7 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
"match_rate": 0,
|
||||
"attempt_status": {},
|
||||
"unit_comparable_count": 0,
|
||||
"rescore_accepted_count": 0,
|
||||
"actionable_review_count": 0,
|
||||
"manual_review_summary": manual_review_summary,
|
||||
"manual_review_total": manual_review_summary["total"],
|
||||
@@ -509,6 +510,7 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
for row in rows
|
||||
}
|
||||
unit_count = sum(statuses.get(status, 0) for status in UNIT_COMPARABLE_STATUSES)
|
||||
rescore_accepted_count = int(statuses.get("rescore_accepted_current") or 0)
|
||||
actionable_count = sum(statuses.get(status, 0) for status in ACTIONABLE_ATTEMPT_STATUSES)
|
||||
return {
|
||||
"active_with_price": active,
|
||||
@@ -517,6 +519,7 @@ def _fetch_competitor_coverage_uncached(engine) -> dict:
|
||||
"match_rate": round(valid / max(active, 1) * 100, 1),
|
||||
"attempt_status": statuses,
|
||||
"unit_comparable_count": unit_count,
|
||||
"rescore_accepted_count": rescore_accepted_count,
|
||||
"actionable_review_count": actionable_count,
|
||||
"manual_review_summary": manual_review_summary,
|
||||
"manual_review_total": manual_review_summary["total"],
|
||||
|
||||
@@ -592,7 +592,8 @@ def _fetch_competitor_summary() -> Dict[str, Any]:
|
||||
)
|
||||
SELECT
|
||||
SUM(CASE WHEN attempt_status IN ('unit_comparable', 'refresh_unit_comparable') THEN 1 ELSE 0 END) AS unit_comparable_count,
|
||||
SUM(CASE WHEN attempt_status IN ('unit_comparable', 'refresh_unit_comparable', 'identity_veto', 'low_score', 'refresh_low_score', 'recoverable_low_score', 'true_low_confidence', 'protected_existing_match', 'expired_match', 'no_result', 'refresh_no_result') THEN 1 ELSE 0 END) AS review_queue_count
|
||||
SUM(CASE WHEN attempt_status = 'rescore_accepted_current' THEN 1 ELSE 0 END) AS rescore_accepted_count,
|
||||
SUM(CASE WHEN attempt_status IN ('rescore_accepted_current', 'unit_comparable', 'refresh_unit_comparable', 'identity_veto', 'low_score', 'refresh_low_score', 'recoverable_low_score', 'true_low_confidence', 'protected_existing_match', 'expired_match', 'no_result', 'refresh_no_result') THEN 1 ELSE 0 END) AS review_queue_count
|
||||
FROM latest_attempt
|
||||
""")).fetchone()
|
||||
return {
|
||||
@@ -601,7 +602,8 @@ def _fetch_competitor_summary() -> Dict[str, Any]:
|
||||
"undercut_count": int(row[2] or 0),
|
||||
"premium_count": int(row[3] or 0),
|
||||
"unit_comparable_count": int((attempt_row[0] if attempt_row else 0) or 0),
|
||||
"review_queue_count": int((attempt_row[1] if attempt_row else 0) or 0),
|
||||
"rescore_accepted_count": int((attempt_row[1] if attempt_row else 0) or 0),
|
||||
"review_queue_count": int((attempt_row[2] if attempt_row else 0) or 0),
|
||||
}
|
||||
return {}
|
||||
except Exception as e:
|
||||
@@ -1487,6 +1489,7 @@ def generate_weekly_strategy_report(
|
||||
被競品削價數:{competitor_summary.get('undercut_count', 0)} 個
|
||||
我方具優勢數:{competitor_summary.get('premium_count', 0)} 個
|
||||
需單位價覆核:{competitor_summary.get('unit_comparable_count', 0)} 個
|
||||
重算可採用待審:{competitor_summary.get('rescore_accepted_count', 0)} 個
|
||||
|
||||
TOP 威脅品項(近48h Hermes 偵測):
|
||||
{_format_threats(threats)}
|
||||
@@ -1767,6 +1770,7 @@ def _legacy_full_gemini_daily_report() -> dict:
|
||||
被削價風險:{competitor_summary.get('undercut_count', 0)} 個(價差超過10%)
|
||||
平均價差:{competitor_summary.get('avg_gap_pct', 0):+.1f}%
|
||||
單位價/身份覆核隊列:{competitor_summary.get('review_queue_count', 0)} 個
|
||||
重算可採用待審:{competitor_summary.get('rescore_accepted_count', 0)} 個
|
||||
|
||||
請按以下結構輸出(使用 HTML <b> 標題):
|
||||
|
||||
@@ -1944,6 +1948,7 @@ def generate_monthly_report() -> dict:
|
||||
月均價差:{competitor_summary.get('avg_gap_pct', 0):+.1f}%
|
||||
被削價風險SKU:{competitor_summary.get('undercut_count', 0)} 個
|
||||
需單位價覆核SKU:{competitor_summary.get('unit_comparable_count', 0)} 個
|
||||
重算可採用待審SKU:{competitor_summary.get('rescore_accepted_count', 0)} 個
|
||||
|
||||
【價格變動概況】
|
||||
本月調價次數:{price_trend_data.get('price_changes', 0)} 次
|
||||
|
||||
@@ -359,6 +359,10 @@
|
||||
<span>需單位價覆核</span>
|
||||
<strong class="momo-mono">{{ comp_coverage.unit_comparable_count | default(0) | number_format }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>重算可採用待審</span>
|
||||
<strong class="momo-mono">{{ comp_coverage.rescore_accepted_count | default(0) | number_format }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>人工採用</span>
|
||||
<strong class="momo-mono">{{ comp_coverage.manual_accept_count | default(0) | number_format }}</strong>
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
<div class="dashboard-kpi-label momo-mono">比價覆核</div>
|
||||
<div class="dashboard-kpi-value momo-mono is-warning">{{ overview.review_queue_count | default(0) | number_format }}</div>
|
||||
<div class="dashboard-kpi-sub momo-mono">
|
||||
<a class="dashboard-kpi-sub-link" href="{{ url_for('dashboard.index', filter='pchome_review', category=current_category, q=search_query, sort_by='pchome_review', order='desc') }}">需單位價覆核 {{ overview.unit_comparable_count | default(0) | number_format }}</a>
|
||||
<a class="dashboard-kpi-sub-link" href="{{ url_for('dashboard.index', filter='pchome_review', category=current_category, q=search_query, review_status='rescore_accepted', sort_by='pchome_review', order='desc') }}">重算可採用 {{ overview.rescore_accepted_count | default(0) | number_format }}</a>
|
||||
· <a class="dashboard-kpi-sub-link" href="{{ url_for('dashboard.index', filter='pchome_review', category=current_category, q=search_query, review_status='unit_comparable', sort_by='pchome_review', order='desc') }}">需單位價 {{ overview.unit_comparable_count | default(0) | number_format }}</a>
|
||||
· 待補抓 {{ overview.pending_match_count | default(0) | number_format }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,7 +62,7 @@
|
||||
<div class="dashboard-backfill-label momo-mono">PCHOME MATCH BACKFILL</div>
|
||||
<div class="dashboard-backfill-title">待比對補抓產線</div>
|
||||
<div class="dashboard-backfill-meta momo-mono">
|
||||
待補抓 {{ overview.pending_match_count | default(0) | number_format }} · 覆核 {{ overview.review_queue_count | default(0) | number_format }} · 單位價 {{ overview.unit_comparable_count | default(0) | number_format }}
|
||||
待補抓 {{ overview.pending_match_count | default(0) | number_format }} · 覆核 {{ overview.review_queue_count | default(0) | number_format }} · 重算可採用 {{ overview.rescore_accepted_count | default(0) | number_format }} · 單位價 {{ overview.unit_comparable_count | default(0) | number_format }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-backfill-progress" aria-hidden="true">
|
||||
@@ -300,7 +301,7 @@
|
||||
{% if current_filter == 'ai_picks' %}
|
||||
{{ total_items | number_format }} / {{ ai_pick_list_limit }} 品
|
||||
{% elif current_filter == 'pchome_review' %}
|
||||
{{ total_items | number_format }} / {{ overview.review_queue_count | default(0) | number_format }} 待處理
|
||||
{% if review_total_is_estimated %}約 {% endif %}{{ total_items | number_format }} / {{ overview.review_queue_count | default(0) | number_format }} 待處理
|
||||
{% else %}
|
||||
{{ total_items | number_format }} 筆
|
||||
{% endif %}
|
||||
|
||||
@@ -149,6 +149,8 @@
|
||||
<strong class="momo-mono">{{ coverage.pending | default(0) | number_format }}</strong>
|
||||
<span>需單位價覆核</span>
|
||||
<strong class="momo-mono">{{ coverage.unit_comparable_count | default(0) | number_format }}</strong>
|
||||
<span>重算可採用待審</span>
|
||||
<strong class="momo-mono">{{ coverage.rescore_accepted_count | default(0) | number_format }}</strong>
|
||||
<span>人工採用</span>
|
||||
<strong class="momo-mono">{{ coverage.manual_accept_count | default(0) | number_format }}</strong>
|
||||
<span>人工否決</span>
|
||||
|
||||
@@ -75,7 +75,8 @@ def test_competitor_coverage_counts_only_active_product_intersection():
|
||||
"def _fetch_manual_review_summary", 1
|
||||
)[0]
|
||||
|
||||
assert "coverage:v4" in source
|
||||
assert "coverage:v5" in source
|
||||
assert "rescore_accepted_count" in coverage_source
|
||||
assert "(SELECT COUNT(*) FROM valid_competitor) AS valid_matches" not in coverage_source
|
||||
assert "FROM latest_momo lm\n JOIN valid_competitor vc ON vc.sku = lm.sku" in coverage_source
|
||||
assert "FROM products p\n JOIN LATERAL" in coverage_source
|
||||
@@ -101,6 +102,7 @@ def test_competitor_review_queue_is_canonical_unit_price_handoff():
|
||||
assert "def fetch_competitor_review_queue" in source
|
||||
assert "\"review_queue\": fetch_competitor_review_queue" in source
|
||||
assert "\"unit_comparable_count\"" in source
|
||||
assert "\"rescore_accepted_count\"" in source
|
||||
assert "manual_review_summary" in source
|
||||
assert "manual_accept_count" in source
|
||||
assert "manual_reject_count" in source
|
||||
@@ -119,9 +121,11 @@ def test_competitor_review_queue_is_canonical_unit_price_handoff():
|
||||
assert "人工單位價" in daily_template
|
||||
assert "competitor_intel.review_queue" in daily_template
|
||||
assert "coverage.unit_comparable_count" in growth_template
|
||||
assert "coverage.rescore_accepted_count" in growth_template
|
||||
assert "coverage.manual_accept_count" in growth_template
|
||||
assert "coverage.manual_reject_count" in growth_template
|
||||
assert "coverage.manual_unit_price_count" in growth_template
|
||||
assert "comp_coverage.rescore_accepted_count" in daily_template
|
||||
|
||||
|
||||
def test_competitor_review_reasons_prefer_json_payload_labels():
|
||||
|
||||
@@ -142,6 +142,8 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
|
||||
assert "or bool(search_query)" in route_source
|
||||
assert "or bool(_normalize_dashboard_category_filter(category_filter))" in route_source
|
||||
assert "count_total=count_total" in route_source
|
||||
assert "review_total_is_estimated = True" in route_source
|
||||
assert "review_total_is_estimated=review_total_is_estimated" in route_source
|
||||
assert "只替 PChome 覆核當頁建立商品列" in route_source
|
||||
assert "_load_competitor_decision_overview(session, unique_items)" in route_source
|
||||
assert "item_map = {}" in route_source
|
||||
@@ -150,6 +152,7 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
|
||||
assert "pending_match_count" in route_source
|
||||
assert "review_queue_count" in route_source
|
||||
assert "unit_comparable_count" in route_source
|
||||
assert "rescore_accepted_count" in route_source
|
||||
assert "filter_type == 'pchome_review'" in route_source
|
||||
assert "total_items = review_queue_total" in route_source
|
||||
assert "REVIEW_STATUS_OPTIONS" in route_source
|
||||
@@ -169,7 +172,9 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
|
||||
assert "overview.top_momo_threats" in dashboard
|
||||
assert "overview.pending_priority" in dashboard
|
||||
assert "overview.review_queue" in dashboard
|
||||
assert "需單位價覆核" in dashboard
|
||||
assert "需單位價 {{ overview.unit_comparable_count" in dashboard
|
||||
assert "重算可採用 {{ overview.rescore_accepted_count" in dashboard
|
||||
assert "{% if review_total_is_estimated %}約 {% endif %}" in dashboard
|
||||
assert "filter='ai_picks'" in dashboard
|
||||
assert "filter='pchome_review'" in dashboard
|
||||
assert "review_status=option.key" in dashboard
|
||||
|
||||
@@ -19,6 +19,7 @@ Operation Ollama-First v5.0 / Phase 3 / A8 — 日報模板路由測試
|
||||
import os
|
||||
import logging
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
@@ -150,6 +151,13 @@ class TestKPIComputation:
|
||||
assert threats[0]["gap_pct"] == 0
|
||||
assert "metadata_json decode failed" in caplog.text
|
||||
|
||||
def test_competitor_summary_tracks_rescore_review_bucket(self):
|
||||
source = (Path(__file__).resolve().parents[1] / "services/openclaw_strategist_service.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "rescore_accepted_count" in source
|
||||
assert "attempt_status = 'rescore_accepted_current'" in source
|
||||
assert "重算可採用待審" in source
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# T4+T5 — Template 渲染與缺欄位優雅降級
|
||||
|
||||
Reference in New Issue
Block a user