"""API-key request → Feishu form: the validity window must reach the card. The ``AI API KEY Apply`` template carries a **required** ``dateInterval`` widget (``widget17763462677850001``, named the literal string "DateInterval" in every locale). Between 2026-05 and 2026-08-14 ``ApiKeyService._SKIP_WIDGET_TYPES`` dropped it from every submission — Feishu accepts a form that omits a required widget, so submits worked and nobody noticed that the approver's card showed the source, project, reason, budget and expected usage with **no validity window at all**. The dates existed only on the Helix row (``valid_from`` / ``valid_until``). Same bug as `computer_borrow`'s borrow window, same fix: the shape in ``app.integrations.feishu_widget_values`` — read off a real Feishu-native instance, not inferred. """ from __future__ import annotations import json import uuid from datetime import date, timedelta from decimal import Decimal from app.domains.apikey.models import ApiKeyProvider from app.domains.apikey.schemas import ApiKeyRequestSubmit from app.domains.apikey.service import ApiKeyService # Trimmed copy of the live prod template (verified 2026-08-14 against # GET /approval/v4/approvals/992B522E-A602-4B7C-8E83-C1A809EDF457). WIDGETS = [ {"id": "w_source", "name": "API Key 来源", "type": "input", "required": True}, {"id": "w_project", "name": "项目名称", "type": "input", "required": True}, {"id": "w_reason", "name": "申请理由", "type": "textarea", "required": True}, {"id": "w_budget", "name": "月度预算", "type": "amount", "required": True}, {"id": "w_interval", "name": "DateInterval", "type": "dateInterval", "required": True}, {"id": "w_usage", "name": "预计用量", "type": "input", "required": True}, ] # Relative to today, not literal dates. ``ApiKeyRequestSubmit`` rejects a # ``valid_from`` more than one day in the past, so the original fixture # (2026-09-01 / 2026-11-30) was a time bomb: it passed while it was written # and started failing on 2026-09-03 with a validation error that reads like # a product bug. Every expectation below is derived from these two, so the # window can move without touching an assertion. VALID_FROM = date.today() + timedelta(days=1) VALID_UNTIL = VALID_FROM + timedelta(days=90) #: Feishu's own convention: inclusive day count. INTERVAL_DAYS = (VALID_UNTIL - VALID_FROM).days + 1 def _feishu_day(value: date) -> str: """The wire shape Feishu's ``dateInterval`` widget uses (Asia/Shanghai).""" return f"{value.isoformat()}T00:00:00+08:00" def _body(**overrides) -> ApiKeyRequestSubmit: payload = { "provider_id": uuid.uuid4(), "project_name": "helix-eval", "reason": "batch evaluation runs", "monthly_budget_usd": Decimal("200"), "valid_from": VALID_FROM, "valid_until": VALID_UNTIL, "expected_usage": "~3M tokens/month", "answers": {}, } payload.update(overrides) return ApiKeyRequestSubmit(**payload) def _provider() -> ApiKeyProvider: return ApiKeyProvider( id=uuid.uuid4(), name="AWS Bedrock", description="", is_active=True, sort_order=0, ) def test_validity_window_reaches_the_feishu_form(): """Whole form item asserted, not just ``value`` — a partial assertion is how the 2026-08-04 amount-widget regression stayed green while production rejected every submission. """ body = _body() answers: dict = {} ApiKeyService._auto_fill_answers(answers, WIDGETS, _provider(), body) serialized = json.loads(ApiKeyService._serialize_feishu_form(WIDGETS, answers)) interval_item = next(i for i in serialized if i["id"] == "w_interval") assert interval_item == { "id": "w_interval", "type": "dateInterval", "value": { "start": _feishu_day(VALID_FROM), "end": _feishu_day(VALID_UNTIL), "interval": INTERVAL_DAYS, "timezoneOffset": -480, }, } # The rest of the form is untouched by this change. assert {i["id"] for i in serialized} == { "w_source", "w_project", "w_reason", "w_budget", "w_interval", "w_usage", } def test_missing_window_omits_the_widget_instead_of_400ing(): """The widget is ``required`` in the template, but an unusable range must not raise APPROVAL_FIELD_REQUIRED — that would newly block submits that worked for the last three months. """ answers: dict = {} ApiKeyService._auto_fill_answers( answers, WIDGETS, _provider(), # ``valid_from``/``valid_until`` are required by the schema, so # the only way to reach the empty branch is a caller that # bypasses it (MCP / replay) — simulate by blanking after # validation. _body().model_copy(update={"valid_until": None}), ) serialized = json.loads(ApiKeyService._serialize_feishu_form(WIDGETS, answers)) assert "w_interval" not in {i["id"] for i in serialized} def test_client_supplied_window_is_overwritten_by_the_granted_one(): """Trust boundary, mirroring the asset flow's Payment Information overwrite: ``body.answers`` is client-controlled, so a hand-crafted interval must NOT reach the approval card. What the approver sees has to be the window that will actually be granted — otherwise a curl / MCP caller shows "one week" on the card while the assignment row (and the expiry cron) carries a year. """ answers = {"w_interval": {"start": "2020-10-05", "end": "2020-10-06"}} ApiKeyService._auto_fill_answers(answers, WIDGETS, _provider(), _body()) serialized = json.loads(ApiKeyService._serialize_feishu_form(WIDGETS, answers)) interval_item = next(i for i in serialized if i["id"] == "w_interval") assert interval_item["value"] == { # body.valid_from / body.valid_until, NOT the client's 2020 window. "start": _feishu_day(VALID_FROM), "end": _feishu_day(VALID_UNTIL), "interval": INTERVAL_DAYS, "timezoneOffset": -480, } # The submit-path degradation (Feishu rejects the range → resubmit without # the widget) is shared by every non-asset flow and is covered end-to-end in # tests/test_approval/test_nonasset_intent_date_interval_degrade.py.