
CodeIgniter v4.6.3 Command Line Tool - Server Time: 2026-08-31 07:48:32 UTC+00:00

────────────────────────────────────────────────────────────
[PASS] ai.endpoints.reachable — AI dashboard & insight endpoints reachable
  category=AI  passed=2  failed=0  duration=42ms
  2 assertion(s) passed
    ✓ /ai/dashboard (auth) → 200/302/404 (got 200)
    ✓ /ai/dashboard (anonymous) is NOT 500 (got 400)

────────────────────────────────────────────────────────────
[PASS] asset.registry.crud — Asset registry CRUD + status lifecycle
  category=Asset  passed=2  failed=0  duration=79ms
  2 assertion(s) passed
    ✓ GET /api/assets (anon) is NOT 500 (got 302)
    ✓ GET /api/assets (auth) → 200/302/401/403 (got 200)

────────────────────────────────────────────────────────────
[PASS] assets.unifies_machine_master — Assets Unify Machine Master (Option C)
  category=Assets  passed=33  failed=0  duration=33ms
  33 assertion(s) passed
    ✓ A. assets table exists
    ✓ A. rental_vendor_assets catalogue table exists (kept)
    ✓ A. assets.supplier_vendor_id column exists
    ✓ A. assets.is_vendor_catalogue_only column exists
    ✓ A. assets.clone_source_vendor_asset_id column exists
    ✓ A. idx_assets_supplier_vendor index exists
    ✓ A. idx_assets_clone_source_vendor_asset index exists
    ✓ B. AssetModel.php exists
    ✓ B. AssetModel allowedFields includes is_vendor_catalogue_only
    ✓ B. AssetModel allowedFields includes clone_source_vendor_asset_id
    ✓ B. AssetModel allowedFields includes supplier_vendor_id
    ✓ C. AssetController.php exists
    ✓ C. assetPayload() captures supplier_vendor_id + is_vendor_catalogue_only
    ✓ C. assetFormOptions() loads vendors dropdown for the form
    ✓ D. assets/form.php exists
    ✓ D. assets/form.php has supplier_vendor_id dropdown
    ✓ D. assets/form.php has is_vendor_catalogue_only checkbox
    ✓ D. assets/form.php wraps vendor source in #vendor_source_block
    ✓ D. assets/form.php JS hides vendor block unless acquisition_type is rented_in/operating_lease/client_supplied
    ✓ E. RentalService.php exists
    ✓ E. RentalService::promoteVendorAssetToAssets() method exists
    ✓ E. createWorkOrder() invokes promoteVendorAssetToAssets when rental_asset_id is set
    ✓ E. promoteVendorAssetToAssets sets acquisition_type=rented_in and supplier_vendor_id
    ✓ E. promoteVendorAssetToAssets sets the unify-master columns
    ✓ E. promoteVendorAssetToAssets is idempotent via clone_source_vendor_asset_id lookup
    ✓ F. FulfillmentRouter.php exists
    ✓ F. buildRentOption counts catalogue + on-hire assets
    ✓ G. promoteVendorAssetToAssets created assets row id=449 from vendor catalogue #1
    ✓ G. new asset row carries supplier_vendor_id=1 (got 1)
    ✓ G. new asset row acquisition_type=rented_in (got "rented_in")
    ✓ G. new asset row is_vendor_catalogue_only=0 (got 0)
    ✓ G. new asset row clone_source_vendor_asset_id=1 (got 1)
    ✓ G. new asset_code uses AST-VND- prefix (got "AST-VND-000001")

────────────────────────────────────────────────────────────
[PASS] auth.login.admin — Admin Login (loginPost)
  category=Auth  passed=3  failed=0  duration=91ms
  3 assertion(s) passed
    ✓ loginPost returns 303, got 303
    ✓ Response sets ci_session cookie
    ✓ Total time: 91ms

────────────────────────────────────────────────────────────
[PASS] hr.leave.lifecycle — Leave application + approve/reject lifecycle
  category=HR  passed=5  failed=0  duration=81ms
  5 assertion(s) passed
    ✓ /hr/leaves (auth) → 200/302 (got 200)
    ✓ /hr/leaves/apply (auth) → 200/302 (got 200)
    ✓ POST /leaves/999999/approve is NOT 500 (got 404)
    ✓ leave_requests table queryable (rows=5)
    ✓ no leave with start_date > end_date (found 0)

────────────────────────────────────────────────────────────
[PASS] inventory.stock.boundary — Inventory tables + stock invariants
  category=Inventory  passed=6  failed=0  duration=172ms
  6 assertion(s) passed
    ✓ items table populated (rows=4418)
    ✓ items table uses category_id (not legacy category)
    ✓ stock has no quantity column (informational)
    ✓ stock_movements.quantity has no negative values (found 0)
    ✓ items.reorder_level has no negative values (found 0)
    ✓ GET /inventory (auth) → 200/302 (got 200)

────────────────────────────────────────────────────────────
[PASS] procurement.indent.approve.auto_artefact — Indent Approve -> Auto-MR/PR artefact
  category=Procurement  passed=16  failed=0  duration=21ms
  16 assertion(s) passed
    ✓ [RENTAL] fixture indent created (id=94)
    ✓ [RENTAL] created=True kind='MR' (got kind=MR)
    ✓ [RENTAL] MR id returned (25)
    ✓ [RENTAL] MR.source_indent_id = 94 (got 94)
    ✓ [RENTAL] MR.request_no starts with 'MR-' (got MR-202608-0001)
    ✓ [RENTAL] idempotent - second call returns same MR id
    ✓ [RENTAL] idempotent - reason notes 'already exists'
    ✓ [PURCHASE] fixture indent created (id=95)
    ✓ [PURCHASE] created=True kind='PR' (got kind=PR)
    ✓ [PURCHASE] PR id returned (37)
    ✓ [PURCHASE] indents.converted_pr_id stamped
    ✓ [OWN_FLEET] fixture indent created (id=96)
    ✓ [OWN_FLEET] no artefact kind returned (got NULL)
    ✓ [OWN_FLEET] reason notes OWN/reservation: OWN_FLEET handled by reservation, no separate artefact
    ✓ [UNRESOLVED] fixture indent created (id=97)
    ✓ [UNRESOLVED] reason notes UNRESOLVED: route is UNRESOLVED, skipping artefact creation

────────────────────────────────────────────────────────────
[PASS] procurement.indent.list-edit — Indent list + edit pages load
  category=Procurement  passed=2  failed=0  duration=116ms
  2 assertion(s) passed
    ✓ indents list returns 200 or 302, got 200
    ✓ indents/edit/85 returns 200 or 302, got 200

────────────────────────────────────────────────────────────
[PASS] procurement.indent.shows_routing — Indent → Routing & Fulfillment Card
  category=Procurement  passed=26  failed=0  duration=2ms
  26 assertion(s) passed
    ✓ A. seed data — at least one RENTAL indent exists (got id=59)
    ✓ A. seed data — at least one OWN_FLEET indent exists (got id=64)
    ✓ A. seed data — at least one UNRESOLVED indent exists (got id=41)
    ✓ B. indent view.php exists
    ✓ B. view.php contains "Routing & Fulfillment" section header
    ✓ B. view.php branches on all 4 fulfillment_source values
    ✓ B. view.php references $linkedMachineRequest (controller pass-through)
    ✓ B. view.php references $linkedReservedAssets
    ✓ B. view.php references $routingDecisions
    ✓ B. view.php renders override_reason banner
    ✓ C. RENTAL indent #59 has a linked machine_requests row (got id=13 request_no=MR-202607-0001)
    ✓ C. linked MR request_no starts with "MR-" (got "MR-202607-0001")
    ✓ D. view.php renders Reserved Until column
    ✓ D. view.php links to /assets/{id} for reserved assets
    ✓ D. view.php links to /rental/machine-requests/{id} for RENTAL MR
    ✓ E. view.php PURCHASE branch renders PR linkage text
    ✓ E. view.php links to /purchase-requisitions/{id} for PURCHASE PR
    ✓ F. view.php UNRESOLVED branch renders "Pending Routing" explanation
    ✓ G. decision_logs table exists
    ✓ G. decision_logs fetchable for indent #59 (got 1 entries)
    ✓ G. first decision_log decision mentions route/create (got "route_chosen")
    ✓ H. IndentController passes linkedMachineRequest to view
    ✓ H. IndentController passes linkedReservedAssets to view
    ✓ H. IndentController passes routingDecisions to view
    ✓ H. IndentController queries machine_requests.source_indent_id
    ✓ H. IndentController queries assets.reserved_for_indent_id

