From 4199feb6813db69ad2a925fb754acaf57bc3cd09 Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Wed, 17 Jun 2026 14:39:26 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E5=90=8C=E6=AD=A5=E6=8A=A5=E9=94=80?= =?UTF-8?q?=E5=AE=A1=E6=89=B9=E6=B5=81=E4=B8=8E=E9=A2=84=E7=AE=97=E5=88=86?= =?UTF-8?q?=E6=9E=90=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增预算审批合并、风险标记去重与占位条目校验用例 - 补充预算分析对当前审核人与财务的可见性断言 - 调整单据删除权限测试以匹配 admin 限制 --- server/tests/test_budget_endpoints.py | 81 ++++- .../test_expense_claim_approval_routing.py | 69 +++- server/tests/test_expense_claim_risk_flags.py | 47 +++ server/tests/test_expense_claim_service.py | 300 +++++++++++++++- web/tests/accessControl.test.mjs | 2 +- web/tests/budget-ontology.test.mjs | 12 + .../documents-center-status-filter.test.mjs | 32 +- ...el-request-detail-leader-approval.test.mjs | 39 ++- ...travel-request-detail-risk-advice.test.mjs | 329 +++++++++++++++++- ...vel-request-detail-submit-confirm.test.mjs | 38 +- 10 files changed, 907 insertions(+), 42 deletions(-) create mode 100644 server/tests/test_expense_claim_risk_flags.py diff --git a/server/tests/test_budget_endpoints.py b/server/tests/test_budget_endpoints.py index dd6a6fe..779d918 100644 --- a/server/tests/test_budget_endpoints.py +++ b/server/tests/test_budget_endpoints.py @@ -313,15 +313,34 @@ def test_budget_analysis_endpoint_is_limited_to_budget_roles() -> None: with session_factory() as db: seed_budget_allocations(db) budget_role, market_department = seed_market_budget_monitor(db) + manager_role = Role(role_code="manager", name="直属领导") + finance_role = Role(role_code="finance", name="财务") + manager = Employee( + employee_no="E-BUDGET-MANAGER", + name="预算领导", + email="budget-manager-review@example.com", + grade="P7", + organization_unit=market_department, + roles=[manager_role], + ) + finance_user = Employee( + employee_no="E-BUDGET-FINANCE", + name="预算财务", + email="budget-finance-review@example.com", + grade="P6", + organization_unit=market_department, + roles=[finance_role], + ) p6_budget_monitor = Employee( employee_no="E-BUDGET-MARKET-P6", name="低级预算", email="p6-budget-monitor@example.com", grade="P6", organization_unit=market_department, + manager=manager, roles=[budget_role], ) - db.add(p6_budget_monitor) + db.add_all([manager, finance_user, p6_budget_monitor]) db.flush() claim = ExpenseClaim( claim_no="APP-BUDGET-ANALYSIS-001", @@ -342,9 +361,49 @@ def test_budget_analysis_endpoint_is_limited_to_budget_roles() -> None: approval_stage="预算管理者审批", risk_flags_json=[], ) - db.add(claim) + leader_claim = ExpenseClaim( + claim_no="RE-BUDGET-ANALYSIS-LEADER", + employee_id=p6_budget_monitor.id, + employee_name="低级预算", + department_id="dept-market", + department_name="市场部", + project_code=None, + expense_type="travel", + reason="客户现场交付报销", + location="上海", + amount=Decimal("6000.00"), + currency="CNY", + invoice_count=1, + occurred_at=datetime(2026, 5, 12, 9, 0, tzinfo=UTC), + submitted_at=datetime(2026, 5, 12, 10, 0, tzinfo=UTC), + status="submitted", + approval_stage="直属领导审批", + risk_flags_json=[], + ) + finance_claim = ExpenseClaim( + claim_no="RE-BUDGET-ANALYSIS-FINANCE", + employee_id=p6_budget_monitor.id, + employee_name="低级预算", + department_id="dept-market", + department_name="市场部", + project_code=None, + expense_type="travel", + reason="客户现场交付报销", + location="上海", + amount=Decimal("8000.00"), + currency="CNY", + invoice_count=1, + occurred_at=datetime(2026, 5, 12, 9, 0, tzinfo=UTC), + submitted_at=datetime(2026, 5, 12, 10, 0, tzinfo=UTC), + status="submitted", + approval_stage="财务审批", + risk_flags_json=[], + ) + db.add_all([claim, leader_claim, finance_claim]) db.commit() claim_id = claim.id + leader_claim_id = leader_claim.id + finance_claim_id = finance_claim.id ordinary_response = client.get( f"/api/v1/reimbursements/claims/{claim_id}/budget-analysis", @@ -367,8 +426,26 @@ def test_budget_analysis_endpoint_is_limited_to_budget_roles() -> None: "x-auth-role-codes": "budget_monitor", }, ) + leader_response = client.get( + f"/api/v1/reimbursements/claims/{leader_claim_id}/budget-analysis", + headers={ + "x-auth-username": "budget-manager-review@example.com", + "x-auth-role-codes": "manager", + }, + ) + finance_response = client.get( + f"/api/v1/reimbursements/claims/{finance_claim_id}/budget-analysis", + headers={ + "x-auth-username": "budget-finance-review@example.com", + "x-auth-role-codes": "finance", + }, + ) assert ordinary_response.status_code == 403 assert p6_monitor_response.status_code == 403 assert monitor_response.status_code == 200 + assert leader_response.status_code == 200 + assert finance_response.status_code == 200 assert Decimal(monitor_response.json()["metrics"]["claim_amount_ratio"]) == Decimal("24.00") + assert Decimal(leader_response.json()["metrics"]["claim_amount_ratio"]) == Decimal("12.00") + assert Decimal(finance_response.json()["metrics"]["claim_amount_ratio"]) == Decimal("16.00") diff --git a/server/tests/test_expense_claim_approval_routing.py b/server/tests/test_expense_claim_approval_routing.py index 7cbf8ba..0928e9e 100644 --- a/server/tests/test_expense_claim_approval_routing.py +++ b/server/tests/test_expense_claim_approval_routing.py @@ -4,6 +4,7 @@ import uuid from datetime import UTC, datetime from decimal import Decimal +import pytest from sqlalchemy import create_engine from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.pool import StaticPool @@ -236,7 +237,7 @@ def test_budget_warning_application_still_skips_budget_manager_when_not_over_bud def test_application_routes_to_budget_manager_when_usage_reaches_90_percent() -> None: with build_session() as db: - department, manager, _budget_manager, employee = _seed_people(db, suffix="OVER-90-APP") + department, manager, budget_manager, employee = _seed_people(db, suffix="OVER-90-APP") _seed_budget_allocation( db, department_id=department.id, @@ -288,6 +289,18 @@ def test_application_routes_to_budget_manager_when_usage_reaches_90_percent() -> for flag in routed.risk_flags_json ) + with pytest.raises(ValueError, match="预算已超过警戒值"): + ExpenseClaimService(db).approve_claim( + claim.id, + CurrentUserContext( + username=budget_manager.email, + name=budget_manager.name, + role_codes=["budget_monitor"], + is_admin=False, + ), + opinion=" ", + ) + def test_application_stage_risk_under_90_percent_does_not_route_to_budget_manager() -> None: with build_session() as db: @@ -496,3 +509,57 @@ def test_risky_reimbursement_routes_to_budget_then_finance() -> None: and flag.get("next_approval_stage") == FINANCE_APPROVAL_STAGE for flag in budget_approved.risk_flags_json ) + + +def test_budget_manager_blank_opinion_defaults_to_agree_when_budget_under_warning() -> None: + with build_session() as db: + department, _manager, budget_manager, employee = _seed_people(db, suffix="BUDGET-NORMAL") + _seed_budget_allocation( + db, + department_id=department.id, + department_name=department.name, + amount=Decimal("10000.00"), + ) + claim = ExpenseClaim( + claim_no="RE-20260530-BUDGET-NORMAL", + employee_id=employee.id, + employee_name=employee.name, + department_id=department.id, + department_name=department.name, + project_code=None, + expense_type="travel", + reason="客户现场沟通", + location="上海", + amount=Decimal("500.00"), + currency="CNY", + invoice_count=1, + occurred_at=datetime(2026, 5, 30, 9, 0, tzinfo=UTC), + submitted_at=datetime(2026, 5, 30, 10, 0, tzinfo=UTC), + status="submitted", + approval_stage=BUDGET_MANAGER_APPROVAL_STAGE, + risk_flags_json=[], + ) + db.add(claim) + db.commit() + + approved = ExpenseClaimService(db).approve_claim( + claim.id, + CurrentUserContext( + username=budget_manager.email, + name=budget_manager.name, + role_codes=["budget_monitor"], + is_admin=False, + ), + opinion=" ", + ) + + assert approved is not None + assert approved.status == "submitted" + assert approved.approval_stage == FINANCE_APPROVAL_STAGE + assert any( + isinstance(flag, dict) + and flag.get("source") == "budget_approval" + and flag.get("event_type") == "expense_claim_budget_approval" + and flag.get("opinion") == "同意" + for flag in approved.risk_flags_json + ) diff --git a/server/tests/test_expense_claim_risk_flags.py b/server/tests/test_expense_claim_risk_flags.py new file mode 100644 index 0000000..e92d583 --- /dev/null +++ b/server/tests/test_expense_claim_risk_flags.py @@ -0,0 +1,47 @@ +from app.services.expense_claim_risk_flags import dedupe_claim_risk_flags + + +def test_dedupe_claim_risk_flags_keeps_highest_severity_for_same_route_issue() -> None: + flags = [ + { + "severity": "high", + "label": "多城市行程待说明", + "message": "检测到本次差旅涉及深圳多个目的地,但当前报销事由未说明中转原因。", + "item_ids": ["route-1", "route-2"], + "business_stage": "reimbursement", + }, + { + "severity": "medium", + "label": "多城市行程缺少说明中风险", + "message": "本次报销识别到多城市行程,但事由中未说明中转、多地拜访或改签原因。", + "item_ids": ["route-2"], + "business_stage": "reimbursement", + }, + ] + + deduped = dedupe_claim_risk_flags(flags) + + assert [flag["label"] for flag in deduped] == ["多城市行程待说明"] + + +def test_dedupe_claim_risk_flags_keeps_distinct_item_risks() -> None: + flags = [ + { + "severity": "high", + "label": "住宿金额超出报销标准", + "message": "第一张住宿票超出住宿标准。", + "item_id": "hotel-1", + "business_stage": "reimbursement", + }, + { + "severity": "medium", + "label": "住宿金额超出报销标准", + "message": "第二张住宿票超出住宿标准。", + "item_id": "hotel-2", + "business_stage": "reimbursement", + }, + ] + + deduped = dedupe_claim_risk_flags(flags) + + assert [flag["item_id"] for flag in deduped] == ["hotel-1", "hotel-2"] diff --git a/server/tests/test_expense_claim_service.py b/server/tests/test_expense_claim_service.py index f0ac1ca..89af7cf 100644 --- a/server/tests/test_expense_claim_service.py +++ b/server/tests/test_expense_claim_service.py @@ -37,6 +37,7 @@ from app.services.expense_claim_workflow_constants import ( APPLICATION_LINK_STATUS_STAGE, BUDGET_MANAGER_APPROVAL_STAGE, DIRECT_MANAGER_APPROVAL_STAGE, + FINANCE_APPROVAL_STAGE, ) from app.services.ontology import SemanticOntologyService from app.services.ocr import OcrService @@ -267,6 +268,81 @@ def test_validate_claim_for_submission_still_requires_hotel_receipt() -> None: assert any("缺少票据标识" in item for item in issues) +def test_validate_claim_for_submission_does_not_block_uploaded_receipt_ocr_gaps() -> None: + service = ExpenseClaimService.__new__(ExpenseClaimService) + claim = build_claim(expense_type="hotel", location="北京") + claim.invoice_count = 1 + claim.amount = Decimal("1086.00") + claim.items[0].item_type = "hotel_ticket" + claim.items[0].item_date = None + claim.items[0].item_reason = "" + claim.items[0].item_amount = Decimal("0.00") + claim.items[0].invoice_id = "claim-1/item-1/hotel-invoice.png" + + issues = service._validate_claim_for_submission(claim) + + assert not any("缺少日期" in item for item in issues) + assert not any("缺少说明" in item for item in issues) + assert not any("缺少金额" in item for item in issues) + assert not any("缺少票据标识" in item for item in issues) + + +def test_validate_claim_for_submission_ignores_trailing_placeholder_item() -> None: + service = ExpenseClaimService.__new__(ExpenseClaimService) + claim = build_claim(expense_type="travel", location="上海") + claim.amount = Decimal("1086.00") + claim.invoice_count = 1 + claim.items[0].item_type = "hotel_ticket" + claim.items[0].item_reason = "上海喜来登酒店" + claim.items[0].item_location = "上海" + claim.items[0].item_amount = Decimal("1086.00") + claim.items[0].invoice_id = "claim-1/item-1/hotel-invoice.png" + claim.items.append( + ExpenseClaimItem( + id="item-2", + claim_id="claim-1", + item_date=date(2026, 2, 23), + item_type="hotel_ticket", + item_reason="", + item_location="", + item_amount=Decimal("0.00"), + invoice_id="", + ) + ) + + issues = service._validate_claim_for_submission(claim) + + assert not any(item.startswith("费用明细第 2 条") for item in issues) + + +def test_validate_claim_for_submission_skips_generated_allowance_item() -> None: + service = ExpenseClaimService.__new__(ExpenseClaimService) + claim = build_claim(expense_type="travel", location="上海") + claim.amount = Decimal("1486.00") + claim.invoice_count = 1 + claim.items[0].item_type = "hotel_ticket" + claim.items[0].item_reason = "上海喜来登酒店" + claim.items[0].item_location = "上海" + claim.items[0].item_amount = Decimal("1086.00") + claim.items[0].invoice_id = "claim-1/item-1/hotel-invoice.png" + claim.items.append( + ExpenseClaimItem( + id="allowance-1", + claim_id="claim-1", + item_date=date(2026, 2, 23), + item_type="travel_allowance", + item_reason="", + item_location="", + item_amount=Decimal("0.00"), + invoice_id="", + ) + ) + + issues = service._validate_claim_for_submission(claim) + + assert not any(item.startswith("费用明细第 2 条") for item in issues) + + def test_save_or_submit_preview_does_not_create_claim_without_explicit_action() -> None: user_id = "preview-only@example.com" message = "业务发生时间:2026-03-04,打车去客户现场,交通费32元,请帮我看看怎么报" @@ -2979,10 +3055,10 @@ def test_delete_claim_item_removes_row_and_attachment_files(monkeypatch, tmp_pat def test_delete_claim_removes_all_claim_attachment_files(monkeypatch, tmp_path) -> None: current_user = CurrentUserContext( - username="emp-1", + username="admin", name="张三", - role_codes=[], - is_admin=False, + role_codes=["admin"], + is_admin=True, ) monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path) @@ -3018,6 +3094,27 @@ def test_delete_claim_removes_all_claim_attachment_files(monkeypatch, tmp_path) assert AgentConversationService(db).get_conversation(conversation.conversation_id) is None +def test_non_admin_cannot_delete_own_draft_claim(monkeypatch, tmp_path) -> None: + current_user = CurrentUserContext( + username="emp-1", + name="张三", + role_codes=[], + is_admin=False, + ) + monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path) + + with build_session() as db: + claim = build_claim(expense_type="office", location="深圳南山") + db.add(claim) + db.commit() + claim_id = claim.id + + with pytest.raises(ValueError, match="只有 admin 管理员可以删除单据"): + ExpenseClaimService(db).delete_claim(claim_id, current_user) + + assert db.get(ExpenseClaim, claim_id) is not None + + def test_attachment_preview_resolves_legacy_filename_in_claim_item_directory(monkeypatch, tmp_path) -> None: current_user = CurrentUserContext( username="emp-1", @@ -3624,6 +3721,29 @@ def test_submit_claim_routes_travel_route_mismatch_to_approval_with_review_flag( current_user=current_user, ) + def fake_platform_route_review(self, claim, *, rule_codes=None, business_stage=None): + return { + "flags": [ + { + "source": "submission_review", + "hit_source": "rule_center", + "rule_code": "risk.travel.medium.multi_city_no_reason", + "severity": "medium", + "label": "多城市行程缺少说明中风险", + "message": "本次报销识别到多城市行程(上海、武汉、成都),但事由中未说明中转、多地拜访或改签原因。", + "item_ids": ["travel-item-2"], + "business_stage": "reimbursement", + } + ], + "blocking_reasons": [], + } + + monkeypatch.setattr( + ExpenseClaimService, + "evaluate_platform_risk_rules", + fake_platform_route_review, + ) + submitted = service.submit_claim(claim.id, current_user) assert submitted is not None @@ -3648,6 +3768,11 @@ def test_submit_claim_routes_travel_route_mismatch_to_approval_with_review_flag( assert route_flags assert all(flag.get("item_ids") for flag in route_flags) assert any("travel-item-2" in flag.get("item_ids", []) for flag in route_flags) + assert not any( + isinstance(flag, dict) + and str(flag.get("label") or "").strip() == "多城市行程缺少说明中风险" + for flag in list(submitted.risk_flags_json or []) + ) def test_submit_claim_allows_round_trip_ticket_origin_inferred_from_route( @@ -5049,6 +5174,175 @@ def test_manager_cannot_operate_own_claim_submitted_to_direct_manager() -> None: assert claim.risk_flags_json == [] +def test_direct_manager_budget_monitor_routes_reimbursement_directly_to_finance() -> None: + current_user = CurrentUserContext( + username="manager-budget-monitor-reimbursement@example.com", + name="李预算经理", + role_codes=["manager", "budget_monitor", "executive"], + is_admin=False, + ) + + with build_session() as db: + budget_role = _seed_budget_monitor_role(db) + department = OrganizationUnit( + unit_code="DELIVERY-REIMBURSEMENT-MERGED", + name="交付部", + unit_type="department", + ) + manager = Employee( + employee_no="E-RB-MERGED-MGR", + name="李预算经理", + email="manager-budget-monitor-reimbursement@example.com", + grade="P8", + organization_unit=department, + roles=[budget_role], + ) + employee = Employee( + employee_no="E-RB-MERGED-EMP", + name="张三", + email="zhangsan-budget-monitor-reimbursement@example.com", + manager=manager, + organization_unit=department, + ) + db.add_all([department, manager, employee]) + db.flush() + claim = ExpenseClaim( + claim_no="RE-20260525-MERGED", + employee_id=employee.id, + employee_name="张三", + department_id=department.id, + department_name="交付部", + project_code="PRJ-A", + expense_type="travel", + reason="上海出差报销", + location="上海", + amount=Decimal("3020.00"), + currency="CNY", + invoice_count=3, + occurred_at=datetime(2026, 5, 25, 9, 0, tzinfo=UTC), + submitted_at=datetime(2026, 5, 25, 10, 0, tzinfo=UTC), + status="submitted", + approval_stage="直属领导审批", + risk_flags_json=[ + { + "source": "submission_review", + "severity": "high", + "label": "报销风险复核", + "message": "多城市行程和住宿超标需要预算管理者二次确认。", + } + ], + ) + db.add(claim) + db.commit() + + approved = ExpenseClaimService(db).approve_claim( + claim.id, + current_user, + opinion="业务必要且预算可承接,同意报销。", + ) + + assert approved is not None + assert approved.status == "submitted" + assert approved.approval_stage == "财务审批" + assert not any( + isinstance(flag, dict) + and flag.get("next_approval_stage") == "预算管理者审批" + for flag in approved.risk_flags_json + ) + assert any( + isinstance(flag, dict) + and flag.get("source") == "manual_approval" + and flag.get("event_type") == "expense_claim_approval" + and flag.get("label") == "领导及预算审核通过" + and flag.get("opinion") == "业务必要且预算可承接,同意报销。" + and flag.get("previous_approval_stage") == "直属领导审批" + and flag.get("next_status") == "submitted" + and flag.get("next_approval_stage") == "财务审批" + and flag.get("budget_approval_merged") is True + and flag.get("budget_approval_merged_reason") == "direct_manager_is_department_budget_approver" + for flag in approved.risk_flags_json + ) + + +def test_duplicate_budget_stage_from_legacy_reimbursement_is_repaired_on_read() -> None: + admin_user = CurrentUserContext( + username="admin", + name="admin", + role_codes=["admin"], + is_admin=True, + ) + + with build_session() as db: + budget_role = _seed_budget_monitor_role(db) + department = OrganizationUnit( + unit_code="DELIVERY-LEGACY-REPAIR", + name="交付部", + unit_type="department", + ) + manager = Employee( + employee_no="E-LEGACY-MGR", + name="李预算经理", + email="manager-legacy-repair@example.com", + grade="P8", + organization_unit=department, + roles=[budget_role], + ) + employee = Employee( + employee_no="E-LEGACY-EMP", + name="张三", + email="zhangsan-legacy-repair@example.com", + manager=manager, + organization_unit=department, + ) + db.add_all([department, manager, employee]) + db.flush() + claim = ExpenseClaim( + claim_no="RE-20260525-LEGACY", + employee_id=employee.id, + employee_name=employee.name, + department_id=department.id, + department_name=department.name, + project_code="PRJ-A", + expense_type="travel", + reason="上海出差报销", + location="上海", + amount=Decimal("3020.00"), + currency="CNY", + invoice_count=3, + occurred_at=datetime(2026, 5, 25, 9, 0, tzinfo=UTC), + submitted_at=datetime(2026, 5, 25, 10, 0, tzinfo=UTC), + status="submitted", + approval_stage=BUDGET_MANAGER_APPROVAL_STAGE, + risk_flags_json=[ + { + "source": "manual_approval", + "event_type": "expense_claim_approval", + "approval_event_id": "legacy-approval-event", + "operator": manager.name, + "operator_username": manager.email, + "previous_approval_stage": DIRECT_MANAGER_APPROVAL_STAGE, + "next_approval_stage": BUDGET_MANAGER_APPROVAL_STAGE, + "next_approver_name": manager.name, + "next_approver_employee_id": manager.id, + } + ], + ) + db.add(claim) + db.commit() + + repaired = ExpenseClaimService(db).get_claim(claim.id, admin_user) + + assert repaired is not None + assert repaired.approval_stage == FINANCE_APPROVAL_STAGE + assert any( + isinstance(flag, dict) + and flag.get("source") == "approval_flow_repair" + and flag.get("event_type") == "duplicate_budget_approval_stage_repaired" + and flag.get("next_approval_stage") == FINANCE_APPROVAL_STAGE + for flag in repaired.risk_flags_json + ) + + def test_application_submit_skips_ai_review_and_receipt_requirements(monkeypatch: pytest.MonkeyPatch) -> None: current_user = CurrentUserContext( username="application-owner@example.com", diff --git a/web/tests/accessControl.test.mjs b/web/tests/accessControl.test.mjs index 0960171..cd3fc7a 100644 --- a/web/tests/accessControl.test.mjs +++ b/web/tests/accessControl.test.mjs @@ -50,7 +50,7 @@ test('direct approvers can return claims without receiving delete permissions', assert.equal(canManageExpenseClaims(approverUser), false) }) -test('finance can return and final approve, but only executives can manage delete permissions', () => { +test('finance can return and final approve, executives can manage claim visibility only', () => { assert.equal(canReturnExpenseClaims({ roleCodes: ['finance'] }), true) assert.equal(canApproveLeaderExpenseClaims({ roleCodes: ['finance'] }), false) assert.equal(canManageExpenseClaims({ roleCodes: ['finance'] }), false) diff --git a/web/tests/budget-ontology.test.mjs b/web/tests/budget-ontology.test.mjs index 18d763b..7cc139e 100644 --- a/web/tests/budget-ontology.test.mjs +++ b/web/tests/budget-ontology.test.mjs @@ -68,6 +68,18 @@ test('budget ontology context maps dialog fields to ontology payload', () => { assert.equal(context.budget_details[0].warning_threshold, '80%') }) +test('budget ontology includes approval review execution metrics', () => { + const fieldKeys = BUDGET_ONTOLOGY_FIELDS.map((field) => field.key) + + assert.ok(fieldKeys.includes('claim_amount')) + assert.ok(fieldKeys.includes('claim_amount_ratio')) + assert.ok(fieldKeys.includes('usage_rate')) + assert.ok(fieldKeys.includes('after_usage_rate')) + assert.ok(fieldKeys.includes('remaining_budget_ratio')) + assert.ok(fieldKeys.includes('available_before_amount')) + assert.ok(fieldKeys.includes('over_budget_amount')) +}) + test('budget expense type options expose real expense type codes', () => { const optionCodes = BUDGET_EXPENSE_TYPE_OPTIONS.map((item) => item.value) diff --git a/web/tests/documents-center-status-filter.test.mjs b/web/tests/documents-center-status-filter.test.mjs index 7261104..aacc006 100644 --- a/web/tests/documents-center-status-filter.test.mjs +++ b/web/tests/documents-center-status-filter.test.mjs @@ -25,7 +25,7 @@ const requestsComposable = readFileSync( 'utf8' ) -test('documents center keeps only the top scope tabs and renders status as a dropdown filter', () => { +test('documents center keeps only the top scope tabs and renders risk level as a dropdown filter', () => { assert.match(documentsCenterView, /