────────────────────────────────────────────────────────────
[FAIL] procurement.vendor_pool.rates — Vendor pool rate-card CRUD + Smart Route hook
  category=Procurement  passed=10  failed=3  duration=5ms
  10/13 passed — first failure: picked daily rate = 13500 (got 2200)
    ✓ found a vendor fixture (id=1)
    ✓ rate card inserted (id=17)
    ✓ rate_per_day roundtrip = 13500
    ✓ pickCheapest("Excavator") returns a row
    ✗ picked daily rate = 13500 (got 2200)
    ✗ transport = mobilisation+demob = 10000 (got 14000)
    ✓ vendor_name returned (Shree Cement Suppliers)
    ✓ update succeeded — rate_per_day now 14500
    ✗ pickCheapest reflects update
    ✓ deactivated card is excluded from pickCheapest (picked=rate_id=6, expected !=17)
    ✓ reactivated card is picked again
    ✓ rent_options populated by FulfillmentRouter
    ✓ rent daily_rate populated (2200)

────────────────────────────────────────────────────────────
[FAIL] project.crud.reachable — Project CRUD endpoints reachable
  category=ProjectMgmt  passed=3  failed=1  duration=46ms
  3/4 passed — first failure: GET /projects (auth) → 200/302 (got 400)
    ✗ GET /projects (auth) → 200/302 (got 400)
    ✓ GET /projects/view/1 (auth) → 200/302/404 (got 404) ← REAL DEFECT: route shadowed
    ✓ GET /projects (anon) is NOT 500 (got 302)
    ✓ no project with assigned_manager_id pointing to missing user (found 0)

────────────────────────────────────────────────────────────
[PASS] rental.dispatch.lifecycle — Dispatch Note lifecycle (draft -> in_transit -> delivered)
  category=Rental  passed=12  failed=0  duration=7ms
  12 assertion(s) passed
    ✓ draft dispatch created (id=14)
    ✓ dispatch_no starts with DN-
    ✓ after transit, status=in_transit (got in_transit)
    ✓ dispatched_at populated
    ✓ assets.status='on_hire' after transit (got 'on_hire')
    ✓ after deliver, status=delivered (got delivered)
    ✓ received_at populated
    ✓ assets.status='active' after deliver (got 'active')
    ✓ illegal transition delivered->draft throws
    ✓ illegal transition delivered->cancelled throws (delivered is terminal)
    ✓ cancel from in_transit → status=cancelled
    ✓ assets.status='idle' after cancel (got 'idle')

────────────────────────────────────────────────────────────
[FAIL] rental.enterprise_work_order — Enterprise Rental Work Order (canonical contract)
  category=Rental  passed=66  failed=51  duration=153ms
  66/117 passed — first failure: 4. create view has section: 'Work Order Header'
    ✓ 1. child table exists: rental_wo_line_items
    ✓ 1. child table exists: rental_wo_assets
    ✓ 1. child table exists: rental_wo_rate_slabs
    ✓ 1. child table exists: rental_wo_billing_rules
    ✓ 1. child table exists: rental_wo_responsibilities
    ✓ 1. child table exists: rental_wo_penalties
    ✓ 1. child table exists: rental_wo_incentives
    ✓ 1. child table exists: rental_wo_operations
    ✓ 1. child table exists: rental_wo_compliance
    ✓ 1. child table exists: rental_wo_mobilization
    ✓ 1. child table exists: rental_wo_workflow
    ✓ 1. child table exists: rental_wo_documents
    ✓ 1. child table exists: rental_wo_payment_milestones
    ✓ 2. rental_work_orders.revision_no exists
    ✓ 2. rental_work_orders.amendment_no exists
    ✓ 2. rental_work_orders.issue_date exists
    ✓ 2. rental_work_orders.effective_date exists
    ✓ 2. rental_work_orders.planned_end_date exists
    ✓ 2. rental_work_orders.actual_end_date exists
    ✓ 2. rental_work_orders.contract_reference exists
    ✓ 2. rental_work_orders.agreement_reference exists
    ✓ 2. rental_work_orders.tender_reference exists
    ✓ 2. rental_work_orders.zone exists
    ✓ 2. rental_work_orders.structure exists
    ✓ 2. rental_work_orders.chainage exists
    ✓ 2. rental_work_orders.activity exists
    ✓ 2. rental_work_orders.work_package exists
    ✓ 2. rental_work_orders.reporting_engineer_id exists
    ✓ 2. rental_work_orders.site_manager_id exists
    ✓ 2. rental_work_orders.department exists
    ✓ 2. rental_work_orders.cost_center exists
    ✓ 2. rental_work_orders.business_unit exists
    ✓ 2. rental_work_orders.shift_pattern exists
    ✓ 2. rental_work_orders.working_days exists
    ✓ 2. rental_work_orders.primary_measurement_method exists
    ✓ 2. rental_work_orders.hybrid_formula_json exists
    ✓ 2. rental_work_orders.min_guarantee_hours exists
    ✓ 2. rental_work_orders.included_hours exists
    ✓ 2. rental_work_orders.max_billable_days exists
    ✓ 2. rental_work_orders.advance_pct exists
    ✓ 2. rental_work_orders.retention_pct exists
    ✓ 2. rental_work_orders.retention_release_days exists
    ✓ 2. rental_work_orders.payment_days exists
    ✓ 2. rental_work_orders.tds_category exists
    ✓ 2. rental_work_orders.milestone_billing exists
    ✓ 2. rental_work_orders.mobilization_status exists
    ✓ 2. rental_work_orders.demobilization_status exists
    ✓ 2. rental_work_orders.current_stage exists
    ✓ 2. rental_work_orders.next_stage exists
    ✓ 2. rental_work_orders.vendor_accepted_at exists
    ✓ 3. rental_wo_line_items has >= 3 rows for WO 151 (got 3)
    ✓ 3. rental_wo_rate_slabs has >= 5 rows for WO 151 (got 7)
    ✓ 3. rental_wo_billing_rules has >= 2 rows for WO 151 (got 3)
    ✓ 3. rental_wo_responsibilities has >= 5 rows for WO 151 (got 9)
    ✓ 3. rental_wo_penalties has >= 3 rows for WO 151 (got 5)
    ✓ 3. rental_wo_incentives has >= 2 rows for WO 151 (got 4)
    ✓ 3. rental_wo_operations has >= 2 rows for WO 151 (got 4)
    ✓ 3. rental_wo_compliance has >= 3 rows for WO 151 (got 5)
    ✓ 3. rental_wo_mobilization has >= 5 rows for WO 151 (got 12)
    ✓ 3. rental_wo_workflow has >= 11 rows for WO 151 (got 11)
    ✓ 3. rental_wo_documents has >= 3 rows for WO 151 (got 5)
    ✓ 3. rental_wo_payment_milestones has >= 3 rows for WO 151 (got 3)
    ✗ 4. create view has section: 'Work Order Header'
    ✗ 4. create view has section: 'Vendor Information'
    ✗ 4. create view has section: 'Project Deployment'
    ✗ 4. create view has section: 'Line Items'
    ✗ 4. create view has section: 'Operation Assignment'
    ✗ 4. create view has section: 'Measurement Rules'
    ✗ 4. create view has section: 'Commercial Rate Slabs'
    ✗ 4. create view has section: 'Dynamic Billing Rules'
    ✗ 4. create view has section: 'Responsibilities'
    ✗ 4. create view has section: 'Penalty Matrix'
    ✗ 4. create view has section: 'Incentive Matrix'
    ✗ 4. create view has section: 'Payment Terms'
    ✗ 4. create view has section: 'Workflow'
    ✗ 5. show view has section: 'Work Order Header'
    ✗ 5. show view has section: 'Vendor Information'
    ✗ 5. show view has section: 'Project Deployment'
    ✗ 5. show view has section: 'Line Items'
    ✗ 5. show view has section: 'Operation Assignment'
    ✗ 5. show view has section: 'Measurement Rules'
    ✗ 5. show view has section: 'Commercial Rate Slabs'
    ✗ 5. show view has section: 'Dynamic Billing Rules'
    ✗ 5. show view has section: 'Responsibilities'
    ✗ 5. show view has section: 'Penalty Matrix'
    ✗ 5. show view has section: 'Incentive Matrix'
    ✗ 5. show view has section: 'Payment Terms'
    ✗ 5. show view has section: 'Workflow'
    ✗ 5. show view has Lifecycle visualisation
    ✗ 5. show view has TOTAL WO VALUE KPI tile
    ✗ 5. show view renders Excavator 20T line item
    ✗ 5. show view renders Tipper 16 cum line item
    ✗ 5. show view renders Mobilization Advance milestone
    ✗ 5. show view renders document attachments
    ✗ 5. show view renders compliance issuer (ICICI Lombard)
    ✗ 5. show view renders penalty data (late mobilization)
    ✗ 5. show view renders incentive data (early completion)
    ✗ 5. show view renders responsibility: Fuel
    ✗ 6a. /rental/vendor-assets/search returns { items, total, ... } JSON
    ✗ 6b. create view has #vendorMachineModal popup
    ✗ 6b. create view has #btnBulkSearch trigger button
    ✗ 6b. create view shows 'Search Vendor Machines' button label
    ✗ 6b. create view has #modalMachinesBody table body
    ✗ 6b. create view has vendor filter in popup
    ✗ 6b. create view has max-age filter in popup
    ✗ 6b. create view has insurance compliance filter
    ✗ 6b. create view has fitness compliance filter
    ✗ 6b. create view has 'Stage Selected' button
    ✗ 6b. create view has 'Apply Staged Machines' button
    ✗ 6b. create view has 'Add Blank Row' button (fallback)
    ✗ 6b. create view posts hidden line_rental_asset_id[] field
    ✗ 6b. create view JS uses VM_SEARCH_URL constant
    ✓ 6c. controller reads line_rental_asset_id[] from POST
    ✓ 6c. controller inserts into rental_wo_assets table when popup-sourced
    ✓ 6d. /rental/vendor-assets/search route exists
    ✓ 6f. search by vendor_id works (vendor #1)
    ✗ 6g. status=available filter works

────────────────────────────────────────────────────────────
[PASS] rental.mr.shows_source_indent — Machine Request → Source Indent Synchronisation
  category=Rental  passed=24  failed=0  duration=8ms
  24 assertion(s) passed
    ✓ A. seed data — found at least one MR with source_indent_id set
    ✓ A. seed MR.source_indent_id is positive (id=59)
    ✓ B. getWithDetails returned a row
    ✓ B. joined field 'source_indent_no' is present in row (got ''IND-202607-0007'')
    ✓ B. joined field 'source_indent_status' is present in row (got ''approved'')
    ✓ B. joined field 'source_indent_priority' is present in row (got ''medium'')
    ✓ B. joined field 'source_indent_fulfillment' is present in row (got ''RENTAL'')
    ✓ B. joined field 'source_indent_required_by' is present in row (got ''2026-07-22'')
    ✓ B. source_indent_no is populated (got "IND-202607-0007")
    ✓ B. source_indent_fulfillment is one of OWN_FLEET|RENTAL|PURCHASE|UNRESOLVED (got "RENTAL")
    ✓ C. show.php exists
    ✓ C. show.php contains "Source Indent" header text
    ✓ C. show.php links use the active /indents/{id} route (not /procurement/indents/show/{id})
    ✓ C. show.php references $r[source_indent_no]
    ✓ C. show.php references $r[source_indent_fulfillment]
    ✓ D. index.php exists
    ✓ D. index.php has a "Source Indent" column header
    ✓ D. index.php link uses active /indents/{id} route
    ✓ D. index.php renders $r[source_indent_no]
    ✓ D. index.php handles legacy_unsourced MRs separately
    ✓ E. getList returned an array
    ✓ E. getList rows include source_indent_no field (got key present=yes)
    ✓ F. getWithDetails works for MR without source_indent_id (id=20)
    ✓ G. no legacy_unsourced=1 row found — scenario skipped

────────────────────────────────────────────────────────────
[PASS] rental.vendor_asset.unified_view — Vendor Asset View matches Asset Master
  category=Rental  passed=75  failed=0  duration=198ms
  75 assertion(s) passed
    ✓ A. seed data — at least one rental_vendor_assets row exists (id=1)
    ✓ B. vendor_assets/show.php exists
    ✓ B. view title includes "Vendor Machine" / "Vendor Asset"
    ✓ C. tab "Overview" (#vat-overview) is present
    ✓ C. tab "Technical" (#vat-technical) is present
    ✓ C. tab "Compliance" (#vat-compliance) is present
    ✓ C. tab "Deployment" (#vat-deployment) is present
    ✓ C. tab "Work Orders" (#vat-wo) is present
    ✓ C. tab "History" (#vat-history) is present
    ✓ C. tab "Operations" (#vat-operations) is present
    ✓ D. KPI card "Compliance Health" present
    ✓ D. KPI card "Active WO" present
    ✓ D. KPI card "Total WO History" present
    ✓ D. KPI card "Age / 5-Year Rule" present
    ✓ E. SVG health ring (matches asset detail.php header)
    ✓ F. Gradient header banner style matches asset detail.php
    ✓ G. Cross-link banner present (Unified or Catalogue-only)
    ✓ G. Cross-link banner uses base_url('assets/vat-operations) to navigate to master
    ✓ H. "Identity & Classification" section present (Overview)
    ✓ H. "Identification Numbers" section present (Overview)
    ✓ H. "Current State" section present (Overview)
    ✓ H. "Engine & Powertrain" section present (Technical)
    ✓ H. "Meters & Hours" section present (Technical)
    ✓ H. "Performance" section present (Technical)
    ✓ I. expiry-color helper used (matches detail.php behaviour)
    ✓ I. Insurance card present
    ✓ I. Fitness Certificate card present
    ✓ J. "Current Deployment" card present
    ✓ J. "Next PM" card present
    ✓ J. PM Schedules list rendered when available
    ✓ K. "Rental Work Order History" table preserved
    ✓ L. Timeline card mentions "Promoted to assets.id" event
    ✓ M. "Quick Status Update" form preserved for /update POST route
    ✓ M. Quick Status Update form posts to /update endpoint
    ✓ M2. Operations sub-section #ops-score is present
    ✓ M2. Operations sub-section title "Operational Score &amp; KPIs" is present
    ✓ M2. Operations sub-section #ops-items is present
    ✓ M2. Operations sub-section title "Asset Items &amp; Installed Components" is present
    ✓ M2. Operations sub-section #ops-compliance is present
    ✓ M2. Operations sub-section title "Complete Compliance Status" is present
    ✓ M2. Operations sub-section #ops-maintenance is present
    ✓ M2. Operations sub-section title "Maintenance Management" is present
    ✓ M2. Operations sub-section #ops-logbook is present
    ✓ M2. Operations sub-section title "Asset Operations — Daily Log Summary" is present
    ✓ M2. Operations sub-section #ops-analysis is present
    ✓ M2. Operations sub-section title "Asset Complete Analysis" is present
    ✓ M2. Operations sub-section #ops-ai is present
    ✓ M2. Operations sub-section title "AI Analysis &amp; Recommendations" is present
    ✓ M2. Operations Ops Score badge present (right side of section nav)
    ✓ M2. Operations YTD Cost Breakdown sub-section present
    ✓ M2. Operations Breakdown History sub-section present
    ✓ M2. Operations Preventive Maintenance Schedule sub-section present
    ✓ M2. Operations YTD Total card present
    ✓ M2. Operations shows friendly message when no linked asset
    ✓ N. RentalVendorAssetController.php exists
    ✓ N. controller passes linkedAsset to view
    ✓ N. controller queries assets.clone_source_vendor_asset_id for cross-link
    ✓ N. controller fetches linked-asset compliance docs
    ✓ N. controller fetches PM schedules
    ✓ N. controller fetches maintenance history
    ✓ N. controller fetches breakdowns (for Operations tab)
    ✓ N. controller fetches perfMetrics (for Operations tab)
    ✓ N. controller fetches installedItems (for Operations tab)
    ✓ N. controller fetches logbookEntries (for Operations tab)
    ✓ N. controller fetches costByType (for Operations tab)
    ✓ N. controller fetches aiInsights (for Operations tab)
    ✓ N. controller fetches aiRecommendations (for Operations tab)
    ✓ O. GET http://localhost/Rajpath%20civitas/public/rental/vendor-assets/1 returned HTTP 200 (165932 bytes)
    ✓ O. rendered HTML contains #vat-overview tab
    ✓ O. rendered HTML contains Compliance Health KPI card
    ✓ O. rendered HTML contains Engine & Powertrain section
    ✓ O. rendered HTML contains #vat-operations tab
    ✓ O. rendered HTML contains Operations sub-section #ops-score
    ✓ O. rendered HTML contains Operational Score heading
    ✓ P. seed data — promoted assets row exists for cross-link (assets.id=449, clone_source_vendor_asset_id=1)

────────────────────────────────────────────────────────────
[PASS] routing.asset.reservation — Asset Reservation (OWN_FLEET lock)
  category=Routing  passed=13  failed=0  duration=17ms
  13 assertion(s) passed
    ✓ Found an OWN_FLEET indent to test against (id=64)
    ✓ No pre-existing reservation for this indent (count=0)
    ✓ autoReserveForOwnFleet() returned array
    ✓ result has reserved key
    ✓ result has reason key
    ✓ reserved=True has asset_id=449
    ✓ reserved_until is in the future (2026-09-03 07:48:33)
    ✓ reservation_hours is 48 or 72 (got 72)
    ✓ assets.reserved_for_indent_id = 64 (got 64)
    ✓ assets.reserved_until is future-dated (2026-09-03 07:48:33)
    ✓ idempotent — second call returns same asset_id
    ✓ second call reason notes "already reserved"
    ✓ Cleanup succeeded — no leftover reservations on test indent

────────────────────────────────────────────────────────────
[PASS] routing.fulfillment.router — Fulfillment Router decisions
  category=Routing  passed=24  failed=0  duration=27ms
  24 assertion(s) passed
    ✓ [hire_type] recommendation is a valid constant (RENTAL)
    ✓ [hire_type] confidence is a float (0.95)
    ✓ [hire_type] reason_bullets non-empty (3)
    ✓ [hire_type] rent_options populated
    ✓ [hire_type] rent vendor_name is real (OEM Direct - Tata Motors)
    ✓ [hire_type] rent vendor_count_available returned (457)
    ✓ [asset_type] recommendation is a valid constant (PURCHASE)
    ✓ [asset_type] confidence is a float (0.75)
    ✓ [asset_type] reason_bullets non-empty (3)
    ✓ [asset_type] rent_options populated
    ✓ [asset_type] rent vendor_name is real (OEM Direct - Tata Motors)
    ✓ [asset_type] rent vendor_count_available returned (457)
    ✓ [material_type] recommendation is a valid constant (PURCHASE)
    ✓ [material_type] confidence is a float (0.75)
    ✓ [material_type] reason_bullets non-empty (3)
    ✓ [material_type] rent_options populated
    ✓ [material_type] rent vendor_name is real (OEM Direct - Tata Motors)
    ✓ [material_type] rent vendor_count_available returned (457)
    ✓ [consumable_type] recommendation is a valid constant (PURCHASE)
    ✓ [consumable_type] confidence is a float (0.75)
    ✓ [consumable_type] reason_bullets non-empty (3)
    ✓ [consumable_type] rent_options populated
    ✓ [consumable_type] rent vendor_name is real (OEM Direct - Tata Motors)
    ✓ [consumable_type] rent vendor_count_available returned (457)

────────────────────────────────────────────────────────────
[PASS] routing.smart-route.card-render — Smart Route card renders on Indent form
  category=Routing  passed=7  failed=0  duration=74ms
  7 assertion(s) passed
    ✓ Page returned 200 or 302, got 200
    ✓ HTML contains "Smart Route"
    ✓ HTML contains OWN column
    ✓ HTML contains RENTAL column
    ✓ HTML contains PURCHASE column
    ✓ Trust banner present
    ✓ fulfillment_source hidden input present

────────────────────────────────────────────────────────────
[FAIL] system.approval-inbox.reachable — Approval inbox + approve endpoint reachability
  category=System  passed=4  failed=4  duration=65ms
  4/8 passed — first failure: /procurement/approvals (auth) → 200/302 (got 400) ← REAL DEFECT
    ✗ /procurement/approvals (auth) → 200/302 (got 400) ← REAL DEFECT
    ✗ /approvals (auth) → 200/302 (got 400) ← REAL DEFECT
    ✗ /procurement/approvals/review/1 (auth) → 200/302 (got 400) ← REAL DEFECT
    ✗ /procurement/approvals/show/1 (auth) → 200/302 (got 400) ← REAL DEFECT
    ✓ /procurement/approvals (anonymous) is NOT 500 (got 302)
    ✓ /approvals (anonymous) is NOT 500 (got 302)
    ✓ /procurement/approvals/review/1 (anonymous) is NOT 500 (got 302)
    ✓ /procurement/approvals/show/1 (anonymous) is NOT 500 (got 302)

────────────────────────────────────────────────────────────
[PASS] system.db.core-tables — Database core tables exist and accessible
  category=Database  passed=13  failed=0  duration=61ms
  13 assertion(s) passed
    ✓ users exists (rows=21)
    ✓ indents exists (rows=15)
    ✓ purchase_requisitions exists (rows=5)
    ✓ assets exists (rows=449)
    ✓ machine_requests exists (rows=5)
    ✓ vendors exists (rows=44)
    ✓ sites exists (rows=3)
    ✓ projects exists (rows=1)
    ✓ decision_logs exists (rows=31)
    ✓ skill_runs exists (rows=761)
    ✓ admin@rajpath.local user present
    ✓ admin has password_hash set (hash prefix: $2y$10$)
    ✓ admin hash verifies against Admin@12345

────────────────────────────────────────────────────────────
[PASS] system.env.config-and-extensions — Environment config + extensions
  category=System  passed=13  failed=0  duration=0ms
  13 assertion(s) passed
    ✓ PHP version 8.1+ — actual: 8.5.5
    ✓ Extension 'curl' loaded
    ✓ Extension 'mbstring' loaded
    ✓ Extension 'mysqli' loaded
    ✓ Extension 'json' loaded
    ✓ Extension 'zip' loaded
    ✓ .env exists
    ✓ .env has DB hostname
    ✓ .env has app.baseURL
    ✓ PDO/MySQL connection established
    ✓ Connected DB: civitas
    ✓ writable/logs is writable
    ✓ writable/ is writable

────────────────────────────────────────────────────────────
[PASS] system.routes.core — Core routes reachable (no 404/500)
  category=System  passed=8  failed=0  duration=305ms
  8 assertion(s) passed
    ✓ login → 200
    ✓ dashboard → 200
    ✓ indents_index → 200
    ✓ indents_create → 200
    ✓ rental_mr_index → 200
    ✓ rental_mr_create → 200
    ✓ assets_index → 200
    ✓ smart_route → 200

────────────────────────────────────────────────────────────
[PASS] system.security.headers — Security headers presence (X-Frame, CSP, X-Content-Type)
  category=System  passed=4  failed=0  duration=48ms
  4 assertion(s) passed
    ✓ / has at least one hardening header
    ✓ /login has at least one hardening header
    ✓ all 2/total pages have hardening headers (got 2/2)
    ✓ POST /loginPost (empty body) is NOT 500 (got 404)


SUITE: All Skills
  total=23  passed=19  failed=4  duration=1700ms

────────────────────────────────────────────────────────────
[PASS] ai.endpoints.reachable — AI dashboard & insight endpoints reachable
  category=AI  passed=2  failed=0  duration=42ms
  2 assertion(s) passed
    ✓ /ai/dashboard (auth) → 200/302/404 (got 200)
    ✓ /ai/dashboard (anonymous) is NOT 500 (got 400)

────────────────────────────────────────────────────────────
[PASS] asset.registry.crud — Asset registry CRUD + status lifecycle
  category=Asset  passed=2  failed=0  duration=79ms
  2 assertion(s) passed
    ✓ GET /api/assets (anon) is NOT 500 (got 302)
    ✓ GET /api/assets (auth) → 200/302/401/403 (got 200)

────────────────────────────────────────────────────────────
[PASS] assets.unifies_machine_master — Assets Unify Machine Master (Option C)
  category=Assets  passed=33  failed=0  duration=33ms
  33 assertion(s) passed
    ✓ A. assets table exists
    ✓ A. rental_vendor_assets catalogue table exists (kept)
    ✓ A. assets.supplier_vendor_id column exists
    ✓ A. assets.is_vendor_catalogue_only column exists
    ✓ A. assets.clone_source_vendor_asset_id column exists
    ✓ A. idx_assets_supplier_vendor index exists
    ✓ A. idx_assets_clone_source_vendor_asset index exists
    ✓ B. AssetModel.php exists
    ✓ B. AssetModel allowedFields includes is_vendor_catalogue_only
    ✓ B. AssetModel allowedFields includes clone_source_vendor_asset_id
    ✓ B. AssetModel allowedFields includes supplier_vendor_id
    ✓ C. AssetController.php exists
    ✓ C. assetPayload() captures supplier_vendor_id + is_vendor_catalogue_only
    ✓ C. assetFormOptions() loads vendors dropdown for the form
    ✓ D. assets/form.php exists
    ✓ D. assets/form.php has supplier_vendor_id dropdown
    ✓ D. assets/form.php has is_vendor_catalogue_only checkbox
    ✓ D. assets/form.php wraps vendor source in #vendor_source_block
    ✓ D. assets/form.php JS hides vendor block unless acquisition_type is rented_in/operating_lease/client_supplied
    ✓ E. RentalService.php exists
    ✓ E. RentalService::promoteVendorAssetToAssets() method exists
    ✓ E. createWorkOrder() invokes promoteVendorAssetToAssets when rental_asset_id is set
    ✓ E. promoteVendorAssetToAssets sets acquisition_type=rented_in and supplier_vendor_id
    ✓ E. promoteVendorAssetToAssets sets the unify-master columns
    ✓ E. promoteVendorAssetToAssets is idempotent via clone_source_vendor_asset_id lookup
    ✓ F. FulfillmentRouter.php exists
    ✓ F. buildRentOption counts catalogue + on-hire assets
    ✓ G. promoteVendorAssetToAssets created assets row id=449 from vendor catalogue #1
    ✓ G. new asset row carries supplier_vendor_id=1 (got 1)
    ✓ G. new asset row acquisition_type=rented_in (got "rented_in")
    ✓ G. new asset row is_vendor_catalogue_only=0 (got 0)
    ✓ G. new asset row clone_source_vendor_asset_id=1 (got 1)
    ✓ G. new asset_code uses AST-VND- prefix (got "AST-VND-000001")

────────────────────────────────────────────────────────────
[PASS] auth.login.admin — Admin Login (loginPost)
  category=Auth  passed=3  failed=0  duration=91ms
  3 assertion(s) passed
    ✓ loginPost returns 303, got 303
    ✓ Response sets ci_session cookie
    ✓ Total time: 91ms

────────────────────────────────────────────────────────────
[PASS] hr.leave.lifecycle — Leave application + approve/reject lifecycle
  category=HR  passed=5  failed=0  duration=81ms
  5 assertion(s) passed
    ✓ /hr/leaves (auth) → 200/302 (got 200)
    ✓ /hr/leaves/apply (auth) → 200/302 (got 200)
    ✓ POST /leaves/999999/approve is NOT 500 (got 404)
    ✓ leave_requests table queryable (rows=5)
    ✓ no leave with start_date > end_date (found 0)

────────────────────────────────────────────────────────────
[PASS] inventory.stock.boundary — Inventory tables + stock invariants
  category=Inventory  passed=6  failed=0  duration=172ms
  6 assertion(s) passed
    ✓ items table populated (rows=4418)
    ✓ items table uses category_id (not legacy category)
    ✓ stock has no quantity column (informational)
    ✓ stock_movements.quantity has no negative values (found 0)
    ✓ items.reorder_level has no negative values (found 0)
    ✓ GET /inventory (auth) → 200/302 (got 200)

────────────────────────────────────────────────────────────
[PASS] procurement.indent.approve.auto_artefact — Indent Approve -> Auto-MR/PR artefact
  category=Procurement  passed=16  failed=0  duration=21ms
  16 assertion(s) passed
    ✓ [RENTAL] fixture indent created (id=94)
    ✓ [RENTAL] created=True kind='MR' (got kind=MR)
    ✓ [RENTAL] MR id returned (25)
    ✓ [RENTAL] MR.source_indent_id = 94 (got 94)
    ✓ [RENTAL] MR.request_no starts with 'MR-' (got MR-202608-0001)
    ✓ [RENTAL] idempotent - second call returns same MR id
    ✓ [RENTAL] idempotent - reason notes 'already exists'
    ✓ [PURCHASE] fixture indent created (id=95)
    ✓ [PURCHASE] created=True kind='PR' (got kind=PR)
    ✓ [PURCHASE] PR id returned (37)
    ✓ [PURCHASE] indents.converted_pr_id stamped
    ✓ [OWN_FLEET] fixture indent created (id=96)
    ✓ [OWN_FLEET] no artefact kind returned (got NULL)
    ✓ [OWN_FLEET] reason notes OWN/reservation: OWN_FLEET handled by reservation, no separate artefact
    ✓ [UNRESOLVED] fixture indent created (id=97)
    ✓ [UNRESOLVED] reason notes UNRESOLVED: route is UNRESOLVED, skipping artefact creation

────────────────────────────────────────────────────────────
[PASS] procurement.indent.list-edit — Indent list + edit pages load
  category=Procurement  passed=2  failed=0  duration=116ms
  2 assertion(s) passed
    ✓ indents list returns 200 or 302, got 200
    ✓ indents/edit/85 returns 200 or 302, got 200

────────────────────────────────────────────────────────────
[PASS] procurement.indent.shows_routing — Indent → Routing & Fulfillment Card
  category=Procurement  passed=26  failed=0  duration=2ms
  26 assertion(s) passed
    ✓ A. seed data — at least one RENTAL indent exists (got id=59)
    ✓ A. seed data — at least one OWN_FLEET indent exists (got id=64)
    ✓ A. seed data — at least one UNRESOLVED indent exists (got id=41)
    ✓ B. indent view.php exists
    ✓ B. view.php contains "Routing & Fulfillment" section header
    ✓ B. view.php branches on all 4 fulfillment_source values
    ✓ B. view.php references $linkedMachineRequest (controller pass-through)
    ✓ B. view.php references $linkedReservedAssets
    ✓ B. view.php references $routingDecisions
    ✓ B. view.php renders override_reason banner
    ✓ C. RENTAL indent #59 has a linked machine_requests row (got id=13 request_no=MR-202607-0001)
    ✓ C. linked MR request_no starts with "MR-" (got "MR-202607-0001")
    ✓ D. view.php renders Reserved Until column
    ✓ D. view.php links to /assets/{id} for reserved assets
    ✓ D. view.php links to /rental/machine-requests/{id} for RENTAL MR
    ✓ E. view.php PURCHASE branch renders PR linkage text
    ✓ E. view.php links to /purchase-requisitions/{id} for PURCHASE PR
    ✓ F. view.php UNRESOLVED branch renders "Pending Routing" explanation
    ✓ G. decision_logs table exists
    ✓ G. decision_logs fetchable for indent #59 (got 1 entries)
    ✓ G. first decision_log decision mentions route/create (got "route_chosen")
    ✓ H. IndentController passes linkedMachineRequest to view
    ✓ H. IndentController passes linkedReservedAssets to view
    ✓ H. IndentController passes routingDecisions to view
    ✓ H. IndentController queries machine_requests.source_indent_id
    ✓ H. IndentController queries assets.reserved_for_indent_id

────────────────────────────────────────────────────────────
[FAIL] procurement.vendor_pool.rates — Vendor pool rate-card CRUD + Smart Route hook
  category=Procurement  passed=10  failed=3  duration=5ms
  10/13 passed — first failure: picked daily rate = 13500 (got 2200)
    ✓ found a vendor fixture (id=1)
    ✓ rate card inserted (id=17)
    ✓ rate_per_day roundtrip = 13500
    ✓ pickCheapest("Excavator") returns a row
    ✗ picked daily rate = 13500 (got 2200)
    ✗ transport = mobilisation+demob = 10000 (got 14000)
    ✓ vendor_name returned (Shree Cement Suppliers)
    ✓ update succeeded — rate_per_day now 14500
    ✗ pickCheapest reflects update
    ✓ deactivated card is excluded from pickCheapest (picked=rate_id=6, expected !=17)
    ✓ reactivated card is picked again
    ✓ rent_options populated by FulfillmentRouter
    ✓ rent daily_rate populated (2200)

────────────────────────────────────────────────────────────
[FAIL] project.crud.reachable — Project CRUD endpoints reachable
  category=ProjectMgmt  passed=3  failed=1  duration=46ms
  3/4 passed — first failure: GET /projects (auth) → 200/302 (got 400)
    ✗ GET /projects (auth) → 200/302 (got 400)
    ✓ GET /projects/view/1 (auth) → 200/302/404 (got 404) ← REAL DEFECT: route shadowed
    ✓ GET /projects (anon) is NOT 500 (got 302)
    ✓ no project with assigned_manager_id pointing to missing user (found 0)

────────────────────────────────────────────────────────────
[PASS] rental.dispatch.lifecycle — Dispatch Note lifecycle (draft -> in_transit -> delivered)
  category=Rental  passed=12  failed=0  duration=7ms
  12 assertion(s) passed
    ✓ draft dispatch created (id=14)
    ✓ dispatch_no starts with DN-
    ✓ after transit, status=in_transit (got in_transit)
    ✓ dispatched_at populated
    ✓ assets.status='on_hire' after transit (got 'on_hire')
    ✓ after deliver, status=delivered (got delivered)
    ✓ received_at populated
    ✓ assets.status='active' after deliver (got 'active')
    ✓ illegal transition delivered->draft throws
    ✓ illegal transition delivered->cancelled throws (delivered is terminal)
    ✓ cancel from in_transit → status=cancelled
    ✓ assets.status='idle' after cancel (got 'idle')

────────────────────────────────────────────────────────────
[FAIL] rental.enterprise_work_order — Enterprise Rental Work Order (canonical contract)
  category=Rental  passed=66  failed=51  duration=153ms
  66/117 passed — first failure: 4. create view has section: 'Work Order Header'
    ✓ 1. child table exists: rental_wo_line_items
    ✓ 1. child table exists: rental_wo_assets
    ✓ 1. child table exists: rental_wo_rate_slabs
    ✓ 1. child table exists: rental_wo_billing_rules
    ✓ 1. child table exists: rental_wo_responsibilities
    ✓ 1. child table exists: rental_wo_penalties
    ✓ 1. child table exists: rental_wo_incentives
    ✓ 1. child table exists: rental_wo_operations
    ✓ 1. child table exists: rental_wo_compliance
    ✓ 1. child table exists: rental_wo_mobilization
    ✓ 1. child table exists: rental_wo_workflow
    ✓ 1. child table exists: rental_wo_documents
    ✓ 1. child table exists: rental_wo_payment_milestones
    ✓ 2. rental_work_orders.revision_no exists
    ✓ 2. rental_work_orders.amendment_no exists
    ✓ 2. rental_work_orders.issue_date exists
    ✓ 2. rental_work_orders.effective_date exists
    ✓ 2. rental_work_orders.planned_end_date exists
    ✓ 2. rental_work_orders.actual_end_date exists
    ✓ 2. rental_work_orders.contract_reference exists
    ✓ 2. rental_work_orders.agreement_reference exists
    ✓ 2. rental_work_orders.tender_reference exists
    ✓ 2. rental_work_orders.zone exists
    ✓ 2. rental_work_orders.structure exists
    ✓ 2. rental_work_orders.chainage exists
    ✓ 2. rental_work_orders.activity exists
    ✓ 2. rental_work_orders.work_package exists
    ✓ 2. rental_work_orders.reporting_engineer_id exists
    ✓ 2. rental_work_orders.site_manager_id exists
    ✓ 2. rental_work_orders.department exists
    ✓ 2. rental_work_orders.cost_center exists
    ✓ 2. rental_work_orders.business_unit exists
    ✓ 2. rental_work_orders.shift_pattern exists
    ✓ 2. rental_work_orders.working_days exists
    ✓ 2. rental_work_orders.primary_measurement_method exists
    ✓ 2. rental_work_orders.hybrid_formula_json exists
    ✓ 2. rental_work_orders.min_guarantee_hours exists
    ✓ 2. rental_work_orders.included_hours exists
    ✓ 2. rental_work_orders.max_billable_days exists
    ✓ 2. rental_work_orders.advance_pct exists
    ✓ 2. rental_work_orders.retention_pct exists
    ✓ 2. rental_work_orders.retention_release_days exists
    ✓ 2. rental_work_orders.payment_days exists
    ✓ 2. rental_work_orders.tds_category exists
    ✓ 2. rental_work_orders.milestone_billing exists
    ✓ 2. rental_work_orders.mobilization_status exists
    ✓ 2. rental_work_orders.demobilization_status exists
    ✓ 2. rental_work_orders.current_stage exists
    ✓ 2. rental_work_orders.next_stage exists
    ✓ 2. rental_work_orders.vendor_accepted_at exists
    ✓ 3. rental_wo_line_items has >= 3 rows for WO 151 (got 3)
    ✓ 3. rental_wo_rate_slabs has >= 5 rows for WO 151 (got 7)
    ✓ 3. rental_wo_billing_rules has >= 2 rows for WO 151 (got 3)
    ✓ 3. rental_wo_responsibilities has >= 5 rows for WO 151 (got 9)
    ✓ 3. rental_wo_penalties has >= 3 rows for WO 151 (got 5)
    ✓ 3. rental_wo_incentives has >= 2 rows for WO 151 (got 4)
    ✓ 3. rental_wo_operations has >= 2 rows for WO 151 (got 4)
    ✓ 3. rental_wo_compliance has >= 3 rows for WO 151 (got 5)
    ✓ 3. rental_wo_mobilization has >= 5 rows for WO 151 (got 12)
    ✓ 3. rental_wo_workflow has >= 11 rows for WO 151 (got 11)
    ✓ 3. rental_wo_documents has >= 3 rows for WO 151 (got 5)
    ✓ 3. rental_wo_payment_milestones has >= 3 rows for WO 151 (got 3)
    ✗ 4. create view has section: 'Work Order Header'
    ✗ 4. create view has section: 'Vendor Information'
    ✗ 4. create view has section: 'Project Deployment'
    ✗ 4. create view has section: 'Line Items'
    ✗ 4. create view has section: 'Operation Assignment'
    ✗ 4. create view has section: 'Measurement Rules'
    ✗ 4. create view has section: 'Commercial Rate Slabs'
    ✗ 4. create view has section: 'Dynamic Billing Rules'
    ✗ 4. create view has section: 'Responsibilities'
    ✗ 4. create view has section: 'Penalty Matrix'
    ✗ 4. create view has section: 'Incentive Matrix'
    ✗ 4. create view has section: 'Payment Terms'
    ✗ 4. create view has section: 'Workflow'
    ✗ 5. show view has section: 'Work Order Header'
    ✗ 5. show view has section: 'Vendor Information'
    ✗ 5. show view has section: 'Project Deployment'
    ✗ 5. show view has section: 'Line Items'
    ✗ 5. show view has section: 'Operation Assignment'
    ✗ 5. show view has section: 'Measurement Rules'
    ✗ 5. show view has section: 'Commercial Rate Slabs'
    ✗ 5. show view has section: 'Dynamic Billing Rules'
    ✗ 5. show view has section: 'Responsibilities'
    ✗ 5. show view has section: 'Penalty Matrix'
    ✗ 5. show view has section: 'Incentive Matrix'
    ✗ 5. show view has section: 'Payment Terms'
    ✗ 5. show view has section: 'Workflow'
    ✗ 5. show view has Lifecycle visualisation
    ✗ 5. show view has TOTAL WO VALUE KPI tile
    ✗ 5. show view renders Excavator 20T line item
    ✗ 5. show view renders Tipper 16 cum line item
    ✗ 5. show view renders Mobilization Advance milestone
    ✗ 5. show view renders document attachments
    ✗ 5. show view renders compliance issuer (ICICI Lombard)
    ✗ 5. show view renders penalty data (late mobilization)
    ✗ 5. show view renders incentive data (early completion)
    ✗ 5. show view renders responsibility: Fuel
    ✗ 6a. /rental/vendor-assets/search returns { items, total, ... } JSON
    ✗ 6b. create view has #vendorMachineModal popup
    ✗ 6b. create view has #btnBulkSearch trigger button
    ✗ 6b. create view shows 'Search Vendor Machines' button label
    ✗ 6b. create view has #modalMachinesBody table body
    ✗ 6b. create view has vendor filter in popup
    ✗ 6b. create view has max-age filter in popup
    ✗ 6b. create view has insurance compliance filter
    ✗ 6b. create view has fitness compliance filter
    ✗ 6b. create view has 'Stage Selected' button
    ✗ 6b. create view has 'Apply Staged Machines' button
    ✗ 6b. create view has 'Add Blank Row' button (fallback)
    ✗ 6b. create view posts hidden line_rental_asset_id[] field
    ✗ 6b. create view JS uses VM_SEARCH_URL constant
    ✓ 6c. controller reads line_rental_asset_id[] from POST
    ✓ 6c. controller inserts into rental_wo_assets table when popup-sourced
    ✓ 6d. /rental/vendor-assets/search route exists
    ✓ 6f. search by vendor_id works (vendor #1)
    ✗ 6g. status=available filter works

────────────────────────────────────────────────────────────
[PASS] rental.mr.shows_source_indent — Machine Request → Source Indent Synchronisation
  category=Rental  passed=24  failed=0  duration=8ms
  24 assertion(s) passed
    ✓ A. seed data — found at least one MR with source_indent_id set
    ✓ A. seed MR.source_indent_id is positive (id=59)
    ✓ B. getWithDetails returned a row
    ✓ B. joined field 'source_indent_no' is present in row (got ''IND-202607-0007'')
    ✓ B. joined field 'source_indent_status' is present in row (got ''approved'')
    ✓ B. joined field 'source_indent_priority' is present in row (got ''medium'')
    ✓ B. joined field 'source_indent_fulfillment' is present in row (got ''RENTAL'')
    ✓ B. joined field 'source_indent_required_by' is present in row (got ''2026-07-22'')
    ✓ B. source_indent_no is populated (got "IND-202607-0007")
    ✓ B. source_indent_fulfillment is one of OWN_FLEET|RENTAL|PURCHASE|UNRESOLVED (got "RENTAL")
    ✓ C. show.php exists
    ✓ C. show.php contains "Source Indent" header text
    ✓ C. show.php links use the active /indents/{id} route (not /procurement/indents/show/{id})
    ✓ C. show.php references $r[source_indent_no]
    ✓ C. show.php references $r[source_indent_fulfillment]
    ✓ D. index.php exists
    ✓ D. index.php has a "Source Indent" column header
    ✓ D. index.php link uses active /indents/{id} route
    ✓ D. index.php renders $r[source_indent_no]
    ✓ D. index.php handles legacy_unsourced MRs separately
    ✓ E. getList returned an array
    ✓ E. getList rows include source_indent_no field (got key present=yes)
    ✓ F. getWithDetails works for MR without source_indent_id (id=20)
    ✓ G. no legacy_unsourced=1 row found — scenario skipped

────────────────────────────────────────────────────────────
[PASS] rental.vendor_asset.unified_view — Vendor Asset View matches Asset Master
  category=Rental  passed=75  failed=0  duration=198ms
  75 assertion(s) passed
    ✓ A. seed data — at least one rental_vendor_assets row exists (id=1)
    ✓ B. vendor_assets/show.php exists
    ✓ B. view title includes "Vendor Machine" / "Vendor Asset"
    ✓ C. tab "Overview" (#vat-overview) is present
    ✓ C. tab "Technical" (#vat-technical) is present
    ✓ C. tab "Compliance" (#vat-compliance) is present
    ✓ C. tab "Deployment" (#vat-deployment) is present
    ✓ C. tab "Work Orders" (#vat-wo) is present
    ✓ C. tab "History" (#vat-history) is present
    ✓ C. tab "Operations" (#vat-operations) is present
    ✓ D. KPI card "Compliance Health" present
    ✓ D. KPI card "Active WO" present
    ✓ D. KPI card "Total WO History" present
    ✓ D. KPI card "Age / 5-Year Rule" present
    ✓ E. SVG health ring (matches asset detail.php header)
    ✓ F. Gradient header banner style matches asset detail.php
    ✓ G. Cross-link banner present (Unified or Catalogue-only)
    ✓ G. Cross-link banner uses base_url('assets/vat-operations) to navigate to master
    ✓ H. "Identity & Classification" section present (Overview)
    ✓ H. "Identification Numbers" section present (Overview)
    ✓ H. "Current State" section present (Overview)
    ✓ H. "Engine & Powertrain" section present (Technical)
    ✓ H. "Meters & Hours" section present (Technical)
    ✓ H. "Performance" section present (Technical)
    ✓ I. expiry-color helper used (matches detail.php behaviour)
    ✓ I. Insurance card present
    ✓ I. Fitness Certificate card present
    ✓ J. "Current Deployment" card present
    ✓ J. "Next PM" card present
    ✓ J. PM Schedules list rendered when available
    ✓ K. "Rental Work Order History" table preserved
    ✓ L. Timeline card mentions "Promoted to assets.id" event
    ✓ M. "Quick Status Update" form preserved for /update POST route
    ✓ M. Quick Status Update form posts to /update endpoint
    ✓ M2. Operations sub-section #ops-score is present
    ✓ M2. Operations sub-section title "Operational Score &amp; KPIs" is present
    ✓ M2. Operations sub-section #ops-items is present
    ✓ M2. Operations sub-section title "Asset Items &amp; Installed Components" is present
    ✓ M2. Operations sub-section #ops-compliance is present
    ✓ M2. Operations sub-section title "Complete Compliance Status" is present
    ✓ M2. Operations sub-section #ops-maintenance is present
    ✓ M2. Operations sub-section title "Maintenance Management" is present
    ✓ M2. Operations sub-section #ops-logbook is present
    ✓ M2. Operations sub-section title "Asset Operations — Daily Log Summary" is present
    ✓ M2. Operations sub-section #ops-analysis is present
    ✓ M2. Operations sub-section title "Asset Complete Analysis" is present
    ✓ M2. Operations sub-section #ops-ai is present
    ✓ M2. Operations sub-section title "AI Analysis &amp; Recommendations" is present
    ✓ M2. Operations Ops Score badge present (right side of section nav)
    ✓ M2. Operations YTD Cost Breakdown sub-section present
    ✓ M2. Operations Breakdown History sub-section present
    ✓ M2. Operations Preventive Maintenance Schedule sub-section present
    ✓ M2. Operations YTD Total card present
    ✓ M2. Operations shows friendly message when no linked asset
    ✓ N. RentalVendorAssetController.php exists
    ✓ N. controller passes linkedAsset to view
    ✓ N. controller queries assets.clone_source_vendor_asset_id for cross-link
    ✓ N. controller fetches linked-asset compliance docs
    ✓ N. controller fetches PM schedules
    ✓ N. controller fetches maintenance history
    ✓ N. controller fetches breakdowns (for Operations tab)
    ✓ N. controller fetches perfMetrics (for Operations tab)
    ✓ N. controller fetches installedItems (for Operations tab)
    ✓ N. controller fetches logbookEntries (for Operations tab)
    ✓ N. controller fetches costByType (for Operations tab)
    ✓ N. controller fetches aiInsights (for Operations tab)
    ✓ N. controller fetches aiRecommendations (for Operations tab)
    ✓ O. GET http://localhost/Rajpath%20civitas/public/rental/vendor-assets/1 returned HTTP 200 (165932 bytes)
    ✓ O. rendered HTML contains #vat-overview tab
    ✓ O. rendered HTML contains Compliance Health KPI card
    ✓ O. rendered HTML contains Engine & Powertrain section
    ✓ O. rendered HTML contains #vat-operations tab
    ✓ O. rendered HTML contains Operations sub-section #ops-score
    ✓ O. rendered HTML contains Operational Score heading
    ✓ P. seed data — promoted assets row exists for cross-link (assets.id=449, clone_source_vendor_asset_id=1)

────────────────────────────────────────────────────────────
[PASS] routing.asset.reservation — Asset Reservation (OWN_FLEET lock)
  category=Routing  passed=13  failed=0  duration=17ms
  13 assertion(s) passed
    ✓ Found an OWN_FLEET indent to test against (id=64)
    ✓ No pre-existing reservation for this indent (count=0)
    ✓ autoReserveForOwnFleet() returned array
    ✓ result has reserved key
    ✓ result has reason key
    ✓ reserved=True has asset_id=449
    ✓ reserved_until is in the future (2026-09-03 07:48:33)
    ✓ reservation_hours is 48 or 72 (got 72)
    ✓ assets.reserved_for_indent_id = 64 (got 64)
    ✓ assets.reserved_until is future-dated (2026-09-03 07:48:33)
    ✓ idempotent — second call returns same asset_id
    ✓ second call reason notes "already reserved"
    ✓ Cleanup succeeded — no leftover reservations on test indent

────────────────────────────────────────────────────────────
[PASS] routing.fulfillment.router — Fulfillment Router decisions
  category=Routing  passed=24  failed=0  duration=27ms
  24 assertion(s) passed
    ✓ [hire_type] recommendation is a valid constant (RENTAL)
    ✓ [hire_type] confidence is a float (0.95)
    ✓ [hire_type] reason_bullets non-empty (3)
    ✓ [hire_type] rent_options populated
    ✓ [hire_type] rent vendor_name is real (OEM Direct - Tata Motors)
    ✓ [hire_type] rent vendor_count_available returned (457)
    ✓ [asset_type] recommendation is a valid constant (PURCHASE)
    ✓ [asset_type] confidence is a float (0.75)
    ✓ [asset_type] reason_bullets non-empty (3)
    ✓ [asset_type] rent_options populated
    ✓ [asset_type] rent vendor_name is real (OEM Direct - Tata Motors)
    ✓ [asset_type] rent vendor_count_available returned (457)
    ✓ [material_type] recommendation is a valid constant (PURCHASE)
    ✓ [material_type] confidence is a float (0.75)
    ✓ [material_type] reason_bullets non-empty (3)
    ✓ [material_type] rent_options populated
    ✓ [material_type] rent vendor_name is real (OEM Direct - Tata Motors)
    ✓ [material_type] rent vendor_count_available returned (457)
    ✓ [consumable_type] recommendation is a valid constant (PURCHASE)
    ✓ [consumable_type] confidence is a float (0.75)
    ✓ [consumable_type] reason_bullets non-empty (3)
    ✓ [consumable_type] rent_options populated
    ✓ [consumable_type] rent vendor_name is real (OEM Direct - Tata Motors)
    ✓ [consumable_type] rent vendor_count_available returned (457)

────────────────────────────────────────────────────────────
[PASS] routing.smart-route.card-render — Smart Route card renders on Indent form
  category=Routing  passed=7  failed=0  duration=74ms
  7 assertion(s) passed
    ✓ Page returned 200 or 302, got 200
    ✓ HTML contains "Smart Route"
    ✓ HTML contains OWN column
    ✓ HTML contains RENTAL column
    ✓ HTML contains PURCHASE column
    ✓ Trust banner present
    ✓ fulfillment_source hidden input present

────────────────────────────────────────────────────────────
[FAIL] system.approval-inbox.reachable — Approval inbox + approve endpoint reachability
  category=System  passed=4  failed=4  duration=65ms
  4/8 passed — first failure: /procurement/approvals (auth) → 200/302 (got 400) ← REAL DEFECT
    ✗ /procurement/approvals (auth) → 200/302 (got 400) ← REAL DEFECT
    ✗ /approvals (auth) → 200/302 (got 400) ← REAL DEFECT
    ✗ /procurement/approvals/review/1 (auth) → 200/302 (got 400) ← REAL DEFECT
    ✗ /procurement/approvals/show/1 (auth) → 200/302 (got 400) ← REAL DEFECT
    ✓ /procurement/approvals (anonymous) is NOT 500 (got 302)
    ✓ /approvals (anonymous) is NOT 500 (got 302)
    ✓ /procurement/approvals/review/1 (anonymous) is NOT 500 (got 302)
    ✓ /procurement/approvals/show/1 (anonymous) is NOT 500 (got 302)

────────────────────────────────────────────────────────────
[PASS] system.db.core-tables — Database core tables exist and accessible
  category=Database  passed=13  failed=0  duration=61ms
  13 assertion(s) passed
    ✓ users exists (rows=21)
    ✓ indents exists (rows=15)
    ✓ purchase_requisitions exists (rows=5)
    ✓ assets exists (rows=449)
    ✓ machine_requests exists (rows=5)
    ✓ vendors exists (rows=44)
    ✓ sites exists (rows=3)
    ✓ projects exists (rows=1)
    ✓ decision_logs exists (rows=31)
    ✓ skill_runs exists (rows=761)
    ✓ admin@rajpath.local user present
    ✓ admin has password_hash set (hash prefix: $2y$10$)
    ✓ admin hash verifies against Admin@12345

────────────────────────────────────────────────────────────
[PASS] system.env.config-and-extensions — Environment config + extensions
  category=System  passed=13  failed=0  duration=0ms
  13 assertion(s) passed
    ✓ PHP version 8.1+ — actual: 8.5.5
    ✓ Extension 'curl' loaded
    ✓ Extension 'mbstring' loaded
    ✓ Extension 'mysqli' loaded
    ✓ Extension 'json' loaded
    ✓ Extension 'zip' loaded
    ✓ .env exists
    ✓ .env has DB hostname
    ✓ .env has app.baseURL
    ✓ PDO/MySQL connection established
    ✓ Connected DB: civitas
    ✓ writable/logs is writable
    ✓ writable/ is writable

────────────────────────────────────────────────────────────
[PASS] system.routes.core — Core routes reachable (no 404/500)
  category=System  passed=8  failed=0  duration=305ms
  8 assertion(s) passed
    ✓ login → 200
    ✓ dashboard → 200
    ✓ indents_index → 200
    ✓ indents_create → 200
    ✓ rental_mr_index → 200
    ✓ rental_mr_create → 200
    ✓ assets_index → 200
    ✓ smart_route → 200

────────────────────────────────────────────────────────────
[PASS] system.security.headers — Security headers presence (X-Frame, CSP, X-Content-Type)
  category=System  passed=4  failed=0  duration=48ms
  4 assertion(s) passed
    ✓ / has at least one hardening header
    ✓ /login has at least one hardening header
    ✓ all 2/total pages have hardening headers (got 2/2)
    ✓ POST /loginPost (empty body) is NOT 500 (got 404)

