@resolveio/server-lib 22.3.221 → 22.3.223
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ai/assistant-core-heuristics.d.ts +11 -0
- package/ai/assistant-core-heuristics.js +356 -0
- package/ai/assistant-core-heuristics.js.map +1 -0
- package/ai/resolveio-platform-intelligence-memory-corpus.d.ts +3 -0
- package/ai/resolveio-platform-intelligence-memory-corpus.js +214 -0
- package/ai/resolveio-platform-intelligence-memory-corpus.js.map +1 -0
- package/ai/resolveio-platform-intelligence-memory.d.ts +20 -0
- package/ai/resolveio-platform-intelligence-memory.js +341 -0
- package/ai/resolveio-platform-intelligence-memory.js.map +1 -0
- package/{src/ai/resolveio-platform-intelligence-types.ts → ai/resolveio-platform-intelligence-types.d.ts} +15 -20
- package/ai/resolveio-platform-intelligence-types.js +4 -0
- package/ai/resolveio-platform-intelligence-types.js.map +1 -0
- package/ai/resolveio-platform-intelligence.d.ts +6 -0
- package/ai/resolveio-platform-intelligence.js +463 -0
- package/ai/resolveio-platform-intelligence.js.map +1 -0
- package/client-server-app.d.ts +1 -0
- package/client-server-app.js +68 -0
- package/client-server-app.js.map +1 -0
- package/collections/ai-run.collection.d.ts +3 -0
- package/collections/ai-run.collection.js +170 -0
- package/collections/ai-run.collection.js.map +1 -0
- package/collections/ai-terminal-conversation.collection.d.ts +2 -0
- package/collections/ai-terminal-conversation.collection.js +140 -0
- package/collections/ai-terminal-conversation.collection.js.map +1 -0
- package/collections/ai-terminal-issue-report.collection.d.ts +2 -0
- package/collections/ai-terminal-issue-report.collection.js +148 -0
- package/collections/ai-terminal-issue-report.collection.js.map +1 -0
- package/collections/ai-terminal-message.collection.d.ts +2 -0
- package/collections/ai-terminal-message.collection.js +121 -0
- package/collections/ai-terminal-message.collection.js.map +1 -0
- package/collections/app-setting.collection.d.ts +3 -0
- package/collections/app-setting.collection.js +103 -0
- package/collections/app-setting.collection.js.map +1 -0
- package/collections/app-status.collection.d.ts +3 -0
- package/collections/app-status.collection.js +57 -0
- package/collections/app-status.collection.js.map +1 -0
- package/collections/communication-metric.collection.d.ts +2 -0
- package/collections/communication-metric.collection.js +133 -0
- package/collections/communication-metric.collection.js.map +1 -0
- package/collections/counter.collection.d.ts +3 -0
- package/collections/counter.collection.js +56 -0
- package/collections/counter.collection.js.map +1 -0
- package/collections/cron-job-history.collection.d.ts +3 -0
- package/collections/cron-job-history.collection.js +137 -0
- package/collections/cron-job-history.collection.js.map +1 -0
- package/collections/cron-job.collection.d.ts +3 -0
- package/collections/cron-job.collection.js +92 -0
- package/collections/cron-job.collection.js.map +1 -0
- package/collections/customer-notification.collection.d.ts +3 -0
- package/collections/customer-notification.collection.js +130 -0
- package/collections/customer-notification.collection.js.map +1 -0
- package/collections/customer-portal-password.collection.d.ts +3 -0
- package/collections/customer-portal-password.collection.js +75 -0
- package/collections/customer-portal-password.collection.js.map +1 -0
- package/collections/email-history.collection.d.ts +3 -0
- package/collections/email-history.collection.js +134 -0
- package/collections/email-history.collection.js.map +1 -0
- package/collections/email-verified.collection.d.ts +3 -0
- package/collections/email-verified.collection.js +62 -0
- package/collections/email-verified.collection.js.map +1 -0
- package/collections/file.collection.d.ts +3 -0
- package/collections/file.collection.js +74 -0
- package/collections/file.collection.js.map +1 -0
- package/collections/flag-update.collection.d.ts +3 -0
- package/collections/flag-update.collection.js +57 -0
- package/collections/flag-update.collection.js.map +1 -0
- package/collections/flag.collection.d.ts +3 -0
- package/collections/flag.collection.js +57 -0
- package/collections/flag.collection.js.map +1 -0
- package/collections/log-method-latency.collection.d.ts +3 -0
- package/collections/log-method-latency.collection.js +77 -0
- package/collections/log-method-latency.collection.js.map +1 -0
- package/collections/log-subscription.collection.d.ts +3 -0
- package/collections/log-subscription.collection.js +80 -0
- package/collections/log-subscription.collection.js.map +1 -0
- package/collections/log.collection.d.ts +3 -0
- package/collections/log.collection.js +93 -0
- package/collections/log.collection.js.map +1 -0
- package/collections/logged-in-users.collection.d.ts +3 -0
- package/collections/logged-in-users.collection.js +67 -0
- package/collections/logged-in-users.collection.js.map +1 -0
- package/collections/monitor-cpu.collection.d.ts +3 -0
- package/collections/monitor-cpu.collection.js +65 -0
- package/collections/monitor-cpu.collection.js.map +1 -0
- package/collections/monitor-function.collection.d.ts +3 -0
- package/collections/monitor-function.collection.js +74 -0
- package/collections/monitor-function.collection.js.map +1 -0
- package/collections/monitor-memory.collection.d.ts +3 -0
- package/collections/monitor-memory.collection.js +77 -0
- package/collections/monitor-memory.collection.js.map +1 -0
- package/collections/monitor-mongo.collection.d.ts +3 -0
- package/collections/monitor-mongo.collection.js +71 -0
- package/collections/monitor-mongo.collection.js.map +1 -0
- package/collections/notification.collection.d.ts +3 -0
- package/collections/notification.collection.js +57 -0
- package/collections/notification.collection.js.map +1 -0
- package/collections/openai-usage-ledger.collection.d.ts +2 -0
- package/collections/openai-usage-ledger.collection.js +188 -0
- package/collections/openai-usage-ledger.collection.js.map +1 -0
- package/collections/report-builder-dashboard-builder.collection.d.ts +3 -0
- package/collections/report-builder-dashboard-builder.collection.js +109 -0
- package/collections/report-builder-dashboard-builder.collection.js.map +1 -0
- package/collections/report-builder-library.collection.d.ts +3 -0
- package/collections/report-builder-library.collection.js +87 -0
- package/collections/report-builder-library.collection.js.map +1 -0
- package/collections/report-builder-report.collection.d.ts +4 -0
- package/collections/report-builder-report.collection.js +184 -0
- package/collections/report-builder-report.collection.js.map +1 -0
- package/collections/user-group.collection.d.ts +4 -0
- package/collections/user-group.collection.js +89 -0
- package/collections/user-group.collection.js.map +1 -0
- package/collections/user-guide.collection.d.ts +3 -0
- package/collections/user-guide.collection.js +57 -0
- package/collections/user-guide.collection.js.map +1 -0
- package/collections/user.collection.d.ts +4 -0
- package/collections/user.collection.js +180 -0
- package/collections/user.collection.js.map +1 -0
- package/cron/cron.d.ts +14 -0
- package/cron/cron.js +216 -0
- package/cron/cron.js.map +1 -0
- package/fixtures/cron-jobs.d.ts +1 -0
- package/fixtures/cron-jobs.js +150 -0
- package/fixtures/cron-jobs.js.map +1 -0
- package/fixtures/init.d.ts +1 -0
- package/fixtures/init.js +91 -0
- package/fixtures/init.js.map +1 -0
- package/http/auth.d.ts +2 -0
- package/http/auth.js +951 -0
- package/http/auth.js.map +1 -0
- package/http/health.d.ts +1 -0
- package/http/health.js +11 -0
- package/http/health.js.map +1 -0
- package/http/home.d.ts +1 -0
- package/http/home.js +134 -0
- package/http/home.js.map +1 -0
- package/http/slow-query-publication.d.ts +2 -0
- package/http/slow-query-publication.js +99 -0
- package/http/slow-query-publication.js.map +1 -0
- package/index.d.ts +1 -0
- package/index.js +19 -0
- package/index.js.map +1 -0
- package/managers/ai-assistant-codex-manager.manager.d.ts +67 -0
- package/managers/ai-assistant-codex-manager.manager.js +1113 -0
- package/managers/ai-assistant-codex-manager.manager.js.map +1 -0
- package/managers/ai-run-evidence.manager.d.ts +36 -0
- package/managers/ai-run-evidence.manager.js +377 -0
- package/managers/ai-run-evidence.manager.js.map +1 -0
- package/managers/communication-metric.manager.d.ts +16 -0
- package/managers/communication-metric.manager.js +134 -0
- package/managers/communication-metric.manager.js.map +1 -0
- package/managers/cron.manager.d.ts +20 -0
- package/managers/cron.manager.js +534 -0
- package/managers/cron.manager.js.map +1 -0
- package/managers/customer-notification-content.manager.d.ts +55 -0
- package/managers/customer-notification-content.manager.js +158 -0
- package/managers/customer-notification-content.manager.js.map +1 -0
- package/managers/diagnostic-manager-bootstrap.d.ts +9 -0
- package/managers/diagnostic-manager-bootstrap.js +260 -0
- package/managers/diagnostic-manager-bootstrap.js.map +1 -0
- package/managers/error-auto-fix.manager.d.ts +149 -0
- package/managers/error-auto-fix.manager.js +3064 -0
- package/managers/error-auto-fix.manager.js.map +1 -0
- package/managers/local-log.manager.d.ts +18 -0
- package/managers/local-log.manager.js +88 -0
- package/managers/local-log.manager.js.map +1 -0
- package/managers/method.manager.d.ts +84 -0
- package/managers/method.manager.js +1964 -0
- package/managers/method.manager.js.map +1 -0
- package/managers/mongo.manager.d.ts +224 -0
- package/managers/mongo.manager.js +5000 -0
- package/managers/mongo.manager.js.map +1 -0
- package/managers/monitor.manager.d.ts +70 -0
- package/managers/monitor.manager.js +550 -0
- package/managers/monitor.manager.js.map +1 -0
- package/managers/openai-usage-ledger.manager.d.ts +30 -0
- package/managers/openai-usage-ledger.manager.js +142 -0
- package/managers/openai-usage-ledger.manager.js.map +1 -0
- package/managers/slow-query-verifier.manager.d.ts +144 -0
- package/managers/slow-query-verifier.manager.js +3857 -0
- package/managers/slow-query-verifier.manager.js.map +1 -0
- package/managers/slow-query.manager.d.ts +28 -0
- package/managers/slow-query.manager.js +468 -0
- package/managers/slow-query.manager.js.map +1 -0
- package/managers/subscription.manager.d.ts +169 -0
- package/managers/subscription.manager.js +3434 -0
- package/managers/subscription.manager.js.map +1 -0
- package/managers/websocket.manager.d.ts +73 -0
- package/managers/websocket.manager.js +673 -0
- package/managers/websocket.manager.js.map +1 -0
- package/managers/worker-dispatcher.manager.d.ts +120 -0
- package/managers/worker-dispatcher.manager.js +1266 -0
- package/managers/worker-dispatcher.manager.js.map +1 -0
- package/managers/worker-server.manager.d.ts +35 -0
- package/managers/worker-server.manager.js +582 -0
- package/managers/worker-server.manager.js.map +1 -0
- package/methods/accounts.d.ts +2 -0
- package/methods/accounts.js +624 -0
- package/methods/accounts.js.map +1 -0
- package/methods/ai-terminal.d.ts +458 -0
- package/methods/ai-terminal.js +27991 -0
- package/methods/ai-terminal.js.map +1 -0
- package/methods/app-settings.d.ts +2 -0
- package/methods/app-settings.js +169 -0
- package/methods/app-settings.js.map +1 -0
- package/methods/aws.d.ts +2 -0
- package/methods/aws.js +877 -0
- package/methods/aws.js.map +1 -0
- package/methods/collections.d.ts +2 -0
- package/methods/collections.js +719 -0
- package/methods/collections.js.map +1 -0
- package/methods/counters.d.ts +2 -0
- package/methods/counters.js +113 -0
- package/methods/counters.js.map +1 -0
- package/methods/cron-jobs.d.ts +2 -0
- package/methods/cron-jobs.js +2475 -0
- package/methods/cron-jobs.js.map +1 -0
- package/methods/customer-notifications.d.ts +2 -0
- package/methods/customer-notifications.js +528 -0
- package/methods/customer-notifications.js.map +1 -0
- package/methods/diagnostics.d.ts +2 -0
- package/methods/diagnostics.js +703 -0
- package/methods/diagnostics.js.map +1 -0
- package/methods/flag-updates.d.ts +2 -0
- package/methods/flag-updates.js +8 -0
- package/methods/flag-updates.js.map +1 -0
- package/methods/flags.d.ts +2 -0
- package/methods/flags.js +8 -0
- package/methods/flags.js.map +1 -0
- package/methods/logs.d.ts +2 -0
- package/methods/logs.js +751 -0
- package/methods/logs.js.map +1 -0
- package/methods/mongo-explorer.d.ts +2 -0
- package/methods/mongo-explorer.js +1808 -0
- package/methods/mongo-explorer.js.map +1 -0
- package/methods/monitor.d.ts +2 -0
- package/methods/monitor.js +543 -0
- package/methods/monitor.js.map +1 -0
- package/methods/pdf.d.ts +2 -0
- package/methods/pdf.js +1216 -0
- package/methods/pdf.js.map +1 -0
- package/methods/publications.d.ts +1 -0
- package/methods/publications.js +183 -0
- package/methods/publications.js.map +1 -0
- package/methods/report-builder.d.ts +2 -0
- package/methods/report-builder.js +3094 -0
- package/methods/report-builder.js.map +1 -0
- package/methods/support.d.ts +2 -0
- package/methods/support.js +430 -0
- package/methods/support.js.map +1 -0
- package/models/ai-run.model.d.ts +19 -0
- package/models/ai-run.model.js +4 -0
- package/models/ai-run.model.js.map +1 -0
- package/models/ai-terminal-conversation.model.d.ts +17 -0
- package/models/ai-terminal-conversation.model.js +4 -0
- package/models/ai-terminal-conversation.model.js.map +1 -0
- package/models/ai-terminal-issue-report.model.d.ts +19 -0
- package/models/ai-terminal-issue-report.model.js +4 -0
- package/models/ai-terminal-issue-report.model.js.map +1 -0
- package/models/ai-terminal-message.model.d.ts +22 -0
- package/models/ai-terminal-message.model.js +4 -0
- package/models/ai-terminal-message.model.js.map +1 -0
- package/models/app-setting.model.d.ts +16 -0
- package/models/app-setting.model.js +4 -0
- package/models/app-setting.model.js.map +1 -0
- package/{src/models/app-status.model.ts → models/app-status.model.d.ts} +2 -3
- package/models/app-status.model.js +4 -0
- package/models/app-status.model.js.map +1 -0
- package/{src/models/billing-logged-in-users.model.ts → models/billing-logged-in-users.model.d.ts} +4 -5
- package/models/billing-logged-in-users.model.js +4 -0
- package/models/billing-logged-in-users.model.js.map +1 -0
- package/models/collection-document.model.d.ts +21 -0
- package/models/collection-document.model.js +4 -0
- package/models/collection-document.model.js.map +1 -0
- package/models/communication-metric.model.d.ts +20 -0
- package/models/communication-metric.model.js +4 -0
- package/models/communication-metric.model.js.map +1 -0
- package/{src/models/counter.model.ts → models/counter.model.d.ts} +3 -4
- package/models/counter.model.js +4 -0
- package/models/counter.model.js.map +1 -0
- package/models/cron-job-history.model.d.ts +15 -0
- package/models/cron-job-history.model.js +4 -0
- package/models/cron-job-history.model.js.map +1 -0
- package/models/cron-job.model.d.ts +14 -0
- package/models/cron-job.model.js +4 -0
- package/models/cron-job.model.js.map +1 -0
- package/models/customer-notification.model.d.ts +26 -0
- package/models/customer-notification.model.js +4 -0
- package/models/customer-notification.model.js.map +1 -0
- package/models/customer-portal-password.model.d.ts +11 -0
- package/models/customer-portal-password.model.js +4 -0
- package/models/customer-portal-password.model.js.map +1 -0
- package/models/dialog.model.d.ts +23 -0
- package/models/dialog.model.js +4 -0
- package/models/dialog.model.js.map +1 -0
- package/models/email-history.model.d.ts +32 -0
- package/{src/models/email-history.model.ts → models/email-history.model.js} +4 -36
- package/models/email-history.model.js.map +1 -0
- package/{src/models/email-verified.model.ts → models/email-verified.model.d.ts} +5 -6
- package/models/email-verified.model.js +4 -0
- package/models/email-verified.model.js.map +1 -0
- package/{src/models/file.model.ts → models/file.model.d.ts} +7 -8
- package/models/file.model.js +4 -0
- package/models/file.model.js.map +1 -0
- package/{src/models/flag-update.model.ts → models/flag-update.model.d.ts} +3 -4
- package/models/flag-update.model.js +4 -0
- package/models/flag-update.model.js.map +1 -0
- package/{src/models/flag.model.ts → models/flag.model.d.ts} +3 -4
- package/models/flag.model.js +4 -0
- package/models/flag.model.js.map +1 -0
- package/models/log-method-latency.model.d.ts +10 -0
- package/models/log-method-latency.model.js +4 -0
- package/models/log-method-latency.model.js.map +1 -0
- package/{src/models/log-subscription.model.ts → models/log-subscription.model.d.ts} +9 -11
- package/models/log-subscription.model.js +4 -0
- package/models/log-subscription.model.js.map +1 -0
- package/models/log.model.d.ts +17 -0
- package/models/log.model.js +4 -0
- package/models/log.model.js.map +1 -0
- package/{src/models/logged-in-users.model.ts → models/logged-in-users.model.d.ts} +5 -6
- package/models/logged-in-users.model.js +4 -0
- package/models/logged-in-users.model.js.map +1 -0
- package/{src/models/method-response.model.ts → models/method-response.model.d.ts} +6 -7
- package/models/method-response.model.js +4 -0
- package/models/method-response.model.js.map +1 -0
- package/models/method.model.d.ts +26 -0
- package/models/method.model.js +4 -0
- package/models/method.model.js.map +1 -0
- package/{src/models/monitor-cpu.model.ts → models/monitor-cpu.model.d.ts} +7 -9
- package/models/monitor-cpu.model.js +4 -0
- package/models/monitor-cpu.model.js.map +1 -0
- package/models/monitor-function.model.d.ts +14 -0
- package/models/monitor-function.model.js +4 -0
- package/models/monitor-function.model.js.map +1 -0
- package/models/monitor-memory.model.d.ts +15 -0
- package/models/monitor-memory.model.js +4 -0
- package/models/monitor-memory.model.js.map +1 -0
- package/models/monitor-mongo.model.d.ts +13 -0
- package/models/monitor-mongo.model.js +4 -0
- package/models/monitor-mongo.model.js.map +1 -0
- package/{src/models/notification.model.ts → models/notification.model.d.ts} +4 -6
- package/models/notification.model.js +4 -0
- package/models/notification.model.js.map +1 -0
- package/models/openai-usage-ledger.model.d.ts +30 -0
- package/models/openai-usage-ledger.model.js +4 -0
- package/models/openai-usage-ledger.model.js.map +1 -0
- package/models/pagination.model.d.ts +11 -0
- package/models/pagination.model.js +28 -0
- package/models/pagination.model.js.map +1 -0
- package/models/permission.model.d.ts +12 -0
- package/models/permission.model.js +4 -0
- package/models/permission.model.js.map +1 -0
- package/models/report-builder-dashboard-builder.model.d.ts +25 -0
- package/models/report-builder-dashboard-builder.model.js +4 -0
- package/models/report-builder-dashboard-builder.model.js.map +1 -0
- package/models/report-builder-library.model.d.ts +17 -0
- package/models/report-builder-library.model.js +4 -0
- package/models/report-builder-library.model.js.map +1 -0
- package/models/report-builder-report.model.d.ts +121 -0
- package/models/report-builder-report.model.js +4 -0
- package/models/report-builder-report.model.js.map +1 -0
- package/models/report-builder.model.d.ts +61 -0
- package/models/report-builder.model.js +4 -0
- package/models/report-builder.model.js.map +1 -0
- package/models/select-data-label.model.d.ts +9 -0
- package/models/select-data-label.model.js +4 -0
- package/models/select-data-label.model.js.map +1 -0
- package/models/server-message.model.d.ts +32 -0
- package/models/server-message.model.js +4 -0
- package/models/server-message.model.js.map +1 -0
- package/models/slow-query-report.model.d.ts +23 -0
- package/models/slow-query-report.model.js +4 -0
- package/models/slow-query-report.model.js.map +1 -0
- package/models/subscription.model.d.ts +31 -0
- package/models/subscription.model.js +4 -0
- package/models/subscription.model.js.map +1 -0
- package/models/support-ticket.model.d.ts +87 -0
- package/models/support-ticket.model.js +4 -0
- package/models/support-ticket.model.js.map +1 -0
- package/models/user-group.model.d.ts +20 -0
- package/models/user-group.model.js +4 -0
- package/models/user-group.model.js.map +1 -0
- package/{src/models/user-guide.model.ts → models/user-guide.model.d.ts} +4 -5
- package/models/user-guide.model.js +4 -0
- package/models/user-guide.model.js.map +1 -0
- package/models/user.model.d.ts +84 -0
- package/models/user.model.js +4 -0
- package/models/user.model.js.map +1 -0
- package/package.json +1 -1
- package/private/images/ResolveIO.png +0 -0
- package/public_api.js +127 -0
- package/public_api.js.map +1 -0
- package/publications/ai-terminal.d.ts +1 -0
- package/publications/ai-terminal.js +122 -0
- package/publications/ai-terminal.js.map +1 -0
- package/publications/app-settings.d.ts +2 -0
- package/publications/app-settings.js +28 -0
- package/publications/app-settings.js.map +1 -0
- package/publications/app-status.d.ts +2 -0
- package/publications/app-status.js +16 -0
- package/publications/app-status.js.map +1 -0
- package/publications/cron-jobs.d.ts +2 -0
- package/publications/cron-jobs.js +88 -0
- package/publications/cron-jobs.js.map +1 -0
- package/publications/customer-notifications.d.ts +2 -0
- package/publications/customer-notifications.js +161 -0
- package/publications/customer-notifications.js.map +1 -0
- package/publications/files.d.ts +2 -0
- package/publications/files.js +36 -0
- package/publications/files.js.map +1 -0
- package/publications/flags-update.d.ts +2 -0
- package/publications/flags-update.js +22 -0
- package/publications/flags-update.js.map +1 -0
- package/publications/flags.d.ts +2 -0
- package/publications/flags.js +22 -0
- package/publications/flags.js.map +1 -0
- package/publications/logs.d.ts +2 -0
- package/publications/logs.js +164 -0
- package/publications/logs.js.map +1 -0
- package/publications/notifications.d.ts +2 -0
- package/publications/notifications.js +16 -0
- package/publications/notifications.js.map +1 -0
- package/publications/report-builder-dashboard-builders.d.ts +2 -0
- package/publications/report-builder-dashboard-builders.js +42 -0
- package/publications/report-builder-dashboard-builders.js.map +1 -0
- package/publications/report-builder-libraries.d.ts +2 -0
- package/publications/report-builder-libraries.js +90 -0
- package/publications/report-builder-libraries.js.map +1 -0
- package/publications/report-builder-reports.d.ts +2 -0
- package/publications/report-builder-reports.js +50 -0
- package/publications/report-builder-reports.js.map +1 -0
- package/publications/super-admin.d.ts +2 -0
- package/publications/super-admin.js +16 -0
- package/publications/super-admin.js.map +1 -0
- package/publications/user-groups.d.ts +1 -0
- package/publications/user-groups.js +16 -0
- package/publications/user-groups.js.map +1 -0
- package/publications/user-guides.d.ts +1 -0
- package/publications/user-guides.js +16 -0
- package/publications/user-guides.js.map +1 -0
- package/resolveio-server-app.d.ts +70 -0
- package/resolveio-server-app.js +801 -0
- package/resolveio-server-app.js.map +1 -0
- package/server-app.d.ts +228 -0
- package/server-app.js +3566 -0
- package/server-app.js.map +1 -0
- package/services/codex-client.d.ts +128 -0
- package/services/codex-client.js +1629 -0
- package/services/codex-client.js.map +1 -0
- package/services/openai-client.d.ts +46 -0
- package/services/openai-client.js +318 -0
- package/services/openai-client.js.map +1 -0
- package/types/error-report.d.ts +25 -0
- package/types/error-report.js +4 -0
- package/types/error-report.js.map +1 -0
- package/types/slow-query-report.d.ts +27 -0
- package/types/slow-query-report.js +6 -0
- package/types/slow-query-report.js.map +1 -0
- package/util/ai-qa-policy.d.ts +124 -0
- package/util/ai-qa-policy.js +736 -0
- package/util/ai-qa-policy.js.map +1 -0
- package/util/ai-run-evidence-adapters.d.ts +109 -0
- package/util/ai-run-evidence-adapters.js +7234 -0
- package/util/ai-run-evidence-adapters.js.map +1 -0
- package/util/ai-run-evidence-dashboard.d.ts +88 -0
- package/util/ai-run-evidence-dashboard.js +343 -0
- package/util/ai-run-evidence-dashboard.js.map +1 -0
- package/util/ai-run-evidence-eval.d.ts +86 -0
- package/util/ai-run-evidence-eval.js +1018 -0
- package/util/ai-run-evidence-eval.js.map +1 -0
- package/util/ai-run-evidence.d.ts +244 -0
- package/util/ai-run-evidence.js +1096 -0
- package/util/ai-run-evidence.js.map +1 -0
- package/util/ai-runner-artifacts.d.ts +82 -0
- package/util/ai-runner-artifacts.js +713 -0
- package/util/ai-runner-artifacts.js.map +1 -0
- package/util/ai-runner-manager-autopilot.d.ts +210 -0
- package/util/ai-runner-manager-autopilot.js +642 -0
- package/util/ai-runner-manager-autopilot.js.map +1 -0
- package/util/ai-runner-manager-policy.d.ts +807 -0
- package/util/ai-runner-manager-policy.js +3501 -0
- package/util/ai-runner-manager-policy.js.map +1 -0
- package/util/ai-runner-qa-auth.d.ts +5 -0
- package/util/ai-runner-qa-auth.js +839 -0
- package/util/ai-runner-qa-auth.js.map +1 -0
- package/util/ai-runner-qa-tools.d.ts +26 -0
- package/util/ai-runner-qa-tools.js +3520 -0
- package/util/ai-runner-qa-tools.js.map +1 -0
- package/util/aicoder-runner-v6.d.ts +426 -0
- package/util/aicoder-runner-v6.js +2464 -0
- package/util/aicoder-runner-v6.js.map +1 -0
- package/util/common.d.ts +31 -0
- package/util/common.js +683 -0
- package/util/common.js.map +1 -0
- package/util/customer-portal-password.d.ts +13 -0
- package/util/customer-portal-password.js +209 -0
- package/util/customer-portal-password.js.map +1 -0
- package/util/error-reporter.d.ts +52 -0
- package/util/error-reporter.js +326 -0
- package/util/error-reporter.js.map +1 -0
- package/util/error-tracking.d.ts +13 -0
- package/util/error-tracking.js +120 -0
- package/util/error-tracking.js.map +1 -0
- package/util/openai-usage-cost.d.ts +6 -0
- package/util/openai-usage-cost.js +103 -0
- package/util/openai-usage-cost.js.map +1 -0
- package/util/report-builder-unwinds.d.ts +15 -0
- package/util/report-builder-unwinds.js +156 -0
- package/util/report-builder-unwinds.js.map +1 -0
- package/util/runner-process-janitor.d.ts +27 -0
- package/util/runner-process-janitor.js +208 -0
- package/util/runner-process-janitor.js.map +1 -0
- package/util/schema-report-builder.d.ts +6 -0
- package/util/schema-report-builder.js +481 -0
- package/util/schema-report-builder.js.map +1 -0
- package/util/slow-query-reporter.d.ts +28 -0
- package/util/slow-query-reporter.js +226 -0
- package/util/slow-query-reporter.js.map +1 -0
- package/util/subscription-dependency-context.d.ts +34 -0
- package/util/subscription-dependency-context.js +1283 -0
- package/util/subscription-dependency-context.js.map +1 -0
- package/util/support-runner-v5.d.ts +1426 -0
- package/util/support-runner-v5.js +7647 -0
- package/util/support-runner-v5.js.map +1 -0
- package/util/tokenizer.d.ts +5 -0
- package/util/tokenizer.js +41 -0
- package/util/tokenizer.js.map +1 -0
- package/workers/codex-runner.worker.d.ts +1 -0
- package/workers/codex-runner.worker.js +192 -0
- package/workers/codex-runner.worker.js.map +1 -0
- package/.nodemon.json +0 -5
- package/.vscode/settings.json +0 -21
- package/AGENTS.md +0 -195
- package/README.md +0 -22
- package/build_package.sh +0 -5
- package/compileDTS.pl +0 -64
- package/docs/ai-assistant-nightly-eval.md +0 -65
- package/docs/ai-assistant-preflight-checklist.md +0 -23
- package/docs/ai-assistant-report-builder-bridge-playbook.md +0 -115
- package/eslint-plugin-custom/index.js +0 -7
- package/eslint-plugin-custom/rules/no-filter-zero-index.js +0 -44
- package/eslint.config.js +0 -103
- package/gulpfile.js +0 -216
- package/methodAndPublicationListGenerator.py +0 -375
- package/mongodbensurers.js +0 -2
- package/mongostop.js +0 -3
- package/scripts/cleanup-bypassed-callmethod-logs.js +0 -616
- package/settings.development.json +0 -25
- package/settings.development.redacted.json +0 -25
- package/src/.env +0 -12
- package/src/ai/assistant-core-heuristics.ts +0 -379
- package/src/ai/resolveio-platform-intelligence-memory-corpus.ts +0 -185
- package/src/ai/resolveio-platform-intelligence-memory.ts +0 -325
- package/src/ai/resolveio-platform-intelligence.ts +0 -462
- package/src/client-server-app.ts +0 -12
- package/src/collections/ai-run.collection.ts +0 -117
- package/src/collections/ai-terminal-conversation.collection.ts +0 -91
- package/src/collections/ai-terminal-issue-report.collection.ts +0 -99
- package/src/collections/ai-terminal-message.collection.ts +0 -77
- package/src/collections/app-setting.collection.ts +0 -104
- package/src/collections/app-status.collection.ts +0 -58
- package/src/collections/communication-metric.collection.ts +0 -84
- package/src/collections/counter.collection.ts +0 -56
- package/src/collections/cron-job-history.collection.ts +0 -94
- package/src/collections/cron-job.collection.ts +0 -92
- package/src/collections/customer-notification.collection.ts +0 -131
- package/src/collections/customer-portal-password.collection.ts +0 -76
- package/src/collections/email-history.collection.ts +0 -134
- package/src/collections/email-verified.collection.ts +0 -62
- package/src/collections/file.collection.ts +0 -74
- package/src/collections/flag-update.collection.ts +0 -57
- package/src/collections/flag.collection.ts +0 -57
- package/src/collections/log-method-latency.collection.ts +0 -77
- package/src/collections/log-subscription.collection.ts +0 -80
- package/src/collections/log.collection.ts +0 -93
- package/src/collections/logged-in-users.collection.ts +0 -67
- package/src/collections/monitor-cpu.collection.ts +0 -65
- package/src/collections/monitor-function.collection.ts +0 -74
- package/src/collections/monitor-memory.collection.ts +0 -77
- package/src/collections/monitor-mongo.collection.ts +0 -71
- package/src/collections/notification.collection.ts +0 -57
- package/src/collections/openai-usage-ledger.collection.ts +0 -131
- package/src/collections/report-builder-dashboard-builder.collection.ts +0 -109
- package/src/collections/report-builder-library.collection.ts +0 -89
- package/src/collections/report-builder-report.collection.ts +0 -184
- package/src/collections/user-group.collection.ts +0 -89
- package/src/collections/user-guide.collection.ts +0 -57
- package/src/collections/user.collection.ts +0 -181
- package/src/cron/cron.ts +0 -117
- package/src/fixtures/cron-jobs.ts +0 -95
- package/src/fixtures/init.ts +0 -35
- package/src/http/auth.ts +0 -818
- package/src/http/health.ts +0 -7
- package/src/http/home.ts +0 -90
- package/src/http/slow-query-publication.ts +0 -49
- package/src/index.ts +0 -1
- package/src/managers/ai-assistant-codex-manager.manager.ts +0 -1131
- package/src/managers/ai-run-evidence.manager.ts +0 -264
- package/src/managers/communication-metric.manager.ts +0 -82
- package/src/managers/cron.manager.ts +0 -333
- package/src/managers/customer-notification-content.manager.ts +0 -236
- package/src/managers/diagnostic-manager-bootstrap.ts +0 -165
- package/src/managers/error-auto-fix.manager.ts +0 -2767
- package/src/managers/local-log.manager.ts +0 -113
- package/src/managers/method.manager.ts +0 -1857
- package/src/managers/mongo.manager.ts +0 -4575
- package/src/managers/monitor.manager.ts +0 -507
- package/src/managers/openai-usage-ledger.manager.ts +0 -112
- package/src/managers/slow-query-verifier.manager.ts +0 -3590
- package/src/managers/slow-query.manager.ts +0 -519
- package/src/managers/subscription.manager.ts +0 -3128
- package/src/managers/websocket.manager.ts +0 -746
- package/src/managers/worker-dispatcher.manager.ts +0 -1360
- package/src/managers/worker-server.manager.ts +0 -536
- package/src/methods/accounts.ts +0 -532
- package/src/methods/ai-terminal.ts +0 -29070
- package/src/methods/app-settings.ts +0 -114
- package/src/methods/aws.ts +0 -649
- package/src/methods/collections.ts +0 -641
- package/src/methods/counters.ts +0 -69
- package/src/methods/cron-jobs.ts +0 -2614
- package/src/methods/customer-notifications.ts +0 -458
- package/src/methods/diagnostics.ts +0 -616
- package/src/methods/flag-updates.ts +0 -7
- package/src/methods/flags.ts +0 -7
- package/src/methods/logs.ts +0 -657
- package/src/methods/mongo-explorer.ts +0 -1880
- package/src/methods/monitor.ts +0 -540
- package/src/methods/pdf.ts +0 -1236
- package/src/methods/publications.ts +0 -129
- package/src/methods/report-builder.ts +0 -3300
- package/src/methods/support.ts +0 -335
- package/src/models/ai-run.model.ts +0 -27
- package/src/models/ai-terminal-conversation.model.ts +0 -19
- package/src/models/ai-terminal-issue-report.model.ts +0 -21
- package/src/models/ai-terminal-message.model.ts +0 -24
- package/src/models/app-setting.model.ts +0 -17
- package/src/models/collection-document.model.ts +0 -24
- package/src/models/communication-metric.model.ts +0 -23
- package/src/models/cron-job-history.model.ts +0 -16
- package/src/models/cron-job.model.ts +0 -15
- package/src/models/customer-notification.model.ts +0 -28
- package/src/models/customer-portal-password.model.ts +0 -12
- package/src/models/dialog.model.ts +0 -25
- package/src/models/log-method-latency.model.ts +0 -11
- package/src/models/log.model.ts +0 -19
- package/src/models/method.model.ts +0 -25
- package/src/models/monitor-function.model.ts +0 -16
- package/src/models/monitor-memory.model.ts +0 -17
- package/src/models/monitor-mongo.model.ts +0 -15
- package/src/models/openai-usage-ledger.model.ts +0 -56
- package/src/models/pagination.model.ts +0 -35
- package/src/models/permission.model.ts +0 -14
- package/src/models/report-builder-dashboard-builder.model.ts +0 -29
- package/src/models/report-builder-library.model.ts +0 -20
- package/src/models/report-builder-report.model.ts +0 -136
- package/src/models/report-builder.model.ts +0 -68
- package/src/models/select-data-label.model.ts +0 -9
- package/src/models/server-message.model.ts +0 -31
- package/src/models/slow-query-report.model.ts +0 -23
- package/src/models/subscription.model.ts +0 -73
- package/src/models/support-ticket.model.ts +0 -104
- package/src/models/user-group.model.ts +0 -24
- package/src/models/user.model.ts +0 -96
- package/src/private/images/ResolveIO.png +0 -0
- package/src/publications/ai-terminal.ts +0 -73
- package/src/publications/app-settings.ts +0 -25
- package/src/publications/app-status.ts +0 -13
- package/src/publications/cron-jobs.ts +0 -40
- package/src/publications/customer-notifications.ts +0 -101
- package/src/publications/files.ts +0 -33
- package/src/publications/flags-update.ts +0 -19
- package/src/publications/flags.ts +0 -19
- package/src/publications/logs.ts +0 -163
- package/src/publications/notifications.ts +0 -13
- package/src/publications/report-builder-dashboard-builders.ts +0 -39
- package/src/publications/report-builder-libraries.ts +0 -41
- package/src/publications/report-builder-reports.ts +0 -47
- package/src/publications/super-admin.ts +0 -13
- package/src/publications/user-groups.ts +0 -12
- package/src/publications/user-guides.ts +0 -12
- package/src/resolveio-server-app.ts +0 -617
- package/src/server-app.ts +0 -3354
- package/src/services/codex-client.ts +0 -1231
- package/src/services/openai-client.ts +0 -265
- package/src/types/error-report.ts +0 -26
- package/src/types/js-tiktoken.d.ts +0 -11
- package/src/types/slow-query-report.ts +0 -28
- package/src/util/ai-qa-policy.ts +0 -925
- package/src/util/ai-run-evidence-adapters.ts +0 -8347
- package/src/util/ai-run-evidence-dashboard.ts +0 -323
- package/src/util/ai-run-evidence-eval.ts +0 -1057
- package/src/util/ai-run-evidence.ts +0 -1430
- package/src/util/ai-runner-artifacts.ts +0 -586
- package/src/util/ai-runner-manager-autopilot.ts +0 -961
- package/src/util/ai-runner-manager-policy.ts +0 -5011
- package/src/util/ai-runner-qa-auth.ts +0 -838
- package/src/util/ai-runner-qa-tools.ts +0 -3536
- package/src/util/aicoder-runner-v6.ts +0 -3121
- package/src/util/common.ts +0 -649
- package/src/util/customer-portal-password.ts +0 -183
- package/src/util/error-reporter.ts +0 -332
- package/src/util/error-tracking.ts +0 -79
- package/src/util/openai-usage-cost.ts +0 -114
- package/src/util/report-builder-unwinds.ts +0 -180
- package/src/util/runner-process-janitor.ts +0 -219
- package/src/util/schema-report-builder.ts +0 -448
- package/src/util/slow-query-reporter.ts +0 -216
- package/src/util/subscription-dependency-context.ts +0 -1096
- package/src/util/support-runner-v5.ts +0 -10040
- package/src/util/tokenizer.ts +0 -38
- package/src/workers/codex-runner.worker.ts +0 -142
- package/start_server.sh +0 -5
- package/tests/ai-assistant-corpus-build.ts +0 -484
- package/tests/ai-assistant-corpus-replay-e2e.ts +0 -774
- package/tests/ai-assistant-data-parity-e2e.ts +0 -1989
- package/tests/ai-assistant-eval-triage.ts +0 -831
- package/tests/ai-assistant-openai-e2e.ts +0 -1061
- package/tests/ai-assistant-openai-git-e2e.ts +0 -155
- package/tests/ai-assistant-preflight-matrix.ts +0 -215
- package/tests/ai-assistant-routing-eval.test.ts +0 -585
- package/tests/ai-assistant-snf-live-eval.ts +0 -975
- package/tests/ai-assistant-utils.test.ts +0 -4834
- package/tests/ai-manager-autopilot-snapshot.test.ts +0 -193
- package/tests/ai-manager-recovery-checkpoint.test.ts +0 -1383
- package/tests/ai-run-eval.test.ts +0 -132
- package/tests/ai-run-evidence.test.ts +0 -3773
- package/tests/ai-runner-contract.test.ts +0 -515
- package/tests/aicoder-runner-v6.test.ts +0 -822
- package/tests/error-reporter.test.ts +0 -145
- package/tests/method-publication-generator.test.ts +0 -46
- package/tests/report-builder-linking.test.ts +0 -79
- package/tests/resolveio-platform-intelligence.test.ts +0 -352
- package/tests/server-app-cron-owner.test.ts +0 -127
- package/tests/subscription-connect-race.test.ts +0 -158
- package/tests/subscription-dependency-context.test.ts +0 -324
- package/tests/subscription-manager-collection-tracking.test.ts +0 -86
- package/tests/subscription-manager-invalidation.test.ts +0 -86
- package/tests/support-runner-v5.test.ts +0 -3201
- package/tsconfig.json +0 -34
- /package/{src/private → private}/email-templates/enrollment.html +0 -0
- /package/{src/private → private}/email-templates/forgot-password.html +0 -0
- /package/{src/private → private}/email-templates/support-ticket-deleted.html +0 -0
- /package/{src/private → private}/email-templates/support-ticket-modified.html +0 -0
- /package/{src/private → private}/email-templates/support-ticket.html +0 -0
- /package/{src/public_api.ts → public_api.d.ts} +0 -0
|
@@ -1,3536 +0,0 @@
|
|
|
1
|
-
import { buildRunnerProcessJanitorShellLibrary } from './runner-process-janitor';
|
|
2
|
-
|
|
3
|
-
export interface ResolveIORunnerQaToolBundleOptions {
|
|
4
|
-
mode?: 'support' | 'runner';
|
|
5
|
-
qaClientPort?: number | string;
|
|
6
|
-
defaultUsername?: string;
|
|
7
|
-
defaultPassword?: string;
|
|
8
|
-
jobId?: string;
|
|
9
|
-
ownerId?: string;
|
|
10
|
-
runnerToken?: string;
|
|
11
|
-
runnerRunId?: string;
|
|
12
|
-
runnerGeneration?: string | number;
|
|
13
|
-
mongoRuntimePath?: string;
|
|
14
|
-
qaLiveDataRequired?: boolean;
|
|
15
|
-
toolsBinPath?: string;
|
|
16
|
-
browserslistPath?: string;
|
|
17
|
-
mongodbBinaryCachePath?: string;
|
|
18
|
-
tmpRoot?: string;
|
|
19
|
-
homeRoot?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function shellDoubleQuote(value: string): string {
|
|
23
|
-
return String(value || '').replace(/["\\$`]/g, '\\$&');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function normalizePort(value: number | string | undefined, fallback: string): string {
|
|
27
|
-
const parsed = Number.parseInt(String(value || ''), 10);
|
|
28
|
-
return Number.isFinite(parsed) && parsed > 0 ? String(parsed) : fallback;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function envVar(mode: 'support' | 'runner', suffix: string): string {
|
|
32
|
-
return mode === 'support' ? `RESOLVEIO_SUPPORT_QA_${suffix}` : `RESOLVEIO_RUNNER_QA_${suffix}`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function buildResolveIORunnerQaEnvScript(options: ResolveIORunnerQaToolBundleOptions = {}): string {
|
|
36
|
-
const mode = options.mode || 'runner';
|
|
37
|
-
const altMode = mode === 'support' ? 'runner' : 'support';
|
|
38
|
-
const tmpRoot = options.tmpRoot || (mode === 'support' ? '/tmp/resolveio-support-qa' : '/tmp/resolveio-ai-runner-qa');
|
|
39
|
-
const homeRoot = options.homeRoot || `${tmpRoot}/home`;
|
|
40
|
-
const defaultPort = normalizePort(options.qaClientPort, '4200');
|
|
41
|
-
const username = shellDoubleQuote(options.defaultUsername || 'admin');
|
|
42
|
-
const password = shellDoubleQuote(options.defaultPassword || '');
|
|
43
|
-
const jobId = shellDoubleQuote(options.jobId || '');
|
|
44
|
-
const ownerId = shellDoubleQuote(options.ownerId || '');
|
|
45
|
-
const runnerToken = shellDoubleQuote(options.runnerToken || '');
|
|
46
|
-
const runnerRunId = shellDoubleQuote(options.runnerRunId || '');
|
|
47
|
-
const runnerGeneration = shellDoubleQuote(String(options.runnerGeneration || ''));
|
|
48
|
-
const mongoRuntimePath = shellDoubleQuote(options.mongoRuntimePath || '');
|
|
49
|
-
const qaLiveDataRequired = options.qaLiveDataRequired === true ? 'true' : 'false';
|
|
50
|
-
const toolsBinPath = options.toolsBinPath || '';
|
|
51
|
-
const browserslistPath = options.browserslistPath || '';
|
|
52
|
-
const mongodbBinaryCachePath = options.mongodbBinaryCachePath || '';
|
|
53
|
-
const clientPortVar = envVar(mode, 'CLIENT_PORT');
|
|
54
|
-
const altClientPortVar = envVar(altMode, 'CLIENT_PORT');
|
|
55
|
-
const clientUrlVar = envVar(mode, 'CLIENT_URL');
|
|
56
|
-
const altClientUrlVar = envVar(altMode, 'CLIENT_URL');
|
|
57
|
-
const serverUrlVar = envVar(mode, 'SERVER_URL');
|
|
58
|
-
const altServerUrlVar = envVar(altMode, 'SERVER_URL');
|
|
59
|
-
const usernameVar = envVar(mode, 'USERNAME');
|
|
60
|
-
const altUsernameVar = envVar(altMode, 'USERNAME');
|
|
61
|
-
const passwordVar = envVar(mode, 'PASSWORD');
|
|
62
|
-
const altPasswordVar = envVar(altMode, 'PASSWORD');
|
|
63
|
-
const viewportWidthVar = envVar(mode, 'VIEWPORT_WIDTH');
|
|
64
|
-
const altViewportWidthVar = envVar(altMode, 'VIEWPORT_WIDTH');
|
|
65
|
-
const viewportHeightVar = envVar(mode, 'VIEWPORT_HEIGHT');
|
|
66
|
-
const altViewportHeightVar = envVar(altMode, 'VIEWPORT_HEIGHT');
|
|
67
|
-
const timeoutVar = envVar(mode, 'ANGULAR_STARTUP_TIMEOUT_SECONDS');
|
|
68
|
-
const altTimeoutVar = envVar(altMode, 'ANGULAR_STARTUP_TIMEOUT_SECONDS');
|
|
69
|
-
const prebundleVar = envVar(mode, 'ANGULAR_PREBUNDLE');
|
|
70
|
-
const altPrebundleVar = envVar(altMode, 'ANGULAR_PREBUNDLE');
|
|
71
|
-
const reuseVar = envVar(mode, 'REUSE_RUNNING');
|
|
72
|
-
const altReuseVar = envVar(altMode, 'REUSE_RUNNING');
|
|
73
|
-
const keepaliveVar = envVar(mode, 'KEEPALIVE');
|
|
74
|
-
const altKeepaliveVar = envVar(altMode, 'KEEPALIVE');
|
|
75
|
-
const mongoPortVar = envVar(mode, 'MONGO_PORT');
|
|
76
|
-
const altMongoPortVar = envVar(altMode, 'MONGO_PORT');
|
|
77
|
-
const mongoUrlVar = envVar(mode, 'MONGO_URL');
|
|
78
|
-
const altMongoUrlVar = envVar(altMode, 'MONGO_URL');
|
|
79
|
-
const browserLoopVar = mode === 'support' ? 'RESOLVEIO_SUPPORT_QA_BROWSER' : 'RESOLVEIO_RUNNER_QA_BROWSER';
|
|
80
|
-
return [
|
|
81
|
-
`export ${mode === 'support' ? 'RESOLVEIO_SUPPORT_QA_TMP' : 'RESOLVEIO_RUNNER_QA_TMP'}="${'${' + (mode === 'support' ? 'RESOLVEIO_SUPPORT_QA_TMP' : 'RESOLVEIO_RUNNER_QA_TMP') + ':-' + tmpRoot + '}'}"`,
|
|
82
|
-
`export ${mode === 'support' ? 'RESOLVEIO_SUPPORT_QA_HOME' : 'RESOLVEIO_RUNNER_QA_HOME'}="${'${' + (mode === 'support' ? 'RESOLVEIO_SUPPORT_QA_HOME' : 'RESOLVEIO_RUNNER_QA_HOME') + ':-' + homeRoot + '}'}"`,
|
|
83
|
-
`RESOLVEIO_QA_TMP="${'${' + (mode === 'support' ? 'RESOLVEIO_SUPPORT_QA_TMP' : 'RESOLVEIO_RUNNER_QA_TMP') + '}'}"`,
|
|
84
|
-
`RESOLVEIO_QA_HOME="${'${' + (mode === 'support' ? 'RESOLVEIO_SUPPORT_QA_HOME' : 'RESOLVEIO_RUNNER_QA_HOME') + '}'}"`,
|
|
85
|
-
'RESOLVEIO_QA_TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"',
|
|
86
|
-
'if [ -z "${RESOLVEIO_SUPPORT_CODEX_LANE:-}" ] && [ -f "$RESOLVEIO_QA_TOOLS_DIR/current-codex-lane" ]; then',
|
|
87
|
-
' RESOLVEIO_SUPPORT_CODEX_LANE="$(tr -d "\\r\\n\\t " < "$RESOLVEIO_QA_TOOLS_DIR/current-codex-lane" 2>/dev/null || true)"',
|
|
88
|
-
' export RESOLVEIO_SUPPORT_CODEX_LANE',
|
|
89
|
-
'fi',
|
|
90
|
-
'resolveio_support_codex_lane_is_build() {',
|
|
91
|
-
' case "${RESOLVEIO_SUPPORT_CODEX_LANE:-}" in build|support_build|support-build) return 0 ;; *) return 1 ;; esac',
|
|
92
|
-
'}',
|
|
93
|
-
'resolveio_support_codex_block_build_lane_qa() {',
|
|
94
|
-
' resolveio_support_codex_lane_is_build || return 0',
|
|
95
|
-
' echo "ResolveIO support lane guard: build lane cannot run local QA/browser/server/client commands; return a build fix and let the QA lane run browser validation." >&2',
|
|
96
|
-
' return 86',
|
|
97
|
-
'}',
|
|
98
|
-
'mkdir -p "$RESOLVEIO_QA_HOME/.nvm" "$RESOLVEIO_QA_TMP/npm-cache" "$RESOLVEIO_QA_TMP/mongodb-binaries" "$RESOLVEIO_QA_TMP/mongodb-memory-server-core"',
|
|
99
|
-
'if [ ! -f "$RESOLVEIO_QA_HOME/.nvm/nvm.sh" ]; then',
|
|
100
|
-
' cat > "$RESOLVEIO_QA_HOME/.nvm/nvm.sh" <<\'RESOLVEIO_NVM_SHIM\'',
|
|
101
|
-
'nvm() {',
|
|
102
|
-
' case "$1" in',
|
|
103
|
-
' use|install|alias) return 0 ;;',
|
|
104
|
-
' current) node -v 2>/dev/null || true; return 0 ;;',
|
|
105
|
-
' *) return 0 ;;',
|
|
106
|
-
' esac',
|
|
107
|
-
'}',
|
|
108
|
-
'RESOLVEIO_NVM_SHIM',
|
|
109
|
-
'fi',
|
|
110
|
-
'export HOME="$RESOLVEIO_QA_HOME"',
|
|
111
|
-
'export NVM_DIR="$RESOLVEIO_QA_HOME/.nvm"',
|
|
112
|
-
'export RESOLVEIO_QA_COMMAND_GUARD_BIN="$RESOLVEIO_QA_TMP/command-guard-bin"',
|
|
113
|
-
'mkdir -p "$RESOLVEIO_QA_COMMAND_GUARD_BIN"',
|
|
114
|
-
'resolveio_qa_find_real_command() {',
|
|
115
|
-
' local name="$1"',
|
|
116
|
-
' local old_ifs="$IFS"',
|
|
117
|
-
' IFS=":"',
|
|
118
|
-
' for dir in $PATH; do',
|
|
119
|
-
' [ -n "$dir" ] || continue',
|
|
120
|
-
' [ "$dir" = "$RESOLVEIO_QA_COMMAND_GUARD_BIN" ] && continue',
|
|
121
|
-
' if [ -x "$dir/$name" ]; then',
|
|
122
|
-
' echo "$dir/$name"',
|
|
123
|
-
' IFS="$old_ifs"',
|
|
124
|
-
' return 0',
|
|
125
|
-
' fi',
|
|
126
|
-
' done',
|
|
127
|
-
' IFS="$old_ifs"',
|
|
128
|
-
' return 1',
|
|
129
|
-
'}',
|
|
130
|
-
'export RESOLVEIO_QA_REAL_NPM="${RESOLVEIO_QA_REAL_NPM:-$(resolveio_qa_find_real_command npm || true)}"',
|
|
131
|
-
'if [ -n "$RESOLVEIO_QA_REAL_NPM" ]; then',
|
|
132
|
-
' cat > "$RESOLVEIO_QA_COMMAND_GUARD_BIN/npm" <<\'RESOLVEIO_QA_NPM_GUARD\'',
|
|
133
|
-
'#!/usr/bin/env bash',
|
|
134
|
-
'set -u',
|
|
135
|
-
'REAL_NPM="${RESOLVEIO_QA_REAL_NPM:-}"',
|
|
136
|
-
'GUARD_BIN="${RESOLVEIO_QA_COMMAND_GUARD_BIN:-}"',
|
|
137
|
-
'if [ -z "$REAL_NPM" ] || [ "$REAL_NPM" = "$0" ]; then',
|
|
138
|
-
' OLD_IFS="$IFS"',
|
|
139
|
-
' IFS=":"',
|
|
140
|
-
' for dir in $PATH; do',
|
|
141
|
-
' [ -n "$dir" ] || continue',
|
|
142
|
-
' [ -n "$GUARD_BIN" ] && [ "$dir" = "$GUARD_BIN" ] && continue',
|
|
143
|
-
' if [ -x "$dir/npm" ]; then REAL_NPM="$dir/npm"; break; fi',
|
|
144
|
-
' done',
|
|
145
|
-
' IFS="$OLD_IFS"',
|
|
146
|
-
'fi',
|
|
147
|
-
'if [ -z "$REAL_NPM" ] || [ ! -x "$REAL_NPM" ]; then',
|
|
148
|
-
' echo "ResolveIO QA command guard: unable to locate real npm." >&2',
|
|
149
|
-
' exit 127',
|
|
150
|
-
'fi',
|
|
151
|
-
'if [ "${RESOLVEIO_SUPPORT_CODEX_LANE:-}" = "build" ] || [ "${RESOLVEIO_SUPPORT_CODEX_LANE:-}" = "support_build" ] || [ "${RESOLVEIO_SUPPORT_CODEX_LANE:-}" = "support-build" ]; then',
|
|
152
|
-
' if [ "${1:-}" = "run" ]; then',
|
|
153
|
-
' case "${2:-}" in',
|
|
154
|
-
' build-dev)',
|
|
155
|
-
' echo "ResolveIO support lane guard: refusing npm run build-dev from the 5.3 build lane. Use a finite build-prod/tsc/unit self-gate or return a blocked self-gate." >&2',
|
|
156
|
-
' exit 86',
|
|
157
|
-
' ;;',
|
|
158
|
-
' server|client|start|dev|serve|local-qa|qa|qa:*|test:browser|test:e2e|e2e)',
|
|
159
|
-
' echo "ResolveIO support lane guard: refusing npm run ${2:-} from the 5.3 build lane. Browser/server/client QA belongs to the 5.4 mini QA lane." >&2',
|
|
160
|
-
' exit 86',
|
|
161
|
-
' ;;',
|
|
162
|
-
' esac',
|
|
163
|
-
' fi',
|
|
164
|
-
'fi',
|
|
165
|
-
'is_angular_build=0',
|
|
166
|
-
'if [ "${1:-}" = "run" ]; then',
|
|
167
|
-
' case "${2:-}" in',
|
|
168
|
-
' build|build-dev|build-prod) is_angular_build=1 ;;',
|
|
169
|
-
' esac',
|
|
170
|
-
'fi',
|
|
171
|
-
'resolveio_lock_live() {',
|
|
172
|
-
' local file="$1"',
|
|
173
|
-
' local ttl="${RESOLVEIO_QA_COMMAND_LOCK_TTL_SECONDS:-1800}"',
|
|
174
|
-
' [ -f "$file" ] || return 1',
|
|
175
|
-
' node - "$file" "$ttl" <<\'RESOLVEIO_QA_LOCK_LIVE\'',
|
|
176
|
-
'const fs = require("fs");',
|
|
177
|
-
'const [file, ttlRaw] = process.argv.slice(2);',
|
|
178
|
-
'const ttlMs = Math.max(1, Number(ttlRaw || 1800)) * 1000;',
|
|
179
|
-
'try {',
|
|
180
|
-
' const data = JSON.parse(fs.readFileSync(file, "utf8"));',
|
|
181
|
-
' const pid = Number(data.pid || 0);',
|
|
182
|
-
' const heartbeat = Date.parse(data.heartbeat_at || data.started_at || "");',
|
|
183
|
-
' if (!pid || !heartbeat || Date.now() - heartbeat > ttlMs) process.exit(1);',
|
|
184
|
-
' process.kill(pid, 0);',
|
|
185
|
-
' process.exit(0);',
|
|
186
|
-
'} catch (error) { process.exit(1); }',
|
|
187
|
-
'RESOLVEIO_QA_LOCK_LIVE',
|
|
188
|
-
'}',
|
|
189
|
-
'if [ "$is_angular_build" = "1" ]; then',
|
|
190
|
-
' PROJECT_ROOT="${RESOLVEIO_SUPPORT_QA_PROJECT_ROOT:-${RESOLVEIO_RUNNER_QA_PROJECT_ROOT:-$(pwd)}}"',
|
|
191
|
-
' PROJECT_ROOT="$(cd "$PROJECT_ROOT" 2>/dev/null && pwd || pwd)"',
|
|
192
|
-
' ARTIFACT_DIR="$PROJECT_ROOT/qa-artifacts"',
|
|
193
|
-
' mkdir -p "$ARTIFACT_DIR/.command-locks"',
|
|
194
|
-
' QA_LOCK="$ARTIFACT_DIR/.qa.lock/lock.json"',
|
|
195
|
-
' if resolveio_lock_live "$QA_LOCK"; then',
|
|
196
|
-
' echo "ResolveIO QA command guard: refusing npm run ${2:-} while local QA harness is active for $PROJECT_ROOT. Stop QA first with stop-local-qa.sh." >&2',
|
|
197
|
-
' exit 86',
|
|
198
|
-
' fi',
|
|
199
|
-
' BUILD_LOCK_DIR="$ARTIFACT_DIR/.command-locks/angular-build.lock"',
|
|
200
|
-
' BUILD_LOCK_JSON="$BUILD_LOCK_DIR/lock.json"',
|
|
201
|
-
' if ! mkdir "$BUILD_LOCK_DIR" 2>/dev/null; then',
|
|
202
|
-
' if resolveio_lock_live "$BUILD_LOCK_JSON"; then',
|
|
203
|
-
' echo "ResolveIO QA command guard: refusing duplicate npm run ${2:-}; another Angular build is active for $PROJECT_ROOT." >&2',
|
|
204
|
-
' exit 86',
|
|
205
|
-
' fi',
|
|
206
|
-
' rm -rf "$BUILD_LOCK_DIR" 2>/dev/null || true',
|
|
207
|
-
' if ! mkdir "$BUILD_LOCK_DIR" 2>/dev/null; then',
|
|
208
|
-
' echo "ResolveIO QA command guard: unable to acquire Angular build lock for $PROJECT_ROOT." >&2',
|
|
209
|
-
' exit 86',
|
|
210
|
-
' fi',
|
|
211
|
-
' fi',
|
|
212
|
-
' cat > "$BUILD_LOCK_JSON" <<EOF',
|
|
213
|
-
'{"pid": $$, "command": "npm $*", "project_root": "$PROJECT_ROOT", "started_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "heartbeat_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"}',
|
|
214
|
-
'EOF',
|
|
215
|
-
' ( while true; do node - "$BUILD_LOCK_JSON" <<\'RESOLVEIO_QA_BUILD_HEARTBEAT\'',
|
|
216
|
-
'const fs = require("fs");',
|
|
217
|
-
'const [file] = process.argv.slice(2);',
|
|
218
|
-
'try {',
|
|
219
|
-
' const data = JSON.parse(fs.readFileSync(file, "utf8"));',
|
|
220
|
-
' data.heartbeat_at = new Date().toISOString();',
|
|
221
|
-
' fs.writeFileSync(file, JSON.stringify(data, null, 2));',
|
|
222
|
-
'} catch (error) {}',
|
|
223
|
-
'RESOLVEIO_QA_BUILD_HEARTBEAT',
|
|
224
|
-
' sleep 10',
|
|
225
|
-
' done ) >/dev/null 2>&1 &',
|
|
226
|
-
' HEARTBEAT_PID="$!"',
|
|
227
|
-
' cleanup_build_lock() {',
|
|
228
|
-
' kill "$HEARTBEAT_PID" >/dev/null 2>&1 || true',
|
|
229
|
-
' rm -rf "$BUILD_LOCK_DIR" >/dev/null 2>&1 || true',
|
|
230
|
-
' }',
|
|
231
|
-
' trap cleanup_build_lock EXIT INT TERM',
|
|
232
|
-
'fi',
|
|
233
|
-
'exec "$REAL_NPM" "$@"',
|
|
234
|
-
'RESOLVEIO_QA_NPM_GUARD',
|
|
235
|
-
' chmod +x "$RESOLVEIO_QA_COMMAND_GUARD_BIN/npm"',
|
|
236
|
-
' export PATH="$RESOLVEIO_QA_COMMAND_GUARD_BIN:$PATH"',
|
|
237
|
-
' hash -r 2>/dev/null || true',
|
|
238
|
-
'fi',
|
|
239
|
-
'export RESOLVEIO_QA_REAL_NODE="${RESOLVEIO_QA_REAL_NODE:-$(resolveio_qa_find_real_command node || true)}"',
|
|
240
|
-
'if [ -n "$RESOLVEIO_QA_REAL_NODE" ]; then',
|
|
241
|
-
' cat > "$RESOLVEIO_QA_COMMAND_GUARD_BIN/node" <<\'RESOLVEIO_QA_NODE_GUARD\'',
|
|
242
|
-
'#!/usr/bin/env bash',
|
|
243
|
-
'set -u',
|
|
244
|
-
'REAL_NODE="${RESOLVEIO_QA_REAL_NODE:-}"',
|
|
245
|
-
'GUARD_BIN="${RESOLVEIO_QA_COMMAND_GUARD_BIN:-}"',
|
|
246
|
-
'if [ -z "$REAL_NODE" ] || [ "$REAL_NODE" = "$0" ]; then',
|
|
247
|
-
' OLD_IFS="$IFS"',
|
|
248
|
-
' IFS=":"',
|
|
249
|
-
' for dir in $PATH; do',
|
|
250
|
-
' [ -n "$dir" ] || continue',
|
|
251
|
-
' [ -n "$GUARD_BIN" ] && [ "$dir" = "$GUARD_BIN" ] && continue',
|
|
252
|
-
' if [ -x "$dir/node" ]; then REAL_NODE="$dir/node"; break; fi',
|
|
253
|
-
' done',
|
|
254
|
-
' IFS="$OLD_IFS"',
|
|
255
|
-
'fi',
|
|
256
|
-
'if [ -z "$REAL_NODE" ] || [ ! -x "$REAL_NODE" ]; then',
|
|
257
|
-
' echo "ResolveIO QA command guard: unable to locate real node." >&2',
|
|
258
|
-
' exit 127',
|
|
259
|
-
'fi',
|
|
260
|
-
'resolveio_qa_node_guard_active() {',
|
|
261
|
-
' case "${RESOLVEIO_SUPPORT_CODEX_LANE:-}" in qa|support_qa|support-qa) return 0 ;; *) return 1 ;; esac',
|
|
262
|
-
'}',
|
|
263
|
-
'resolveio_qa_scan_inline_node_script() {',
|
|
264
|
-
' local script_file="$1"',
|
|
265
|
-
' [ -f "$script_file" ] || return 0',
|
|
266
|
-
` if grep -Eiq "puppeteer browsers install|npx puppeteer|playwright install" "$script_file"; then`,
|
|
267
|
-
' echo "ResolveIO QA command guard: refusing browser install/download commands. Use the staged browser executable from PUPPETEER_EXECUTABLE_PATH/CHROME_BIN." >&2',
|
|
268
|
-
' exit 86',
|
|
269
|
-
' fi',
|
|
270
|
-
'}',
|
|
271
|
-
'if resolveio_qa_node_guard_active; then',
|
|
272
|
-
' case "${1:-}" in',
|
|
273
|
-
' -)',
|
|
274
|
-
' TMP_SCRIPT="${RESOLVEIO_QA_TMP:-/tmp/resolveio-support-qa}/inline-node-guard-$$.js"',
|
|
275
|
-
' mkdir -p "$(dirname "$TMP_SCRIPT")"',
|
|
276
|
-
' cat > "$TMP_SCRIPT"',
|
|
277
|
-
' resolveio_qa_scan_inline_node_script "$TMP_SCRIPT"',
|
|
278
|
-
' shift',
|
|
279
|
-
' exec "$REAL_NODE" "$TMP_SCRIPT" "$@"',
|
|
280
|
-
' ;;',
|
|
281
|
-
' -e|--eval)',
|
|
282
|
-
' TMP_SCRIPT="${RESOLVEIO_QA_TMP:-/tmp/resolveio-support-qa}/inline-node-guard-$$.js"',
|
|
283
|
-
' mkdir -p "$(dirname "$TMP_SCRIPT")"',
|
|
284
|
-
' printf "%s\\n" "${2:-}" > "$TMP_SCRIPT"',
|
|
285
|
-
' resolveio_qa_scan_inline_node_script "$TMP_SCRIPT"',
|
|
286
|
-
' exec "$REAL_NODE" "$@"',
|
|
287
|
-
' ;;',
|
|
288
|
-
' esac',
|
|
289
|
-
'fi',
|
|
290
|
-
'exec "$REAL_NODE" "$@"',
|
|
291
|
-
'RESOLVEIO_QA_NODE_GUARD',
|
|
292
|
-
' chmod +x "$RESOLVEIO_QA_COMMAND_GUARD_BIN/node"',
|
|
293
|
-
' export PATH="$RESOLVEIO_QA_COMMAND_GUARD_BIN:$PATH"',
|
|
294
|
-
' hash -r 2>/dev/null || true',
|
|
295
|
-
'fi',
|
|
296
|
-
toolsBinPath ? `export PATH="${toolsBinPath}:$PATH"` : '',
|
|
297
|
-
'export NODE_ENV=development',
|
|
298
|
-
'# Local QA must start the application HTTP server even when launched from a dedicated support-manager worker.',
|
|
299
|
-
'export IS_WORKERS_ENABLED=false',
|
|
300
|
-
'export IS_WORKER_INSTANCE=false',
|
|
301
|
-
'export DISABLE_WORKER_SERVER_CONNECTION=false',
|
|
302
|
-
'export SUPPORT_CODEX_MANAGER_PROCESS_ONLY=false',
|
|
303
|
-
'export SUPPORT_AUTO_MANAGER_PROCESS_ONLY=false',
|
|
304
|
-
'export AI_ASSISTANT_CODEX_MANAGER_PROCESS_ONLY=false',
|
|
305
|
-
browserslistPath ? `export BROWSERSLIST_CONFIG="${browserslistPath}"` : '',
|
|
306
|
-
`export ${clientPortVar}="${'${' + clientPortVar + ':-${' + altClientPortVar + ':-' + defaultPort + '}}'}"`,
|
|
307
|
-
`export ${altClientPortVar}="${'${' + altClientPortVar + ':-${' + clientPortVar + '}}'}"`,
|
|
308
|
-
`export ${clientUrlVar}="${'${' + clientUrlVar + ':-${' + altClientUrlVar + ':-http://localhost:${' + clientPortVar + '}}}'}"`,
|
|
309
|
-
`export ${altClientUrlVar}="${'${' + altClientUrlVar + ':-${' + clientUrlVar + '}}'}"`,
|
|
310
|
-
`export ${serverUrlVar}="${'${' + serverUrlVar + ':-${' + altServerUrlVar + ':-http://localhost:8080}}'}"`,
|
|
311
|
-
`export ${altServerUrlVar}="${'${' + altServerUrlVar + ':-${' + serverUrlVar + '}}'}"`,
|
|
312
|
-
'export ADDITIONAL_ALLOWED_ORIGINS="${ADDITIONAL_ALLOWED_ORIGINS:-$' + clientUrlVar + '}"',
|
|
313
|
-
`export ${viewportWidthVar}="${'${' + viewportWidthVar + ':-${' + altViewportWidthVar + ':-1920}}'}"`,
|
|
314
|
-
`export ${altViewportWidthVar}="${'${' + altViewportWidthVar + ':-${' + viewportWidthVar + '}}'}"`,
|
|
315
|
-
`export ${viewportHeightVar}="${'${' + viewportHeightVar + ':-${' + altViewportHeightVar + ':-1080}}'}"`,
|
|
316
|
-
`export ${altViewportHeightVar}="${'${' + altViewportHeightVar + ':-${' + viewportHeightVar + '}}'}"`,
|
|
317
|
-
`export ${timeoutVar}="${'${' + timeoutVar + ':-${' + altTimeoutVar + ':-900}}'}"`,
|
|
318
|
-
`export ${altTimeoutVar}="${'${' + altTimeoutVar + ':-${' + timeoutVar + '}}'}"`,
|
|
319
|
-
`export ${prebundleVar}="${'${' + prebundleVar + ':-${' + altPrebundleVar + ':-false}}'}"`,
|
|
320
|
-
`export ${altPrebundleVar}="${'${' + altPrebundleVar + ':-${' + prebundleVar + '}}'}"`,
|
|
321
|
-
`export ${reuseVar}="${'${' + reuseVar + ':-${' + altReuseVar + ':-true}}'}"`,
|
|
322
|
-
`export ${altReuseVar}="${'${' + altReuseVar + ':-${' + reuseVar + '}}'}"`,
|
|
323
|
-
`export ${keepaliveVar}="${'${' + keepaliveVar + ':-${' + altKeepaliveVar + ':-false}}'}"`,
|
|
324
|
-
`export ${altKeepaliveVar}="${'${' + altKeepaliveVar + ':-${' + keepaliveVar + '}}'}"`,
|
|
325
|
-
`export ${mongoPortVar}="${'${' + mongoPortVar + ':-${' + altMongoPortVar + ':-3001}}'}"`,
|
|
326
|
-
`export ${altMongoPortVar}="${'${' + altMongoPortVar + ':-${' + mongoPortVar + '}}'}"`,
|
|
327
|
-
'export RESOLVEIO_QA_INHERITED_MONGO_URL="${RESOLVEIO_QA_INHERITED_MONGO_URL:-${MONGO_URL:-}}"',
|
|
328
|
-
`export ${mongoUrlVar}="mongodb://127.0.0.1:${'${' + mongoPortVar + '}'}/resolveio?directConnection=true"`,
|
|
329
|
-
`export ${altMongoUrlVar}="${'${' + mongoUrlVar + '}'}"`,
|
|
330
|
-
`export MONGO_URL="${'${' + mongoUrlVar + '}'}"`,
|
|
331
|
-
`export ${usernameVar}="${'${' + usernameVar + ':-${' + altUsernameVar + ':-' + username + '}}'}"`,
|
|
332
|
-
`export ${altUsernameVar}="${'${' + altUsernameVar + ':-${' + usernameVar + '}}'}"`,
|
|
333
|
-
`export ${passwordVar}="${'${' + passwordVar + ':-${' + altPasswordVar + ':-' + password + '}}'}"`,
|
|
334
|
-
`export ${altPasswordVar}="${'${' + altPasswordVar + ':-${' + passwordVar + '}}'}"`,
|
|
335
|
-
`export ${envVar(mode, 'JOB_ID')}="${'${' + envVar(mode, 'JOB_ID') + ':-${' + envVar(altMode, 'JOB_ID') + ':-' + jobId + '}}'}"`,
|
|
336
|
-
`export ${envVar(altMode, 'JOB_ID')}="${'${' + envVar(altMode, 'JOB_ID') + ':-${' + envVar(mode, 'JOB_ID') + '}}'}"`,
|
|
337
|
-
`export ${envVar(mode, 'OWNER_ID')}="${'${' + envVar(mode, 'OWNER_ID') + ':-${' + envVar(altMode, 'OWNER_ID') + ':-' + ownerId + '}}'}"`,
|
|
338
|
-
`export ${envVar(altMode, 'OWNER_ID')}="${'${' + envVar(altMode, 'OWNER_ID') + ':-${' + envVar(mode, 'OWNER_ID') + '}}'}"`,
|
|
339
|
-
`export ${envVar(mode, 'RUNNER_TOKEN')}="${'${' + envVar(mode, 'RUNNER_TOKEN') + ':-${' + envVar(altMode, 'RUNNER_TOKEN') + ':-' + runnerToken + '}}'}"`,
|
|
340
|
-
`export ${envVar(altMode, 'RUNNER_TOKEN')}="${'${' + envVar(altMode, 'RUNNER_TOKEN') + ':-${' + envVar(mode, 'RUNNER_TOKEN') + '}}'}"`,
|
|
341
|
-
`export ${envVar(mode, 'RUNNER_RUN_ID')}="${'${' + envVar(mode, 'RUNNER_RUN_ID') + ':-${' + envVar(altMode, 'RUNNER_RUN_ID') + ':-' + runnerRunId + '}}'}"`,
|
|
342
|
-
`export ${envVar(altMode, 'RUNNER_RUN_ID')}="${'${' + envVar(altMode, 'RUNNER_RUN_ID') + ':-${' + envVar(mode, 'RUNNER_RUN_ID') + '}}'}"`,
|
|
343
|
-
`export ${envVar(mode, 'RUNNER_GENERATION')}="${'${' + envVar(mode, 'RUNNER_GENERATION') + ':-${' + envVar(altMode, 'RUNNER_GENERATION') + ':-' + runnerGeneration + '}}'}"`,
|
|
344
|
-
`export ${envVar(altMode, 'RUNNER_GENERATION')}="${'${' + envVar(altMode, 'RUNNER_GENERATION') + ':-${' + envVar(mode, 'RUNNER_GENERATION') + '}}'}"`,
|
|
345
|
-
mongoRuntimePath ? `export ${envVar(mode, 'MONGO_RUNTIME_PATH')}="${'${' + envVar(mode, 'MONGO_RUNTIME_PATH') + ':-${' + envVar(altMode, 'MONGO_RUNTIME_PATH') + ':-' + mongoRuntimePath + '}}'}"` : '',
|
|
346
|
-
mongoRuntimePath ? `export ${envVar(altMode, 'MONGO_RUNTIME_PATH')}="${'${' + envVar(altMode, 'MONGO_RUNTIME_PATH') + ':-${' + envVar(mode, 'MONGO_RUNTIME_PATH') + '}}'}"` : '',
|
|
347
|
-
mongoRuntimePath ? `export RESOLVEIO_QA_MONGO_RUNTIME_PATH="${'${RESOLVEIO_QA_MONGO_RUNTIME_PATH:-$' + envVar(mode, 'MONGO_RUNTIME_PATH') + '}'}"` : '',
|
|
348
|
-
`export ${envVar(mode, 'LIVE_DATA_REQUIRED')}="${'${' + envVar(mode, 'LIVE_DATA_REQUIRED') + ':-${' + envVar(altMode, 'LIVE_DATA_REQUIRED') + ':-' + qaLiveDataRequired + '}}'}"`,
|
|
349
|
-
`export ${envVar(altMode, 'LIVE_DATA_REQUIRED')}="${'${' + envVar(altMode, 'LIVE_DATA_REQUIRED') + ':-${' + envVar(mode, 'LIVE_DATA_REQUIRED') + '}}'}"`,
|
|
350
|
-
`export RESOLVEIO_QA_LIVE_DATA_REQUIRED="${'${RESOLVEIO_QA_LIVE_DATA_REQUIRED:-$' + envVar(mode, 'LIVE_DATA_REQUIRED') + '}'}"`,
|
|
351
|
-
'export PUPPETEER_CACHE_DIR="/var/lib/resolveio/puppeteer"',
|
|
352
|
-
'RESOLVEIO_QA_NODE_PATH_ENTRIES=""',
|
|
353
|
-
'for RESOLVEIO_QA_NODE_MODULES in "$PWD/node_modules" "$PWD/server/node_modules" "$(cd "$PWD/.." 2>/dev/null && pwd)/node_modules" "$(cd "$PWD/.." 2>/dev/null && pwd)/server/node_modules" /var/app/current/node_modules; do',
|
|
354
|
-
' if [ -d "$RESOLVEIO_QA_NODE_MODULES" ]; then',
|
|
355
|
-
' if [ -z "$RESOLVEIO_QA_NODE_PATH_ENTRIES" ]; then RESOLVEIO_QA_NODE_PATH_ENTRIES="$RESOLVEIO_QA_NODE_MODULES"; else RESOLVEIO_QA_NODE_PATH_ENTRIES="$RESOLVEIO_QA_NODE_PATH_ENTRIES:$RESOLVEIO_QA_NODE_MODULES"; fi',
|
|
356
|
-
' fi',
|
|
357
|
-
'done',
|
|
358
|
-
'if [ -n "$RESOLVEIO_QA_NODE_PATH_ENTRIES" ]; then',
|
|
359
|
-
' export NODE_PATH="${NODE_PATH:+$NODE_PATH:}$RESOLVEIO_QA_NODE_PATH_ENTRIES"',
|
|
360
|
-
'fi',
|
|
361
|
-
'unset RESOLVEIO_QA_NODE_MODULES RESOLVEIO_QA_NODE_PATH_ENTRIES',
|
|
362
|
-
'export PLAYWRIGHT_BROWSERS_PATH="${PLAYWRIGHT_BROWSERS_PATH:-$RESOLVEIO_QA_HOME/.cache/ms-playwright}"',
|
|
363
|
-
'# QA rows should launch their own scoped browser unless the runner explicitly provides a live endpoint later.',
|
|
364
|
-
'unset RESOLVEIO_RUNNER_QA_BROWSER_URL RESOLVEIO_SUPPORT_QA_BROWSER_URL',
|
|
365
|
-
'if [ -n "${PUPPETEER_EXECUTABLE_PATH:-}" ] && [ -x "$PUPPETEER_EXECUTABLE_PATH" ]; then',
|
|
366
|
-
' export CHROME_BIN="${CHROME_BIN:-$PUPPETEER_EXECUTABLE_PATH}"',
|
|
367
|
-
'elif [ -n "${CHROME_BIN:-}" ] && [ -x "$CHROME_BIN" ]; then',
|
|
368
|
-
' export PUPPETEER_EXECUTABLE_PATH="$CHROME_BIN"',
|
|
369
|
-
'else',
|
|
370
|
-
` for ${browserLoopVar} in /var/lib/resolveio/puppeteer/chrome-headless-shell/linux-*/chrome-headless-shell-linux64/chrome-headless-shell /var/lib/resolveio/puppeteer/chrome/linux-*/chrome-linux64/chrome /usr/bin/google-chrome-stable /usr/bin/google-chrome /usr/bin/chromium /usr/bin/chromium-browser; do`,
|
|
371
|
-
` if [ -x "$${browserLoopVar}" ]; then`,
|
|
372
|
-
` export PUPPETEER_EXECUTABLE_PATH="$${browserLoopVar}"`,
|
|
373
|
-
` export CHROME_BIN="$${browserLoopVar}"`,
|
|
374
|
-
' break',
|
|
375
|
-
' fi',
|
|
376
|
-
' done',
|
|
377
|
-
'fi',
|
|
378
|
-
`unset ${browserLoopVar}`,
|
|
379
|
-
'export NPM_CONFIG_CACHE="$RESOLVEIO_QA_TMP/npm-cache"',
|
|
380
|
-
'export NPM_CONFIG_PREFER_OFFLINE=true',
|
|
381
|
-
'export NPM_CONFIG_PRODUCTION=false',
|
|
382
|
-
'export npm_config_production=false',
|
|
383
|
-
'export RESOLVEIO_SUPPORT_MONGOMS_PACKAGE_ROOT="$RESOLVEIO_QA_TMP/mongodb-memory-server-core"',
|
|
384
|
-
'export MONGOMS_DOWNLOAD_DIR="$RESOLVEIO_QA_TMP/mongodb-binaries"',
|
|
385
|
-
mongodbBinaryCachePath ? `export RESOLVEIO_SUPPORT_SHARED_MONGOMS_DOWNLOAD_DIR="${mongodbBinaryCachePath}"` : '',
|
|
386
|
-
'export MONGOMS_VERSION="${MONGOMS_VERSION:-7.0.14}"',
|
|
387
|
-
''
|
|
388
|
-
].filter((line) => line !== '').join('\n');
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
export function buildResolveIORunnerQaPreflightScript(): string {
|
|
392
|
-
return [
|
|
393
|
-
'#!/usr/bin/env node',
|
|
394
|
-
'const fs = require("fs");',
|
|
395
|
-
'const path = require("path");',
|
|
396
|
-
'const { spawnSync } = require("child_process");',
|
|
397
|
-
'const projectRoot = path.resolve(process.argv[2] || process.cwd());',
|
|
398
|
-
'const artifactDir = path.join(projectRoot, "qa-artifacts");',
|
|
399
|
-
'const resultPath = path.join(artifactDir, "qa-preflight-result.json");',
|
|
400
|
-
'fs.mkdirSync(artifactDir, { recursive: true });',
|
|
401
|
-
'const checks = [];',
|
|
402
|
-
'function now() { return new Date().toISOString(); }',
|
|
403
|
-
'function redact(value) { return String(value || "").replace(/mongodb(?:\\+srv)?:\\/\\/[^\\s"\\\']+/ig, "[redacted-mongo-url]").replace(/\\b(?:sk|ghp|AKIA)[A-Za-z0-9_\\-]{12,}\\b/g, "[redacted-secret]"); }',
|
|
404
|
-
'function check(name, status, message, extra = {}) { checks.push({ name, status, message: redact(message), checkedAt: now(), ...extra }); }',
|
|
405
|
-
'function fileExecutable(filePath) { try { fs.accessSync(filePath, fs.constants.X_OK); return true; } catch (error) { return false; } }',
|
|
406
|
-
'function readJson(filePath) { try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch (error) { return null; } }',
|
|
407
|
-
'function packageScripts(root) { const pkg = readJson(path.join(root, "package.json")); return pkg && pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {}; }',
|
|
408
|
-
'function requireFromCandidates(candidates, label) {',
|
|
409
|
-
' for (const candidate of candidates) {',
|
|
410
|
-
' try { return { module: require(candidate), candidate }; } catch (error) {}',
|
|
411
|
-
' }',
|
|
412
|
-
' throw new Error(`Unable to require ${label} from ${candidates.join(", ")}`);',
|
|
413
|
-
'}',
|
|
414
|
-
'function shellWhich(command) {',
|
|
415
|
-
' const result = spawnSync("bash", ["-lc", `command -v ${command}`], { encoding: "utf8", timeout: 3000 });',
|
|
416
|
-
' return result.status === 0 ? String(result.stdout || "").trim().split(/\\r?\\n/)[0] : "";',
|
|
417
|
-
'}',
|
|
418
|
-
'try {',
|
|
419
|
-
' const nodeMajor = Number(String(process.versions.node || "").split(".")[0] || 0);',
|
|
420
|
-
' check("node_version", nodeMajor >= 22 && nodeMajor < 23 ? "pass" : "fail", `Node ${process.version}`, { version: process.version });',
|
|
421
|
-
'} catch (error) { check("node_version", "fail", error && error.message || error); }',
|
|
422
|
-
'try {',
|
|
423
|
-
' const npmPath = shellWhich("npm");',
|
|
424
|
-
' check("npm_available", npmPath ? "pass" : "fail", npmPath ? `npm found at ${npmPath}` : "npm was not found on PATH", { path: npmPath });',
|
|
425
|
-
'} catch (error) { check("npm_available", "fail", error && error.message || error); }',
|
|
426
|
-
'try {',
|
|
427
|
-
' const scripts = packageScripts(projectRoot);',
|
|
428
|
-
' const serverScripts = fs.existsSync(path.join(projectRoot, "server")) ? packageScripts(path.join(projectRoot, "server")) : {};',
|
|
429
|
-
' check("client_start_script", scripts.client || scripts.start || scripts.server ? "pass" : "blocked", "Project package scripts checked.", { scripts: Object.keys(scripts).filter((key) => /^(client|start|server|dev|build|build-dev|build-prod)$/.test(key)) });',
|
|
430
|
-
' check("server_start_script", !fs.existsSync(path.join(projectRoot, "server")) || serverScripts.server || serverScripts.start || serverScripts["build-prod"] ? "pass" : "blocked", "Server package scripts checked.", { scripts: Object.keys(serverScripts).filter((key) => /^(server|start|dev|build|build-dev|build-prod)$/.test(key)) });',
|
|
431
|
-
'} catch (error) { check("package_scripts", "blocked", error && error.message || error); }',
|
|
432
|
-
'try {',
|
|
433
|
-
' const settingsCandidates = [',
|
|
434
|
-
' path.join(projectRoot, "server", "settings.json"),',
|
|
435
|
-
' path.join(projectRoot, "server", "settings.local.json"),',
|
|
436
|
-
' path.join(projectRoot, "settings.json"),',
|
|
437
|
-
' path.join(projectRoot, "settings.local.json"),',
|
|
438
|
-
' path.join(projectRoot, "server", ".env"),',
|
|
439
|
-
' path.join(projectRoot, "server", ".env.codex"),',
|
|
440
|
-
' path.join(projectRoot, ".env"),',
|
|
441
|
-
' path.join(projectRoot, ".env.codex")',
|
|
442
|
-
' ];',
|
|
443
|
-
' const found = settingsCandidates.find((candidate) => fs.existsSync(candidate));',
|
|
444
|
-
' check("settings_available", found || !fs.existsSync(path.join(projectRoot, "server")) ? "pass" : "blocked", found ? `Runtime settings/env file exists at ${path.relative(projectRoot, found)}` : "No server directory or settings file required.", { path: found ? path.relative(projectRoot, found) : "" });',
|
|
445
|
-
'} catch (error) { check("settings_available", "blocked", error && error.message || error); }',
|
|
446
|
-
'let puppeteer = null;',
|
|
447
|
-
'try {',
|
|
448
|
-
' const candidates = [',
|
|
449
|
-
' path.join(projectRoot, "node_modules", "puppeteer"),',
|
|
450
|
-
' path.join(projectRoot, "server", "node_modules", "puppeteer"),',
|
|
451
|
-
' path.join(projectRoot, "node_modules", "puppeteer-core"),',
|
|
452
|
-
' path.join(projectRoot, "server", "node_modules", "puppeteer-core"),',
|
|
453
|
-
' "puppeteer",',
|
|
454
|
-
' "puppeteer-core"',
|
|
455
|
-
' ];',
|
|
456
|
-
' const found = requireFromCandidates(candidates, "puppeteer/puppeteer-core");',
|
|
457
|
-
' puppeteer = found.module;',
|
|
458
|
-
' check("puppeteer_require", "pass", `Loaded ${found.candidate}`);',
|
|
459
|
-
'} catch (error) { check("puppeteer_require", "blocked", error && error.message || error); }',
|
|
460
|
-
'let chromePath = String(process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_BIN || "").trim();',
|
|
461
|
-
'if (!chromePath) {',
|
|
462
|
-
' const candidates = [',
|
|
463
|
-
' "/var/lib/resolveio/puppeteer/chrome-headless-shell/linux-*/chrome-headless-shell-linux64/chrome-headless-shell",',
|
|
464
|
-
' "/var/lib/resolveio/puppeteer/chrome/linux-*/chrome-linux64/chrome",',
|
|
465
|
-
' "/usr/bin/google-chrome-stable",',
|
|
466
|
-
' "/usr/bin/google-chrome",',
|
|
467
|
-
' "/usr/bin/chromium",',
|
|
468
|
-
' "/usr/bin/chromium-browser"',
|
|
469
|
-
' ];',
|
|
470
|
-
' for (const pattern of candidates) {',
|
|
471
|
-
' const result = spawnSync("bash", ["-lc", `for f in ${pattern}; do [ -x "$f" ] && echo "$f" && exit 0; done; exit 1`], { encoding: "utf8", timeout: 3000 });',
|
|
472
|
-
' if (result.status === 0 && String(result.stdout || "").trim()) { chromePath = String(result.stdout || "").trim().split(/\\r?\\n/)[0]; break; }',
|
|
473
|
-
' }',
|
|
474
|
-
'}',
|
|
475
|
-
'check("chrome_executable", chromePath && fileExecutable(chromePath) ? "pass" : "blocked", chromePath ? `Chrome executable ${chromePath}` : "No executable Chrome/Chromium path found.", { path: chromePath });',
|
|
476
|
-
'if (puppeteer && chromePath && fileExecutable(chromePath) && String(process.env.RESOLVEIO_QA_PREFLIGHT_SKIP_BROWSER_LAUNCH || "").toLowerCase() !== "true") {',
|
|
477
|
-
' (async () => {',
|
|
478
|
-
' let browser = null;',
|
|
479
|
-
' try {',
|
|
480
|
-
' browser = await puppeteer.launch({ executablePath: chromePath, headless: "new", args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] });',
|
|
481
|
-
' check("chrome_launch", "pass", "Puppeteer launched the staged browser.");',
|
|
482
|
-
' } catch (error) {',
|
|
483
|
-
' check("chrome_launch", "blocked", error && error.message || error);',
|
|
484
|
-
' } finally {',
|
|
485
|
-
' if (browser) await browser.close().catch(() => undefined);',
|
|
486
|
-
' finish();',
|
|
487
|
-
' }',
|
|
488
|
-
' })().catch((error) => { check("chrome_launch", "blocked", error && error.message || error); finish(); });',
|
|
489
|
-
'}',
|
|
490
|
-
'else {',
|
|
491
|
-
' if (!puppeteer || !chromePath) check("chrome_launch", "skipped", "Browser launch skipped because Puppeteer or Chrome executable is unavailable.");',
|
|
492
|
-
' finish();',
|
|
493
|
-
'}',
|
|
494
|
-
'function finish() {',
|
|
495
|
-
' const failed = checks.filter((entry) => ["fail", "blocked"].includes(entry.status));',
|
|
496
|
-
' const result = { status: failed.length ? "blocked" : "pass", outcome: failed.length ? "infra_failed" : "infra_passed", projectRoot, checks, updatedAt: now() };',
|
|
497
|
-
' fs.writeFileSync(resultPath, JSON.stringify(result, null, 2));',
|
|
498
|
-
' console.log(JSON.stringify(result, null, 2));',
|
|
499
|
-
' process.exit(failed.length ? 2 : 0);',
|
|
500
|
-
'}',
|
|
501
|
-
''
|
|
502
|
-
].join('\n');
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
export function buildResolveIORunnerLocalQaScript(): string {
|
|
506
|
-
return [
|
|
507
|
-
'#!/usr/bin/env bash',
|
|
508
|
-
'set -u',
|
|
509
|
-
'TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"',
|
|
510
|
-
'source "$TOOLS_DIR/env.sh"',
|
|
511
|
-
'if resolveio_support_codex_lane_is_build 2>/dev/null; then',
|
|
512
|
-
' resolveio_support_codex_block_build_lane_qa',
|
|
513
|
-
' exit 86',
|
|
514
|
-
'fi',
|
|
515
|
-
'PROJECT_ROOT="${1:-$(pwd)}"',
|
|
516
|
-
'PROJECT_ROOT="$(cd "$PROJECT_ROOT" && pwd)"',
|
|
517
|
-
'ARTIFACT_DIR="$PROJECT_ROOT/qa-artifacts"',
|
|
518
|
-
'mkdir -p "$ARTIFACT_DIR"',
|
|
519
|
-
'CLIENT_URL="${RESOLVEIO_RUNNER_QA_CLIENT_URL:-${RESOLVEIO_SUPPORT_QA_CLIENT_URL:-http://localhost:${RESOLVEIO_RUNNER_QA_CLIENT_PORT:-${RESOLVEIO_SUPPORT_QA_CLIENT_PORT:-4200}}}}"',
|
|
520
|
-
'SERVER_URL="${RESOLVEIO_RUNNER_QA_SERVER_URL:-${RESOLVEIO_SUPPORT_QA_SERVER_URL:-http://localhost:8080}}"',
|
|
521
|
-
'CLIENT_PORT="${RESOLVEIO_RUNNER_QA_CLIENT_PORT:-${RESOLVEIO_SUPPORT_QA_CLIENT_PORT:-4200}}"',
|
|
522
|
-
'SERVER_PORT="${RESOLVEIO_RUNNER_QA_SERVER_PORT:-${RESOLVEIO_SUPPORT_QA_SERVER_PORT:-8080}}"',
|
|
523
|
-
'MONGO_PORT="${RESOLVEIO_RUNNER_QA_MONGO_PORT:-${RESOLVEIO_SUPPORT_QA_MONGO_PORT:-3001}}"',
|
|
524
|
-
'INSPECT_PORT="${RESOLVEIO_RUNNER_QA_INSPECT_PORT:-${RESOLVEIO_SUPPORT_QA_INSPECT_PORT:-9229}}"',
|
|
525
|
-
'STARTUP_TIMEOUT="${RESOLVEIO_RUNNER_QA_ANGULAR_STARTUP_TIMEOUT_SECONDS:-${RESOLVEIO_SUPPORT_QA_ANGULAR_STARTUP_TIMEOUT_SECONDS:-900}}"',
|
|
526
|
-
'ANGULAR_PREBUNDLE="${RESOLVEIO_RUNNER_QA_ANGULAR_PREBUNDLE:-${RESOLVEIO_SUPPORT_QA_ANGULAR_PREBUNDLE:-false}}"',
|
|
527
|
-
'REUSE_RUNNING="${RESOLVEIO_RUNNER_QA_REUSE_RUNNING:-${RESOLVEIO_SUPPORT_QA_REUSE_RUNNING:-true}}"',
|
|
528
|
-
'if [[ "$(basename "$TOOLS_DIR")" == *support* ]]; then',
|
|
529
|
-
' KEEPALIVE="${RESOLVEIO_SUPPORT_QA_KEEPALIVE:-${RESOLVEIO_RUNNER_QA_KEEPALIVE:-false}}"',
|
|
530
|
-
'else',
|
|
531
|
-
' KEEPALIVE="${RESOLVEIO_RUNNER_QA_KEEPALIVE:-${RESOLVEIO_SUPPORT_QA_KEEPALIVE:-false}}"',
|
|
532
|
-
'fi',
|
|
533
|
-
'case "${KEEPALIVE,,}" in',
|
|
534
|
-
' true|1|yes|on) exec >> "$ARTIFACT_DIR/runner.log" 2>&1 ;;',
|
|
535
|
-
'esac',
|
|
536
|
-
'SERVER_STABLE_SECONDS="${RESOLVEIO_RUNNER_QA_SERVER_STABLE_SECONDS:-${RESOLVEIO_SUPPORT_QA_SERVER_STABLE_SECONDS:-20}}"',
|
|
537
|
-
'LOCK_DIR="$ARTIFACT_DIR/.qa.lock"',
|
|
538
|
-
'SERVER_PID=""',
|
|
539
|
-
'CLIENT_PID=""',
|
|
540
|
-
'SERVER_REQUIRED=0',
|
|
541
|
-
'RUNNER_REUSED_READY=0',
|
|
542
|
-
'ANGULAR_PREBUNDLE_ARGS=()',
|
|
543
|
-
'REPO_ROOT="$(git -C "$PROJECT_ROOT" rev-parse --show-toplevel 2>/dev/null || echo "$PROJECT_ROOT")"',
|
|
544
|
-
buildRunnerProcessJanitorShellLibrary({ mode: 'support' }),
|
|
545
|
-
'case "${ANGULAR_PREBUNDLE,,}" in',
|
|
546
|
-
' false|0|no|off) ANGULAR_PREBUNDLE_ARGS=(--prebundle=false) ;;',
|
|
547
|
-
'esac',
|
|
548
|
-
'kill_tree() {',
|
|
549
|
-
' local pid="$1"',
|
|
550
|
-
' [ -n "$pid" ] || return 0',
|
|
551
|
-
' for child in $(pgrep -P "$pid" 2>/dev/null || true); do kill_tree "$child"; done',
|
|
552
|
-
' kill "$pid" >/dev/null 2>&1 || true',
|
|
553
|
-
' sleep 1',
|
|
554
|
-
' kill -0 "$pid" >/dev/null 2>&1 && kill -9 "$pid" >/dev/null 2>&1 || true',
|
|
555
|
-
'}',
|
|
556
|
-
'kill_port_listeners() {',
|
|
557
|
-
' local port="$1"',
|
|
558
|
-
' [ -n "$port" ] || return 0',
|
|
559
|
-
' local pids=""',
|
|
560
|
-
' if command -v lsof >/dev/null 2>&1; then pids="$pids $(janitor_bounded 3 lsof -ti tcp:"$port")"; fi',
|
|
561
|
-
' if command -v fuser >/dev/null 2>&1; then pids="$pids $(janitor_bounded 3 fuser -n tcp "$port")"; fi',
|
|
562
|
-
' if command -v ss >/dev/null 2>&1; then pids="$pids $(janitor_bounded 3 ss -ltnp "sport = :$port" | sed -n \'s/.*pid=\\([0-9][0-9]*\\).*/\\1/p\' || true)"; fi',
|
|
563
|
-
' for pid in $pids; do',
|
|
564
|
-
' [ "$pid" = "$$" ] && continue',
|
|
565
|
-
' kill_tree "$pid"',
|
|
566
|
-
' done',
|
|
567
|
-
' sleep 1',
|
|
568
|
-
'}',
|
|
569
|
-
'kill_env_marked_processes() {',
|
|
570
|
-
' [ -d /proc ] || return 0',
|
|
571
|
-
' local job_id="${RESOLVEIO_RUNNER_QA_JOB_ID:-${RESOLVEIO_SUPPORT_QA_JOB_ID:-}}"',
|
|
572
|
-
' local owner_id="${RESOLVEIO_RUNNER_QA_OWNER_ID:-${RESOLVEIO_SUPPORT_QA_OWNER_ID:-}}"',
|
|
573
|
-
' local token="${RESOLVEIO_RUNNER_QA_RUNNER_TOKEN:-${RESOLVEIO_SUPPORT_QA_RUNNER_TOKEN:-}}"',
|
|
574
|
-
' [ -n "$job_id$owner_id$token" ] || return 0',
|
|
575
|
-
' for env_file in /proc/[0-9]*/environ; do',
|
|
576
|
-
' pid="${env_file#/proc/}"',
|
|
577
|
-
' pid="${pid%/environ}"',
|
|
578
|
-
' skip_cleanup_pid "$pid" && continue',
|
|
579
|
-
' [ "$pid" = "$$" ] && continue',
|
|
580
|
-
' [ -r "$env_file" ] || continue',
|
|
581
|
-
' env_text="$(tr "\\0" "\\n" < "$env_file" 2>/dev/null || true)"',
|
|
582
|
-
' [ -n "$env_text" ] || continue',
|
|
583
|
-
' matched=0',
|
|
584
|
-
' [ -n "$job_id" ] && echo "$env_text" | grep -Eq "^(RESOLVEIO_RUNNER_QA_JOB_ID|RESOLVEIO_SUPPORT_QA_JOB_ID)=$job_id$" && matched=1',
|
|
585
|
-
' [ "$matched" = "0" ] && [ -n "$owner_id" ] && echo "$env_text" | grep -Eq "^(RESOLVEIO_RUNNER_QA_OWNER_ID|RESOLVEIO_SUPPORT_QA_OWNER_ID)=$owner_id$" && matched=1',
|
|
586
|
-
' [ "$matched" = "0" ] && [ -n "$token" ] && echo "$env_text" | grep -Eq "^(RESOLVEIO_RUNNER_QA_RUNNER_TOKEN|RESOLVEIO_SUPPORT_QA_RUNNER_TOKEN)=$token$" && matched=1',
|
|
587
|
-
' [ "$matched" = "1" ] && kill_tree "$pid"',
|
|
588
|
-
' done',
|
|
589
|
-
'}',
|
|
590
|
-
'kill_artifact_log_writers() {',
|
|
591
|
-
' local artifact="$ARTIFACT_DIR"',
|
|
592
|
-
' [ -n "$artifact" ] || return 0',
|
|
593
|
-
' for pid in $(ps -eo pid=,args= | awk -v artifact="$artifact" \'index($0, artifact) && $0 !~ /awk -v artifact=/ && $0 !~ /ps -eo pid=,args=/ && $0 ~ /(^|[ /])(tee|tail)( |$)/ {print $1}\' 2>/dev/null || true); do',
|
|
594
|
-
' skip_cleanup_pid "$pid" && continue',
|
|
595
|
-
' kill_tree "$pid"',
|
|
596
|
-
' done',
|
|
597
|
-
'}',
|
|
598
|
-
'kill_local_mongo_processes() {',
|
|
599
|
-
' local mongo_port="$MONGO_PORT"',
|
|
600
|
-
' local root="$PROJECT_ROOT"',
|
|
601
|
-
' [ -n "$mongo_port$root" ] || return 0',
|
|
602
|
-
' for pid in $(ps -eo pid=,args= | awk -v port="$mongo_port" -v root="$root" \'$0 !~ /awk -v port=/ && $0 !~ /ps -eo pid=,args=/ && $0 ~ /mongod/ && (($0 ~ ("--port " port)) || index($0, root "/server/mongo/data/db") || index($0, root "/mongo/data/db")) && ($0 ~ /\\/tmp\\/resolveio-support-qa\\/.*mongod/ || index($0, root) || $0 ~ /--dbpath .*mongo\\/data\\/db/) {print $1}\' 2>/dev/null || true); do',
|
|
603
|
-
' skip_cleanup_pid "$pid" && continue',
|
|
604
|
-
' [ "$pid" = "$$" ] && continue',
|
|
605
|
-
' kill_tree "$pid"',
|
|
606
|
-
' sleep 1',
|
|
607
|
-
' kill -0 "$pid" >/dev/null 2>&1 && kill -9 "$pid" >/dev/null 2>&1 || true',
|
|
608
|
-
' done',
|
|
609
|
-
'}',
|
|
610
|
-
'RUNNER_ANCESTOR_PIDS=" $$ ${PPID:-} "',
|
|
611
|
-
'ancestor_pid="${PPID:-}"',
|
|
612
|
-
'while [ -n "$ancestor_pid" ] && [ "$ancestor_pid" != "0" ] && [ "$ancestor_pid" != "1" ]; do',
|
|
613
|
-
' ancestor_pid="$(ps -o ppid= -p "$ancestor_pid" 2>/dev/null | tr -d " " || true)"',
|
|
614
|
-
' [ -n "$ancestor_pid" ] && RUNNER_ANCESTOR_PIDS="$RUNNER_ANCESTOR_PIDS $ancestor_pid "',
|
|
615
|
-
'done',
|
|
616
|
-
'skip_cleanup_pid() {',
|
|
617
|
-
' case "$RUNNER_ANCESTOR_PIDS" in *" $1 "*) return 0 ;; esac',
|
|
618
|
-
' return 1',
|
|
619
|
-
'}',
|
|
620
|
-
'cleanup_project_processes() {',
|
|
621
|
-
' for pid_file in "$ARTIFACT_DIR/server.pid" "$ARTIFACT_DIR/client.pid"; do',
|
|
622
|
-
' [ -f "$pid_file" ] || continue',
|
|
623
|
-
' pid="$(cat "$pid_file" 2>/dev/null || true)"',
|
|
624
|
-
' kill_tree "$pid"',
|
|
625
|
-
' rm -f "$pid_file"',
|
|
626
|
-
' done',
|
|
627
|
-
' for pass in 1 2 3; do',
|
|
628
|
-
' for port in "$CLIENT_PORT" "$SERVER_PORT" "$MONGO_PORT" "$INSPECT_PORT"; do kill_port_listeners "$port"; done',
|
|
629
|
-
' kill_env_marked_processes',
|
|
630
|
-
' kill_artifact_log_writers',
|
|
631
|
-
' kill_local_mongo_processes',
|
|
632
|
-
' for pid in $(ps -eo pid=,args= | awk -v root="$PROJECT_ROOT" \'index($0, root) && $0 !~ /awk -v root=/ && $0 !~ /ps -eo pid=,args=/ && $0 !~ /run-local-qa\\.sh|stop-local-qa\\.sh|bugfix-comparison-qa\\.sh/ && $0 ~ /(ng serve|node .*node_modules\\/\\.bin\\/ng[[:space:]]+serve|esbuild --service|npm run client|start_client\\.sh|npm run server|start_server\\.sh|nodemon|node .*tmp\\/index\\.js|mongod|mongodb-binaries\\/mongod)/ {print $1}\' 2>/dev/null || true); do',
|
|
633
|
-
' skip_cleanup_pid "$pid" && continue',
|
|
634
|
-
' kill_tree "$pid"',
|
|
635
|
-
' done',
|
|
636
|
-
' # Do not kill arbitrary processes by project cwd; completion builds run',
|
|
637
|
-
' # from the same cwd. Cleanup is limited to known QA process signatures,',
|
|
638
|
-
' # recorded PIDs, scoped ports, and QA runner env markers.',
|
|
639
|
-
' sleep 1',
|
|
640
|
-
' done',
|
|
641
|
-
' local wait_until=$((SECONDS + 60))',
|
|
642
|
-
' while [ "$SECONDS" -lt "$wait_until" ]; do',
|
|
643
|
-
' local found=0',
|
|
644
|
-
' for port in "$CLIENT_PORT" "$SERVER_PORT" "$MONGO_PORT" "$INSPECT_PORT"; do',
|
|
645
|
-
' port_pids=""',
|
|
646
|
-
' if command -v lsof >/dev/null 2>&1; then port_pids="$port_pids $(janitor_bounded 3 lsof -ti tcp:"$port")"; fi',
|
|
647
|
-
' for pid in $port_pids; do skip_cleanup_pid "$pid" && continue; found=1; kill_tree "$pid"; done',
|
|
648
|
-
' done',
|
|
649
|
-
' if [ -d /proc ]; then',
|
|
650
|
-
' job_id="${RESOLVEIO_RUNNER_QA_JOB_ID:-${RESOLVEIO_SUPPORT_QA_JOB_ID:-}}"',
|
|
651
|
-
' owner_id="${RESOLVEIO_RUNNER_QA_OWNER_ID:-${RESOLVEIO_SUPPORT_QA_OWNER_ID:-}}"',
|
|
652
|
-
' token="${RESOLVEIO_RUNNER_QA_RUNNER_TOKEN:-${RESOLVEIO_SUPPORT_QA_RUNNER_TOKEN:-}}"',
|
|
653
|
-
' for env_file in /proc/[0-9]*/environ; do',
|
|
654
|
-
' pid="${env_file#/proc/}"',
|
|
655
|
-
' pid="${pid%/environ}"',
|
|
656
|
-
' skip_cleanup_pid "$pid" && continue',
|
|
657
|
-
' [ -r "$env_file" ] || continue',
|
|
658
|
-
' env_text="$(tr "\\0" "\\n" < "$env_file" 2>/dev/null || true)"',
|
|
659
|
-
' matched=0',
|
|
660
|
-
' [ -n "$job_id" ] && echo "$env_text" | grep -Eq "^(RESOLVEIO_RUNNER_QA_JOB_ID|RESOLVEIO_SUPPORT_QA_JOB_ID)=$job_id$" && matched=1',
|
|
661
|
-
' [ "$matched" = "0" ] && [ -n "$owner_id" ] && echo "$env_text" | grep -Eq "^(RESOLVEIO_RUNNER_QA_OWNER_ID|RESOLVEIO_SUPPORT_QA_OWNER_ID)=$owner_id$" && matched=1',
|
|
662
|
-
' [ "$matched" = "0" ] && [ -n "$token" ] && echo "$env_text" | grep -Eq "^(RESOLVEIO_RUNNER_QA_RUNNER_TOKEN|RESOLVEIO_SUPPORT_QA_RUNNER_TOKEN)=$token$" && matched=1',
|
|
663
|
-
' [ "$matched" = "1" ] && found=1 && kill_tree "$pid"',
|
|
664
|
-
' done',
|
|
665
|
-
' fi',
|
|
666
|
-
' kill_artifact_log_writers',
|
|
667
|
-
' kill_local_mongo_processes',
|
|
668
|
-
' for pid in $(ps -eo pid=,args= | awk -v root="$PROJECT_ROOT" \'index($0, root) && $0 !~ /awk -v root=/ && $0 !~ /ps -eo pid=,args=/ && $0 !~ /run-local-qa\\.sh|stop-local-qa\\.sh|bugfix-comparison-qa\\.sh/ && $0 ~ /(ng serve|node .*node_modules\\/\\.bin\\/ng[[:space:]]+serve|esbuild --service|npm run client|start_client\\.sh|npm run server|start_server\\.sh|nodemon|node .*tmp\\/index\\.js|mongod|mongodb-binaries\\/mongod)/ {print $1}\' 2>/dev/null || true); do',
|
|
669
|
-
' skip_cleanup_pid "$pid" && continue',
|
|
670
|
-
' found=1',
|
|
671
|
-
' done',
|
|
672
|
-
' [ "$found" = "0" ] && break',
|
|
673
|
-
' sleep 2',
|
|
674
|
-
' done',
|
|
675
|
-
'}',
|
|
676
|
-
'cleanup() {',
|
|
677
|
-
' [ "${RUNNER_REUSED_READY:-0}" = "1" ] && return 0',
|
|
678
|
-
' janitor_stop_heartbeat',
|
|
679
|
-
' rm -f "$ARTIFACT_DIR/heartbeat.pid"',
|
|
680
|
-
' janitor_update_manifest_status cleanup_started',
|
|
681
|
-
' kill_tree "$SERVER_PID"',
|
|
682
|
-
' kill_tree "$CLIENT_PID"',
|
|
683
|
-
' cleanup_project_processes',
|
|
684
|
-
' janitor_update_manifest_status cleanup_complete',
|
|
685
|
-
' janitor_write_cleanup_status complete 0',
|
|
686
|
-
' rm -rf "$LOCK_DIR" >/dev/null 2>&1 || true',
|
|
687
|
-
'}',
|
|
688
|
-
'trap cleanup EXIT',
|
|
689
|
-
'detach_keepalive() {',
|
|
690
|
-
' if ! probe_url "$CLIENT_URL"; then',
|
|
691
|
-
' echo "ResolveIO AI runner QA keepalive refused: client URL is no longer reachable at $CLIENT_URL." | tee -a "$ARTIFACT_DIR/runner.log"',
|
|
692
|
-
' return 1',
|
|
693
|
-
' fi',
|
|
694
|
-
' if [ "${SERVER_REQUIRED:-0}" = "1" ] && ! probe_url "$SERVER_URL"; then',
|
|
695
|
-
' echo "ResolveIO AI runner QA keepalive refused: server URL is no longer reachable at $SERVER_URL." | tee -a "$ARTIFACT_DIR/runner.log"',
|
|
696
|
-
' return 4',
|
|
697
|
-
' fi',
|
|
698
|
-
' janitor_update_manifest_status ready_keepalive',
|
|
699
|
-
' janitor_write_cleanup_status ready_keepalive 0',
|
|
700
|
-
' rm -rf "$LOCK_DIR" >/dev/null 2>&1 || true',
|
|
701
|
-
' trap - EXIT',
|
|
702
|
-
' disown >/dev/null 2>&1 || true',
|
|
703
|
-
' echo "ResolveIO AI runner QA keepalive detached at $CLIENT_URL; stop with $TOOLS_DIR/stop-local-qa.sh $PROJECT_ROOT."',
|
|
704
|
-
' exit 0',
|
|
705
|
-
'}',
|
|
706
|
-
'probe_url() { node - "$1" <<\'RESOLVEIO_PROBE\'',
|
|
707
|
-
'const http = require("http");',
|
|
708
|
-
'const https = require("https");',
|
|
709
|
-
'const url = process.argv[2];',
|
|
710
|
-
'const mod = /^https:/i.test(url) ? https : http;',
|
|
711
|
-
'const req = mod.get(url, { timeout: 2500 }, (res) => { res.resume(); process.exit(res.statusCode && res.statusCode < 500 ? 0 : 1); });',
|
|
712
|
-
'req.on("timeout", () => req.destroy(new Error("timeout")));',
|
|
713
|
-
'req.on("error", () => process.exit(1));',
|
|
714
|
-
'RESOLVEIO_PROBE',
|
|
715
|
-
'}',
|
|
716
|
-
'truthy() { case "${1,,}" in true|1|yes|on) return 0 ;; *) return 1 ;; esac; }',
|
|
717
|
-
'reuse_running_app_if_ready() {',
|
|
718
|
-
' truthy "$REUSE_RUNNING" || return 1',
|
|
719
|
-
' [ -d "$PROJECT_ROOT/server" ] && SERVER_REQUIRED=1 || SERVER_REQUIRED=0',
|
|
720
|
-
' probe_url "$CLIENT_URL" || return 1',
|
|
721
|
-
' if [ "$SERVER_REQUIRED" = "1" ] && ! probe_url "$SERVER_URL"; then return 1; fi',
|
|
722
|
-
' echo "ResolveIO AI runner QA reusing already-ready local app at $CLIENT_URL"',
|
|
723
|
-
' return 0',
|
|
724
|
-
'}',
|
|
725
|
-
'log_has_fatal() {',
|
|
726
|
-
' local file="$1"',
|
|
727
|
-
' [ -f "$file" ] || return 1',
|
|
728
|
-
' grep -Eiq "Unhandled Rejection|Cannot read properties of undefined|TypeError|ReferenceError|EADDRINUSE|app crashed|Failed to compile|Compilation failed|TypeScript:.*(semantic errors|Compilation failed)|error TS[0-9]{4}|NG[0-9]{4}|TS[0-9]{4}|Error: Cannot find module" "$file"',
|
|
729
|
-
'}',
|
|
730
|
-
'prepare_angular_cache_dirs() {',
|
|
731
|
-
' [ -d "$PROJECT_ROOT" ] || return 0',
|
|
732
|
-
' mkdir -p "$PROJECT_ROOT/.angular/cache" 2>/dev/null || true',
|
|
733
|
-
' local version=""',
|
|
734
|
-
' for pkg in "$PROJECT_ROOT/node_modules/@angular/build/package.json" "$PROJECT_ROOT/node_modules/@angular/cli/package.json" "$PROJECT_ROOT/node_modules/@angular-devkit/build-angular/package.json"; do',
|
|
735
|
-
' if [ -f "$pkg" ]; then',
|
|
736
|
-
' version="$(node -e "try{console.log(require(process.argv[1]).version||\\\"\\\")}catch(e){}" "$pkg" 2>/dev/null | head -1)"',
|
|
737
|
-
' [ -n "$version" ] && break',
|
|
738
|
-
' fi',
|
|
739
|
-
' done',
|
|
740
|
-
' [ -n "$version" ] || return 0',
|
|
741
|
-
' node - "$PROJECT_ROOT/angular.json" "$(basename "$PROJECT_ROOT")" <<\'RESOLVEIO_ANGULAR_CACHE_PROJECTS\' | while IFS= read -r project; do',
|
|
742
|
-
'const fs = require("fs");',
|
|
743
|
-
'const path = process.argv[2];',
|
|
744
|
-
'const fallback = process.argv[3];',
|
|
745
|
-
'const names = new Set([fallback].filter(Boolean));',
|
|
746
|
-
'try {',
|
|
747
|
-
' const parsed = JSON.parse(fs.readFileSync(path, "utf8"));',
|
|
748
|
-
' if (parsed.defaultProject) names.add(String(parsed.defaultProject));',
|
|
749
|
-
' if (parsed.projects && typeof parsed.projects === "object") Object.keys(parsed.projects).forEach((name) => names.add(String(name)));',
|
|
750
|
-
'} catch (error) {}',
|
|
751
|
-
'for (const name of names) console.log(name.replace(/[^A-Za-z0-9._-]/g, "_"));',
|
|
752
|
-
'RESOLVEIO_ANGULAR_CACHE_PROJECTS',
|
|
753
|
-
' [ -n "$project" ] || continue',
|
|
754
|
-
' mkdir -p "$PROJECT_ROOT/.angular/cache/$version/$project/vite" 2>/dev/null || true',
|
|
755
|
-
' done',
|
|
756
|
-
'}',
|
|
757
|
-
'server_has_started() {',
|
|
758
|
-
' local file="$ARTIFACT_DIR/server.log"',
|
|
759
|
-
' [ -f "$file" ] || return 1',
|
|
760
|
-
' grep -Eiq "nodemon.*starting|node .*tmp/index\\.js|Running as Worker|Standalone Node Reaper|Running HTTP/WS server on port|listening on|server listening|Server listening|app listening|App listening|Finished .default." "$file"',
|
|
761
|
-
'}',
|
|
762
|
-
'wait_for_server_ready() {',
|
|
763
|
-
' [ "$SERVER_REQUIRED" = "1" ] || return 0',
|
|
764
|
-
' local end=$((SECONDS + STARTUP_TIMEOUT))',
|
|
765
|
-
' while [ "$SECONDS" -lt "$end" ]; do',
|
|
766
|
-
' if log_has_fatal "$ARTIFACT_DIR/server.log"; then return 4; fi',
|
|
767
|
-
' if server_has_started; then',
|
|
768
|
-
' local stable_until=$((SECONDS + SERVER_STABLE_SECONDS))',
|
|
769
|
-
' while [ "$SECONDS" -lt "$stable_until" ]; do if log_has_fatal "$ARTIFACT_DIR/server.log"; then return 4; fi; sleep 2; done',
|
|
770
|
-
' return 0',
|
|
771
|
-
' fi',
|
|
772
|
-
' if probe_url "$SERVER_URL"; then',
|
|
773
|
-
' local stable_until=$((SECONDS + SERVER_STABLE_SECONDS))',
|
|
774
|
-
' while [ "$SECONDS" -lt "$stable_until" ]; do if log_has_fatal "$ARTIFACT_DIR/server.log"; then return 4; fi; sleep 2; done',
|
|
775
|
-
' return 0',
|
|
776
|
-
' fi',
|
|
777
|
-
' sleep 5',
|
|
778
|
-
' done',
|
|
779
|
-
' return 4',
|
|
780
|
-
'}',
|
|
781
|
-
'wait_for_client() {',
|
|
782
|
-
' local end=$((SECONDS + STARTUP_TIMEOUT))',
|
|
783
|
-
' while [ "$SECONDS" -lt "$end" ]; do',
|
|
784
|
-
' if [ -n "$CLIENT_PID" ] && ! kill -0 "$CLIENT_PID" >/dev/null 2>&1; then return 2; fi',
|
|
785
|
-
' if log_has_fatal "$ARTIFACT_DIR/client.log"; then return 3; fi',
|
|
786
|
-
' if log_has_fatal "$ARTIFACT_DIR/server.log"; then return 4; fi',
|
|
787
|
-
' if probe_url "$CLIENT_URL"; then wait_for_server_ready || return $?; return 0; fi',
|
|
788
|
-
' sleep 5',
|
|
789
|
-
' done',
|
|
790
|
-
' return 1',
|
|
791
|
-
'}',
|
|
792
|
-
'mongo_tcp_ready() {',
|
|
793
|
-
' node - "$MONGO_PORT" <<\'RESOLVEIO_QA_MONGO_TCP\'',
|
|
794
|
-
'const net = require("net");',
|
|
795
|
-
'const port = Number(process.argv[2] || 3001);',
|
|
796
|
-
'const socket = net.createConnection({ host: "127.0.0.1", port, timeout: 1200 });',
|
|
797
|
-
'socket.once("connect", () => { socket.destroy(); process.exit(0); });',
|
|
798
|
-
'socket.once("timeout", () => { socket.destroy(); process.exit(1); });',
|
|
799
|
-
'socket.once("error", () => process.exit(1));',
|
|
800
|
-
'RESOLVEIO_QA_MONGO_TCP',
|
|
801
|
-
'}',
|
|
802
|
-
'mongo_primary_ready() {',
|
|
803
|
-
' mongo_tcp_ready >/dev/null 2>&1 || return 1',
|
|
804
|
-
' node - "$PROJECT_ROOT" "$REPO_ROOT" "$MONGO_PORT" <<\'RESOLVEIO_QA_MONGO_PRIMARY\'',
|
|
805
|
-
'const path = require("path");',
|
|
806
|
-
'const [projectRoot, repoRoot, portRaw] = process.argv.slice(2);',
|
|
807
|
-
'function requireMongo() {',
|
|
808
|
-
' const candidates = [',
|
|
809
|
-
' path.join(projectRoot || "", "server", "node_modules", "mongodb"),',
|
|
810
|
-
' path.join(projectRoot || "", "node_modules", "mongodb"),',
|
|
811
|
-
' path.join(repoRoot || "", "node_modules", "mongodb"),',
|
|
812
|
-
' "mongodb"',
|
|
813
|
-
' ];',
|
|
814
|
-
' for (const candidate of candidates) {',
|
|
815
|
-
' try { return require(candidate); } catch (error) {}',
|
|
816
|
-
' }',
|
|
817
|
-
' throw new Error("Unable to require mongodb for local QA primary readiness.");',
|
|
818
|
-
'}',
|
|
819
|
-
'(async () => {',
|
|
820
|
-
' const port = Number(portRaw || 3001);',
|
|
821
|
-
' const uri = `mongodb://127.0.0.1:${port}/admin?directConnection=true`;',
|
|
822
|
-
' const { MongoClient } = requireMongo();',
|
|
823
|
-
' const client = new MongoClient(uri, { serverSelectionTimeoutMS: 2500 });',
|
|
824
|
-
' await client.connect();',
|
|
825
|
-
' try {',
|
|
826
|
-
' const admin = client.db("admin");',
|
|
827
|
-
' try {',
|
|
828
|
-
' await admin.command({ replSetInitiate: { _id: "rs0", members: [{ _id: 0, host: `127.0.0.1:${port}` }] } });',
|
|
829
|
-
' } catch (error) {',
|
|
830
|
-
' const message = String(error && error.message || error);',
|
|
831
|
-
' if (!/already initialized|already.*initiated|InvalidReplicaSetConfig|already exists/i.test(message)) throw error;',
|
|
832
|
-
' }',
|
|
833
|
-
' let hello = null;',
|
|
834
|
-
' try { hello = await admin.command({ hello: 1 }); } catch (error) {',
|
|
835
|
-
' try { hello = await admin.command({ isMaster: 1 }); } catch (inner) { hello = null; }',
|
|
836
|
-
' }',
|
|
837
|
-
' const writable = !hello || hello.isWritablePrimary === true || hello.ismaster === true || !hello.setName;',
|
|
838
|
-
' process.exit(writable ? 0 : 1);',
|
|
839
|
-
' } finally {',
|
|
840
|
-
' await client.close().catch(() => undefined);',
|
|
841
|
-
' }',
|
|
842
|
-
'})().catch((error) => { console.error(error && error.stack || error); process.exit(1); });',
|
|
843
|
-
'RESOLVEIO_QA_MONGO_PRIMARY',
|
|
844
|
-
'}',
|
|
845
|
-
'start_local_mongo_for_prestart_seed() {',
|
|
846
|
-
' if mongo_primary_ready >/dev/null 2>&1; then return 0; fi',
|
|
847
|
-
' if command -v mongod >/dev/null 2>&1; then',
|
|
848
|
-
' mkdir -p "$PROJECT_ROOT/server/mongo/data/db" "$PROJECT_ROOT/server/mongo/log"',
|
|
849
|
-
' mongod --fork --dbpath "$PROJECT_ROOT/server/mongo/data/db" --logpath "$PROJECT_ROOT/server/mongo/log/mongo.log" --port "$MONGO_PORT" --replSet rs0 --bind_ip 127.0.0.1 >/dev/null 2>&1 || true',
|
|
850
|
-
' fi',
|
|
851
|
-
' local end=$((SECONDS + 45))',
|
|
852
|
-
' while [ "$SECONDS" -lt "$end" ]; do',
|
|
853
|
-
' if mongo_tcp_ready >/dev/null 2>&1; then break; fi',
|
|
854
|
-
' sleep 2',
|
|
855
|
-
' done',
|
|
856
|
-
' if ! mongo_tcp_ready >/dev/null 2>&1; then return 1; fi',
|
|
857
|
-
' if command -v mongosh >/dev/null 2>&1; then',
|
|
858
|
-
' timeout 8s mongosh "mongodb://127.0.0.1:$MONGO_PORT/admin?directConnection=true" --quiet --eval "try { rs.initiate({_id: \'rs0\', members: [{ _id: 0, host: \'127.0.0.1:$MONGO_PORT\' }]}) } catch (e) {}" >/dev/null 2>&1 || true',
|
|
859
|
-
' fi',
|
|
860
|
-
' local primary_end=$((SECONDS + 75))',
|
|
861
|
-
' while [ "$SECONDS" -lt "$primary_end" ]; do',
|
|
862
|
-
' if mongo_primary_ready >/dev/null 2>&1; then return 0; fi',
|
|
863
|
-
' if command -v mongosh >/dev/null 2>&1; then',
|
|
864
|
-
' timeout 6s mongosh "mongodb://127.0.0.1:$MONGO_PORT/admin?directConnection=true" --quiet --eval "try { rs.initiate({_id: \'rs0\', members: [{ _id: 0, host: \'127.0.0.1:$MONGO_PORT\' }]}) } catch (e) {}" >/dev/null 2>&1 || true',
|
|
865
|
-
' fi',
|
|
866
|
-
' sleep 2',
|
|
867
|
-
' done',
|
|
868
|
-
' mongo_primary_ready >/dev/null 2>&1',
|
|
869
|
-
'}',
|
|
870
|
-
'materialize_live_mongo_runtime() {',
|
|
871
|
-
' local runtime_file="$ARTIFACT_DIR/.mongo-runtime.json"',
|
|
872
|
-
' node - "$runtime_file" "$PROJECT_ROOT" "$REPO_ROOT" <<\'RESOLVEIO_QA_MONGO_RUNTIME\'',
|
|
873
|
-
'const fs = require("fs");',
|
|
874
|
-
'const path = require("path");',
|
|
875
|
-
'const [runtimeFile, projectRootArg, repoRootArg] = process.argv.slice(2);',
|
|
876
|
-
'function parseEnv(filePath) {',
|
|
877
|
-
' const out = {};',
|
|
878
|
-
' try {',
|
|
879
|
-
' for (const raw of fs.readFileSync(filePath, "utf8").split(/\\n/)) {',
|
|
880
|
-
' const line = raw.trim();',
|
|
881
|
-
' if (!line || line.startsWith("#")) continue;',
|
|
882
|
-
' if (line.includes("=")) {',
|
|
883
|
-
' const index = line.indexOf("=");',
|
|
884
|
-
' out[line.slice(0, index).trim().replace(/^[\\\'"]|[\\\'"]$/g, "")] = line.slice(index + 1).trim().replace(/^[\\\'"]|[\\\'"],?$/g, "");',
|
|
885
|
-
' continue;',
|
|
886
|
-
' }',
|
|
887
|
-
' const jsonish = line.match(/^[\\\'"]?([A-Z0-9_]+)[\\\'"]?\\s*:\\s*[\\\'"]?(.+?)[\\\'"]?,?$/i);',
|
|
888
|
-
' if (jsonish) out[jsonish[1]] = jsonish[2].trim();',
|
|
889
|
-
' }',
|
|
890
|
-
' } catch (error) {}',
|
|
891
|
-
' return out;',
|
|
892
|
-
'}',
|
|
893
|
-
'function readJson(filePath) { try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch (error) { return {}; } }',
|
|
894
|
-
'function pick(obj, keys) { for (const key of keys) { if (obj && typeof obj[key] === "string" && obj[key].trim()) return obj[key].trim(); } return ""; }',
|
|
895
|
-
'function isLocalMongo(uri) { return /mongodb(?:\\+srv)?:\\/\\/(?:[^@/]+@)?(?:127\\.0\\.0\\.1|localhost)(?::\\d+)?\\//i.test(String(uri || "")); }',
|
|
896
|
-
'function unique(values) { return Array.from(new Set(values.filter(Boolean).map((value) => path.resolve(String(value))))); }',
|
|
897
|
-
'const appName = projectRootArg ? path.basename(projectRootArg) : "";',
|
|
898
|
-
'const runtimeFileCandidates = unique([',
|
|
899
|
-
' process.env.RESOLVEIO_QA_MONGO_RUNTIME_PATH,',
|
|
900
|
-
' process.env.RESOLVEIO_SUPPORT_QA_MONGO_RUNTIME_PATH,',
|
|
901
|
-
' process.env.RESOLVEIO_RUNNER_QA_MONGO_RUNTIME_PATH,',
|
|
902
|
-
' repoRootArg && path.join(repoRootArg, "mongo-context", ".mongo-runtime.json"),',
|
|
903
|
-
' projectRootArg && path.join(projectRootArg, "mongo-context", ".mongo-runtime.json"),',
|
|
904
|
-
' repoRootArg && path.join(repoRootArg, ".resolveio-support-context", "mongo-context", ".mongo-runtime.json"),',
|
|
905
|
-
' projectRootArg && path.join(projectRootArg, ".resolveio-support-context", "mongo-context", ".mongo-runtime.json"),',
|
|
906
|
-
' repoRootArg && path.join(repoRootArg, ".resolveio-context", "mongo-context", ".mongo-runtime.json"),',
|
|
907
|
-
' projectRootArg && path.join(projectRootArg, ".resolveio-context", "mongo-context", ".mongo-runtime.json")',
|
|
908
|
-
']);',
|
|
909
|
-
'for (const candidate of runtimeFileCandidates) {',
|
|
910
|
-
' const parsed = readJson(candidate);',
|
|
911
|
-
' const uri = pick(parsed, ["mongo_uri", "MONGO_URL"]);',
|
|
912
|
-
' const database = pick(parsed, ["mongo_db", "DATABASE", "database"]);',
|
|
913
|
-
' if (uri && !isLocalMongo(uri)) {',
|
|
914
|
-
' fs.mkdirSync(path.dirname(runtimeFile), { recursive: true });',
|
|
915
|
-
' fs.writeFileSync(runtimeFile, JSON.stringify({ mongo_uri: uri, mongo_db: database || "" }, null, 2));',
|
|
916
|
-
' process.exit(0);',
|
|
917
|
-
' }',
|
|
918
|
-
'}',
|
|
919
|
-
'const runtimeRoots = unique([',
|
|
920
|
-
' process.env.RESOLVEIO_QA_RUNTIME_ROOT,',
|
|
921
|
-
' process.env.RESOLVEIO_SUPPORT_QA_RUNTIME_ROOT,',
|
|
922
|
-
' projectRootArg && path.join(projectRootArg, "server"),',
|
|
923
|
-
' projectRootArg,',
|
|
924
|
-
' repoRootArg,',
|
|
925
|
-
' appName && path.join("/var/app/current", appName, "server"),',
|
|
926
|
-
' appName && path.join("/var/app/current", appName),',
|
|
927
|
-
' appName === "resolveio" && "/var/app/current"',
|
|
928
|
-
']);',
|
|
929
|
-
'for (const root of runtimeRoots) {',
|
|
930
|
-
' const env = Object.assign({}, parseEnv(path.join(root, ".env")), parseEnv(path.join(root, ".env.production")), parseEnv(path.join(root, ".env.runtime")), parseEnv(path.join(root, ".env.codex")));',
|
|
931
|
-
' const settings = Object.assign({}, readJson(path.join(root, "settings.json")), readJson(path.join(root, "settings.local.json")));',
|
|
932
|
-
' const uri = pick(env, ["MONGO_URL"]) || pick(settings, ["MONGO_URL", "mongo_url"]);',
|
|
933
|
-
' const database = pick(env, ["DATABASE"]) || pick(settings, ["DATABASE", "database"]);',
|
|
934
|
-
' if (uri && !isLocalMongo(uri)) {',
|
|
935
|
-
' fs.mkdirSync(path.dirname(runtimeFile), { recursive: true });',
|
|
936
|
-
' fs.writeFileSync(runtimeFile, JSON.stringify({ mongo_uri: uri, mongo_db: database || "" }, null, 2));',
|
|
937
|
-
' process.exit(0);',
|
|
938
|
-
' }',
|
|
939
|
-
'}',
|
|
940
|
-
'const fallbackUri = pick(process.env, ["RESOLVEIO_QA_LIVE_MONGO_URL", "RESOLVEIO_SUPPORT_QA_LIVE_MONGO_URL", "RESOLVEIO_RUNNER_QA_LIVE_MONGO_URL"]);',
|
|
941
|
-
'if (fallbackUri && !isLocalMongo(fallbackUri)) {',
|
|
942
|
-
' const fallbackDatabase = pick(process.env, ["RESOLVEIO_QA_LIVE_MONGO_DB", "RESOLVEIO_SUPPORT_QA_LIVE_MONGO_DB", "RESOLVEIO_RUNNER_QA_LIVE_MONGO_DB"]);',
|
|
943
|
-
' fs.mkdirSync(path.dirname(runtimeFile), { recursive: true });',
|
|
944
|
-
' fs.writeFileSync(runtimeFile, JSON.stringify({ mongo_uri: fallbackUri, mongo_db: fallbackDatabase || "" }, null, 2));',
|
|
945
|
-
' process.exit(0);',
|
|
946
|
-
'}',
|
|
947
|
-
'process.exit(0);',
|
|
948
|
-
'RESOLVEIO_QA_MONGO_RUNTIME',
|
|
949
|
-
' if [ -s "$runtime_file" ]; then export RESOLVEIO_QA_MONGO_RUNTIME_PATH="$runtime_file"; fi',
|
|
950
|
-
'}',
|
|
951
|
-
'prestart_seed_live_data() {',
|
|
952
|
-
' local required="${RESOLVEIO_RUNNER_QA_LIVE_DATA_REQUIRED:-${RESOLVEIO_SUPPORT_QA_LIVE_DATA_REQUIRED:-false}}"',
|
|
953
|
-
' truthy "$required" || return 0',
|
|
954
|
-
' [ -f "$TOOLS_DIR/qa-live-data-seed.js" ] || return 0',
|
|
955
|
-
' echo "ResolveIO AI runner QA pre-start live data seed/cleanup starting before app server startup." | tee -a "$ARTIFACT_DIR/runner.log"',
|
|
956
|
-
' if ! start_local_mongo_for_prestart_seed; then',
|
|
957
|
-
' echo "ResolveIO AI runner QA pre-start local Mongo did not become ready for live-data seed." | tee -a "$ARTIFACT_DIR/runner.log"',
|
|
958
|
-
' return 7',
|
|
959
|
-
' fi',
|
|
960
|
-
' materialize_live_mongo_runtime',
|
|
961
|
-
' node "$TOOLS_DIR/qa-live-data-seed.js" "$PROJECT_ROOT" >> "$ARTIFACT_DIR/runner.log" 2>&1',
|
|
962
|
-
' local seed_rc="$?"',
|
|
963
|
-
' if [ "$seed_rc" != "0" ]; then',
|
|
964
|
-
' echo "ResolveIO AI runner QA pre-start live data seed failed before app server startup (exit $seed_rc)." | tee -a "$ARTIFACT_DIR/runner.log"',
|
|
965
|
-
' return "$seed_rc"',
|
|
966
|
-
' fi',
|
|
967
|
-
' export RESOLVEIO_QA_PRESTART_MONGO_READY=1',
|
|
968
|
-
' return 0',
|
|
969
|
-
'}',
|
|
970
|
-
'prepare_tmp_server_settings() {',
|
|
971
|
-
' mkdir -p "$PROJECT_ROOT/server/tmp"',
|
|
972
|
-
' for f in settings.json settings.local.json .env .env.runtime .env.production .env.dev; do',
|
|
973
|
-
' if [ -f "$PROJECT_ROOT/server/$f" ]; then cp "$PROJECT_ROOT/server/$f" "$PROJECT_ROOT/server/tmp/$f"; elif [ -f "$PROJECT_ROOT/$f" ]; then cp "$PROJECT_ROOT/$f" "$PROJECT_ROOT/server/tmp/$f"; fi',
|
|
974
|
-
' done',
|
|
975
|
-
' if [ ! -f "$PROJECT_ROOT/server/tmp/settings.json" ] && [ -f "$PROJECT_ROOT/server/tmp/settings.local.json" ]; then cp "$PROJECT_ROOT/server/tmp/settings.local.json" "$PROJECT_ROOT/server/tmp/settings.json"; fi',
|
|
976
|
-
' [ -f "$PROJECT_ROOT/server/tmp/settings.json" ]',
|
|
977
|
-
'}',
|
|
978
|
-
'preseeded_tmp_server_ready() {',
|
|
979
|
-
' [ -f "$PROJECT_ROOT/server/tmp/index.js" ] || return 1',
|
|
980
|
-
' prepare_tmp_server_settings >/dev/null 2>&1 || return 1',
|
|
981
|
-
' return 0',
|
|
982
|
-
'}',
|
|
983
|
-
'run_preseeded_tmp_server() {',
|
|
984
|
-
' cd "$PROJECT_ROOT/server" || exit 4',
|
|
985
|
-
' source "$TOOLS_DIR/env.sh"',
|
|
986
|
-
' if [ -f "$HOME/.nvm/nvm.sh" ]; then source "$HOME/.nvm/nvm.sh"; nvm use 22 >/dev/null 2>&1 || true; fi',
|
|
987
|
-
' if preseeded_tmp_server_ready; then',
|
|
988
|
-
' echo "ResolveIO AI runner QA using existing compiled server/tmp/index.js for pre-seeded local Mongo."',
|
|
989
|
-
' echo "ResolveIO AI runner QA starting node tmp/index.js"',
|
|
990
|
-
' exec node --inspect=127.0.0.1:"$INSPECT_PORT" --max_old_space_size=3000 tmp/index.js',
|
|
991
|
-
' fi',
|
|
992
|
-
' echo "ResolveIO AI runner QA compiling server tmp output for pre-seeded local Mongo."',
|
|
993
|
-
' node_modules/gulp/bin/gulp.js compile dotenv methodAndPublicationListGenerator',
|
|
994
|
-
' compile_rc="$?"',
|
|
995
|
-
' if [ "$compile_rc" != "0" ]; then echo "ResolveIO AI runner QA server compile failed before tmp startup (exit $compile_rc)."; exit "$compile_rc"; fi',
|
|
996
|
-
' if ! prepare_tmp_server_settings; then echo "ResolveIO AI runner QA missing settings.json/settings.local.json for tmp server startup"; exit 4; fi',
|
|
997
|
-
' if [ ! -f tmp/index.js ]; then echo "ResolveIO AI runner QA missing server/tmp/index.js after compile"; exit 4; fi',
|
|
998
|
-
' echo "ResolveIO AI runner QA starting node tmp/index.js"',
|
|
999
|
-
' exec node --inspect=127.0.0.1:"$INSPECT_PORT" --max_old_space_size=3000 tmp/index.js',
|
|
1000
|
-
'}',
|
|
1001
|
-
'if reuse_running_app_if_ready; then RUNNER_REUSED_READY=1; exit 0; fi',
|
|
1002
|
-
'if ! mkdir "$LOCK_DIR" 2>/dev/null; then',
|
|
1003
|
-
' if janitor_lock_is_live; then',
|
|
1004
|
-
' echo "ResolveIO AI runner QA lock is already held by a live runner for $PROJECT_ROOT; refusing duplicate startup." | tee "$ARTIFACT_DIR/runner.log"',
|
|
1005
|
-
' exit 6',
|
|
1006
|
-
' fi',
|
|
1007
|
-
' echo "ResolveIO AI runner QA lock is stale for $PROJECT_ROOT; cleaning scoped local QA processes before retrying." | tee "$ARTIFACT_DIR/runner.log"',
|
|
1008
|
-
' cleanup_project_processes',
|
|
1009
|
-
' rm -rf "$LOCK_DIR" >/dev/null 2>&1 || true',
|
|
1010
|
-
' if ! mkdir "$LOCK_DIR" 2>/dev/null; then',
|
|
1011
|
-
' echo "ResolveIO AI runner QA lock is still held for $PROJECT_ROOT after cleanup. Stop the existing QA runner before starting another." | tee -a "$ARTIFACT_DIR/runner.log"',
|
|
1012
|
-
' exit 6',
|
|
1013
|
-
' fi',
|
|
1014
|
-
'fi',
|
|
1015
|
-
'janitor_write_lock',
|
|
1016
|
-
'cleanup_project_processes',
|
|
1017
|
-
': > "$ARTIFACT_DIR/server.log"',
|
|
1018
|
-
': > "$ARTIFACT_DIR/client.log"',
|
|
1019
|
-
': > "$ARTIFACT_DIR/runner.log"',
|
|
1020
|
-
'janitor_write_manifest',
|
|
1021
|
-
'janitor_start_heartbeat',
|
|
1022
|
-
'echo "$RUNNER_JANITOR_HEARTBEAT_PID" > "$ARTIFACT_DIR/heartbeat.pid"',
|
|
1023
|
-
'janitor_check_resources || exit $?',
|
|
1024
|
-
'prestart_seed_live_data || exit $?',
|
|
1025
|
-
'if [ -d "$PROJECT_ROOT/server" ]; then',
|
|
1026
|
-
' SERVER_REQUIRED=1',
|
|
1027
|
-
' if [ "${RESOLVEIO_QA_PRESTART_MONGO_READY:-}" = "1" ] && [ -x "$PROJECT_ROOT/server/node_modules/gulp/bin/gulp.js" ] && [ -f "$PROJECT_ROOT/server/gulpfile.js" ] && grep -q "mongo-start" "$PROJECT_ROOT/server/gulpfile.js"; then',
|
|
1028
|
-
' echo "ResolveIO AI runner QA using pre-seeded local Mongo; starting server without mongo-start." | tee -a "$ARTIFACT_DIR/runner.log"',
|
|
1029
|
-
' (run_preseeded_tmp_server 2>&1 | tee "$ARTIFACT_DIR/server.log") &',
|
|
1030
|
-
' elif node -e "const p=require(process.argv[1]); process.exit(p.scripts&&p.scripts.server?0:1)" "$PROJECT_ROOT/server/package.json" >/dev/null 2>&1; then',
|
|
1031
|
-
' (cd "$PROJECT_ROOT/server" && source "$TOOLS_DIR/env.sh" && npm run server 2>&1 | tee "$ARTIFACT_DIR/server.log") &',
|
|
1032
|
-
' elif [ -x "$PROJECT_ROOT/server/start_server.sh" ]; then',
|
|
1033
|
-
' (cd "$PROJECT_ROOT/server" && source "$TOOLS_DIR/env.sh" && ./start_server.sh 2>&1 | tee "$ARTIFACT_DIR/server.log") &',
|
|
1034
|
-
' else',
|
|
1035
|
-
' echo "ResolveIO AI runner QA cannot find npm server script or start_server.sh for $PROJECT_ROOT/server" | tee "$ARTIFACT_DIR/server.log"',
|
|
1036
|
-
' exit 5',
|
|
1037
|
-
' fi',
|
|
1038
|
-
' SERVER_PID=$!',
|
|
1039
|
-
' wait_for_server_ready',
|
|
1040
|
-
' SERVER_RESULT=$?',
|
|
1041
|
-
' if [ "$SERVER_RESULT" != "0" ]; then echo "ResolveIO AI runner QA server startup fatal error. See $ARTIFACT_DIR/server.log"; exit "$SERVER_RESULT"; fi',
|
|
1042
|
-
'fi',
|
|
1043
|
-
'CLIENT_HOST="${RESOLVEIO_RUNNER_QA_CLIENT_HOST:-${RESOLVEIO_SUPPORT_QA_CLIENT_HOST:-0.0.0.0}}"',
|
|
1044
|
-
'if [ -x "$PROJECT_ROOT/node_modules/.bin/ng" ]; then',
|
|
1045
|
-
' prepare_angular_cache_dirs',
|
|
1046
|
-
' ANGULAR_OLD_SPACE="${RESOLVEIO_RUNNER_QA_ANGULAR_OLD_SPACE_MB:-${RESOLVEIO_SUPPORT_QA_ANGULAR_OLD_SPACE_MB:-3072}}"',
|
|
1047
|
-
' (cd "$PROJECT_ROOT" && source "$TOOLS_DIR/env.sh" && node --max_old_space_size="$ANGULAR_OLD_SPACE" ./node_modules/.bin/ng serve --watch --configuration local --host "$CLIENT_HOST" --port "$CLIENT_PORT" "${ANGULAR_PREBUNDLE_ARGS[@]}" 2>&1 | tee "$ARTIFACT_DIR/client.log") &',
|
|
1048
|
-
'elif [ -x "$PROJECT_ROOT/start_client.sh" ]; then',
|
|
1049
|
-
' (cd "$PROJECT_ROOT" && source "$TOOLS_DIR/env.sh" && ./start_client.sh 2>&1 | tee "$ARTIFACT_DIR/client.log") &',
|
|
1050
|
-
'elif node -e "const p=require(process.argv[1]); process.exit(p.scripts&&p.scripts.client?0:1)" "$PROJECT_ROOT/package.json" >/dev/null 2>&1; then',
|
|
1051
|
-
' (cd "$PROJECT_ROOT" && source "$TOOLS_DIR/env.sh" && npm run client 2>&1 | tee "$ARTIFACT_DIR/client.log") &',
|
|
1052
|
-
'else',
|
|
1053
|
-
' echo "ResolveIO AI runner QA cannot find Angular CLI, start_client.sh, or npm client script for $PROJECT_ROOT" | tee "$ARTIFACT_DIR/client.log"',
|
|
1054
|
-
' exit 5',
|
|
1055
|
-
'fi',
|
|
1056
|
-
'CLIENT_PID=$!',
|
|
1057
|
-
'wait_for_client',
|
|
1058
|
-
'RESULT=$?',
|
|
1059
|
-
'case "$RESULT" in',
|
|
1060
|
-
' 0)',
|
|
1061
|
-
' echo "ResolveIO AI runner QA local app ready at $CLIENT_URL";',
|
|
1062
|
-
' echo "$SERVER_PID" > "$ARTIFACT_DIR/server.pid";',
|
|
1063
|
-
' echo "$CLIENT_PID" > "$ARTIFACT_DIR/client.pid";',
|
|
1064
|
-
' if truthy "$KEEPALIVE"; then detach_keepalive || exit $?; fi;',
|
|
1065
|
-
' exit 0',
|
|
1066
|
-
' ;;',
|
|
1067
|
-
' 2) echo "ResolveIO AI runner QA client process exited before $CLIENT_URL became ready. See $ARTIFACT_DIR/client.log"; exit 2 ;;',
|
|
1068
|
-
' 3) echo "ResolveIO AI runner QA client startup fatal error. See $ARTIFACT_DIR/client.log"; exit 3 ;;',
|
|
1069
|
-
' 4) echo "ResolveIO AI runner QA server startup fatal error. See $ARTIFACT_DIR/server.log"; exit 4 ;;',
|
|
1070
|
-
' *) echo "ResolveIO AI runner QA local app did not become ready at $CLIENT_URL within ${STARTUP_TIMEOUT}s. See $ARTIFACT_DIR/client.log and $ARTIFACT_DIR/server.log"; exit 1 ;;',
|
|
1071
|
-
'esac',
|
|
1072
|
-
''
|
|
1073
|
-
].join('\n');
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
export function buildResolveIORunnerLocalQaStopperScript(): string {
|
|
1077
|
-
return [
|
|
1078
|
-
'#!/usr/bin/env bash',
|
|
1079
|
-
'set -u',
|
|
1080
|
-
'TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"',
|
|
1081
|
-
'source "$TOOLS_DIR/env.sh"',
|
|
1082
|
-
'PROJECT_ROOT="${1:-$(pwd)}"',
|
|
1083
|
-
'PROJECT_ROOT="$(cd "$PROJECT_ROOT" && pwd)"',
|
|
1084
|
-
'ARTIFACT_DIR="$PROJECT_ROOT/qa-artifacts"',
|
|
1085
|
-
'CLIENT_PORT="${RESOLVEIO_RUNNER_QA_CLIENT_PORT:-${RESOLVEIO_SUPPORT_QA_CLIENT_PORT:-4200}}"',
|
|
1086
|
-
'SERVER_PORT="${RESOLVEIO_RUNNER_QA_SERVER_PORT:-${RESOLVEIO_SUPPORT_QA_SERVER_PORT:-8080}}"',
|
|
1087
|
-
'MONGO_PORT="${RESOLVEIO_RUNNER_QA_MONGO_PORT:-${RESOLVEIO_SUPPORT_QA_MONGO_PORT:-3001}}"',
|
|
1088
|
-
'INSPECT_PORT="${RESOLVEIO_RUNNER_QA_INSPECT_PORT:-${RESOLVEIO_SUPPORT_QA_INSPECT_PORT:-9229}}"',
|
|
1089
|
-
'REPO_ROOT="$(git -C "$PROJECT_ROOT" rev-parse --show-toplevel 2>/dev/null || echo "$PROJECT_ROOT")"',
|
|
1090
|
-
buildRunnerProcessJanitorShellLibrary({ mode: 'support' }),
|
|
1091
|
-
'STOP_LOCK_DIR="$ARTIFACT_DIR/.qa.stop.lock"',
|
|
1092
|
-
'if ! mkdir "$STOP_LOCK_DIR" 2>/dev/null; then',
|
|
1093
|
-
' lock_pid="$(cat "$STOP_LOCK_DIR/pid" 2>/dev/null || true)"',
|
|
1094
|
-
' if [ -n "$lock_pid" ] && kill -0 "$lock_pid" >/dev/null 2>&1; then',
|
|
1095
|
-
' echo "ResolveIO AI runner QA cleanup already active for $PROJECT_ROOT; refusing duplicate stopper."',
|
|
1096
|
-
' exit 0',
|
|
1097
|
-
' fi',
|
|
1098
|
-
' rm -rf "$STOP_LOCK_DIR" >/dev/null 2>&1 || true',
|
|
1099
|
-
' mkdir "$STOP_LOCK_DIR" 2>/dev/null || { echo "ResolveIO AI runner QA cleanup lock still active for $PROJECT_ROOT."; exit 0; }',
|
|
1100
|
-
'fi',
|
|
1101
|
-
'echo "$$" > "$STOP_LOCK_DIR/pid" 2>/dev/null || true',
|
|
1102
|
-
'trap \'rm -rf "$STOP_LOCK_DIR" >/dev/null 2>&1 || true\' EXIT',
|
|
1103
|
-
'kill_tree() {',
|
|
1104
|
-
' local pid="$1"',
|
|
1105
|
-
' [ -n "$pid" ] || return 0',
|
|
1106
|
-
' kill -0 "$pid" >/dev/null 2>&1 || return 0',
|
|
1107
|
-
' for child in $(pgrep -P "$pid" 2>/dev/null || true); do kill_tree "$child"; done',
|
|
1108
|
-
' kill "$pid" >/dev/null 2>&1 || true',
|
|
1109
|
-
' sleep 1',
|
|
1110
|
-
' kill -0 "$pid" >/dev/null 2>&1 && kill -9 "$pid" >/dev/null 2>&1 || true',
|
|
1111
|
-
'}',
|
|
1112
|
-
'kill_port_listeners() {',
|
|
1113
|
-
' local port="$1"',
|
|
1114
|
-
' [ -n "$port" ] || return 0',
|
|
1115
|
-
' local pids=""',
|
|
1116
|
-
' if command -v lsof >/dev/null 2>&1; then pids="$pids $(janitor_bounded 3 lsof -ti tcp:"$port")"; fi',
|
|
1117
|
-
' if command -v fuser >/dev/null 2>&1; then pids="$pids $(janitor_bounded 3 fuser -n tcp "$port")"; fi',
|
|
1118
|
-
' if command -v ss >/dev/null 2>&1; then pids="$pids $(janitor_bounded 3 ss -ltnp "sport = :$port" | sed -n \'s/.*pid=\\([0-9][0-9]*\\).*/\\1/p\' || true)"; fi',
|
|
1119
|
-
' for pid in $pids; do',
|
|
1120
|
-
' [ "$pid" = "$$" ] && continue',
|
|
1121
|
-
' kill_tree "$pid"',
|
|
1122
|
-
' done',
|
|
1123
|
-
' sleep 1',
|
|
1124
|
-
'}',
|
|
1125
|
-
'kill_env_marked_processes() {',
|
|
1126
|
-
' [ -d /proc ] || return 0',
|
|
1127
|
-
' local job_id="${RESOLVEIO_RUNNER_QA_JOB_ID:-${RESOLVEIO_SUPPORT_QA_JOB_ID:-}}"',
|
|
1128
|
-
' local owner_id="${RESOLVEIO_RUNNER_QA_OWNER_ID:-${RESOLVEIO_SUPPORT_QA_OWNER_ID:-}}"',
|
|
1129
|
-
' local token="${RESOLVEIO_RUNNER_QA_RUNNER_TOKEN:-${RESOLVEIO_SUPPORT_QA_RUNNER_TOKEN:-}}"',
|
|
1130
|
-
' [ -n "$job_id$owner_id$token" ] || return 0',
|
|
1131
|
-
' for env_file in /proc/[0-9]*/environ; do',
|
|
1132
|
-
' pid="${env_file#/proc/}"',
|
|
1133
|
-
' pid="${pid%/environ}"',
|
|
1134
|
-
' skip_cleanup_pid "$pid" && continue',
|
|
1135
|
-
' [ "$pid" = "$$" ] && continue',
|
|
1136
|
-
' [ -r "$env_file" ] || continue',
|
|
1137
|
-
' env_text="$(tr "\\0" "\\n" < "$env_file" 2>/dev/null || true)"',
|
|
1138
|
-
' [ -n "$env_text" ] || continue',
|
|
1139
|
-
' matched=0',
|
|
1140
|
-
' [ -n "$job_id" ] && echo "$env_text" | grep -Eq "^(RESOLVEIO_RUNNER_QA_JOB_ID|RESOLVEIO_SUPPORT_QA_JOB_ID)=$job_id$" && matched=1',
|
|
1141
|
-
' [ "$matched" = "0" ] && [ -n "$owner_id" ] && echo "$env_text" | grep -Eq "^(RESOLVEIO_RUNNER_QA_OWNER_ID|RESOLVEIO_SUPPORT_QA_OWNER_ID)=$owner_id$" && matched=1',
|
|
1142
|
-
' [ "$matched" = "0" ] && [ -n "$token" ] && echo "$env_text" | grep -Eq "^(RESOLVEIO_RUNNER_QA_RUNNER_TOKEN|RESOLVEIO_SUPPORT_QA_RUNNER_TOKEN)=$token$" && matched=1',
|
|
1143
|
-
' [ "$matched" = "1" ] && kill_tree "$pid" && killed_count=$((killed_count + 1))',
|
|
1144
|
-
' done',
|
|
1145
|
-
'}',
|
|
1146
|
-
'kill_artifact_log_writers() {',
|
|
1147
|
-
' local artifact="$ARTIFACT_DIR"',
|
|
1148
|
-
' [ -n "$artifact" ] || return 0',
|
|
1149
|
-
' for pid in $(ps -eo pid=,args= | awk -v artifact="$artifact" \'index($0, artifact) && $0 !~ /awk -v artifact=/ && $0 !~ /ps -eo pid=,args=/ && $0 ~ /(^|[ /])(tee|tail)( |$)/ {print $1}\' 2>/dev/null || true); do',
|
|
1150
|
-
' skip_cleanup_pid "$pid" && continue',
|
|
1151
|
-
' kill_tree "$pid"',
|
|
1152
|
-
' killed_count=$((killed_count + 1))',
|
|
1153
|
-
' done',
|
|
1154
|
-
'}',
|
|
1155
|
-
'kill_local_mongo_processes() {',
|
|
1156
|
-
' local mongo_port="$MONGO_PORT"',
|
|
1157
|
-
' local root="$PROJECT_ROOT"',
|
|
1158
|
-
' [ -n "$mongo_port$root" ] || return 0',
|
|
1159
|
-
' for pid in $(ps -eo pid=,args= | awk -v port="$mongo_port" -v root="$root" \'$0 !~ /awk -v port=/ && $0 !~ /ps -eo pid=,args=/ && $0 ~ /mongod/ && (($0 ~ ("--port " port)) || index($0, root "/server/mongo/data/db") || index($0, root "/mongo/data/db")) && ($0 ~ /\\/tmp\\/resolveio-support-qa\\/.*mongod/ || index($0, root) || $0 ~ /--dbpath .*mongo\\/data\\/db/) {print $1}\' 2>/dev/null || true); do',
|
|
1160
|
-
' skip_cleanup_pid "$pid" && continue',
|
|
1161
|
-
' [ "$pid" = "$$" ] && continue',
|
|
1162
|
-
' kill_tree "$pid"',
|
|
1163
|
-
' sleep 1',
|
|
1164
|
-
' kill -0 "$pid" >/dev/null 2>&1 && kill -9 "$pid" >/dev/null 2>&1 || true',
|
|
1165
|
-
' killed_count=$((killed_count + 1))',
|
|
1166
|
-
' done',
|
|
1167
|
-
'}',
|
|
1168
|
-
'RUNNER_ANCESTOR_PIDS=" $$ ${PPID:-} "',
|
|
1169
|
-
'ancestor_pid="${PPID:-}"',
|
|
1170
|
-
'while [ -n "$ancestor_pid" ] && [ "$ancestor_pid" != "0" ] && [ "$ancestor_pid" != "1" ]; do',
|
|
1171
|
-
' ancestor_pid="$(ps -o ppid= -p "$ancestor_pid" 2>/dev/null | tr -d " " || true)"',
|
|
1172
|
-
' [ -n "$ancestor_pid" ] && RUNNER_ANCESTOR_PIDS="$RUNNER_ANCESTOR_PIDS $ancestor_pid "',
|
|
1173
|
-
'done',
|
|
1174
|
-
'skip_cleanup_pid() {',
|
|
1175
|
-
' case "$RUNNER_ANCESTOR_PIDS" in *" $1 "*) return 0 ;; esac',
|
|
1176
|
-
' return 1',
|
|
1177
|
-
'}',
|
|
1178
|
-
'janitor_update_manifest_status cleanup_started',
|
|
1179
|
-
'killed_count=0',
|
|
1180
|
-
'for pid_file in "$ARTIFACT_DIR/heartbeat.pid" "$ARTIFACT_DIR/server.pid" "$ARTIFACT_DIR/client.pid"; do',
|
|
1181
|
-
' [ -f "$pid_file" ] || continue',
|
|
1182
|
-
' pid="$(cat "$pid_file" 2>/dev/null || true)"',
|
|
1183
|
-
' kill_tree "$pid"',
|
|
1184
|
-
' killed_count=$((killed_count + 1))',
|
|
1185
|
-
' rm -f "$pid_file"',
|
|
1186
|
-
'done',
|
|
1187
|
-
'for pass in 1 2 3; do',
|
|
1188
|
-
' for port in "$CLIENT_PORT" "$SERVER_PORT" "$MONGO_PORT" "$INSPECT_PORT"; do kill_port_listeners "$port"; done',
|
|
1189
|
-
' kill_env_marked_processes',
|
|
1190
|
-
' kill_artifact_log_writers',
|
|
1191
|
-
' kill_local_mongo_processes',
|
|
1192
|
-
' for pid in $(ps -eo pid=,args= | awk -v root="$PROJECT_ROOT" \'index($0, root) && $0 !~ /awk -v root=/ && $0 !~ /ps -eo pid=,args=/ && $0 !~ /run-local-qa\\.sh|stop-local-qa\\.sh|bugfix-comparison-qa\\.sh/ && $0 ~ /(ng serve|node .*node_modules\\/\\.bin\\/ng[[:space:]]+serve|esbuild --service|npm run client|start_client\\.sh|npm run server|start_server\\.sh|nodemon|node .*tmp\\/index\\.js|mongod|mongodb-binaries\\/mongod)/ {print $1}\' 2>/dev/null || true); do',
|
|
1193
|
-
' skip_cleanup_pid "$pid" && continue',
|
|
1194
|
-
' kill_tree "$pid"',
|
|
1195
|
-
' killed_count=$((killed_count + 1))',
|
|
1196
|
-
' done',
|
|
1197
|
-
' # Do not kill arbitrary processes by project cwd; completion builds run',
|
|
1198
|
-
' # from the same cwd. Stop only known QA process signatures, recorded PIDs,',
|
|
1199
|
-
' # scoped ports, and QA runner env markers.',
|
|
1200
|
-
' sleep 1',
|
|
1201
|
-
'done',
|
|
1202
|
-
'wait_until=$((SECONDS + 60))',
|
|
1203
|
-
'while [ "$SECONDS" -lt "$wait_until" ]; do',
|
|
1204
|
-
' found=0',
|
|
1205
|
-
' for port in "$CLIENT_PORT" "$SERVER_PORT" "$MONGO_PORT" "$INSPECT_PORT"; do',
|
|
1206
|
-
' port_pids=""',
|
|
1207
|
-
' if command -v lsof >/dev/null 2>&1; then port_pids="$port_pids $(janitor_bounded 3 lsof -ti tcp:"$port")"; fi',
|
|
1208
|
-
' for pid in $port_pids; do skip_cleanup_pid "$pid" && continue; found=1; kill_tree "$pid"; done',
|
|
1209
|
-
' done',
|
|
1210
|
-
' if [ -d /proc ]; then',
|
|
1211
|
-
' job_id="${RESOLVEIO_RUNNER_QA_JOB_ID:-${RESOLVEIO_SUPPORT_QA_JOB_ID:-}}"',
|
|
1212
|
-
' owner_id="${RESOLVEIO_RUNNER_QA_OWNER_ID:-${RESOLVEIO_SUPPORT_QA_OWNER_ID:-}}"',
|
|
1213
|
-
' token="${RESOLVEIO_RUNNER_QA_RUNNER_TOKEN:-${RESOLVEIO_SUPPORT_QA_RUNNER_TOKEN:-}}"',
|
|
1214
|
-
' for env_file in /proc/[0-9]*/environ; do',
|
|
1215
|
-
' pid="${env_file#/proc/}"',
|
|
1216
|
-
' pid="${pid%/environ}"',
|
|
1217
|
-
' skip_cleanup_pid "$pid" && continue',
|
|
1218
|
-
' [ -r "$env_file" ] || continue',
|
|
1219
|
-
' env_text="$(tr "\\0" "\\n" < "$env_file" 2>/dev/null || true)"',
|
|
1220
|
-
' matched=0',
|
|
1221
|
-
' [ -n "$job_id" ] && echo "$env_text" | grep -Eq "^(RESOLVEIO_RUNNER_QA_JOB_ID|RESOLVEIO_SUPPORT_QA_JOB_ID)=$job_id$" && matched=1',
|
|
1222
|
-
' [ "$matched" = "0" ] && [ -n "$owner_id" ] && echo "$env_text" | grep -Eq "^(RESOLVEIO_RUNNER_QA_OWNER_ID|RESOLVEIO_SUPPORT_QA_OWNER_ID)=$owner_id$" && matched=1',
|
|
1223
|
-
' [ "$matched" = "0" ] && [ -n "$token" ] && echo "$env_text" | grep -Eq "^(RESOLVEIO_RUNNER_QA_RUNNER_TOKEN|RESOLVEIO_SUPPORT_QA_RUNNER_TOKEN)=$token$" && matched=1',
|
|
1224
|
-
' [ "$matched" = "1" ] && found=1 && kill_tree "$pid"',
|
|
1225
|
-
' done',
|
|
1226
|
-
' fi',
|
|
1227
|
-
' kill_artifact_log_writers',
|
|
1228
|
-
' kill_local_mongo_processes',
|
|
1229
|
-
' for pid in $(ps -eo pid=,args= | awk -v root="$PROJECT_ROOT" \'index($0, root) && $0 !~ /awk -v root=/ && $0 !~ /ps -eo pid=,args=/ && $0 !~ /run-local-qa\\.sh|stop-local-qa\\.sh|bugfix-comparison-qa\\.sh/ && $0 ~ /(ng serve|node .*node_modules\\/\\.bin\\/ng[[:space:]]+serve|esbuild --service|npm run client|start_client\\.sh|npm run server|start_server\\.sh|nodemon|node .*tmp\\/index\\.js|mongod|mongodb-binaries\\/mongod)/ {print $1}\' 2>/dev/null || true); do',
|
|
1230
|
-
' skip_cleanup_pid "$pid" && continue',
|
|
1231
|
-
' found=1',
|
|
1232
|
-
' done',
|
|
1233
|
-
' [ "$found" = "0" ] && break',
|
|
1234
|
-
' sleep 2',
|
|
1235
|
-
'done',
|
|
1236
|
-
'rm -rf "$ARTIFACT_DIR/.qa.lock" >/dev/null 2>&1 || true',
|
|
1237
|
-
'janitor_update_manifest_status stopped',
|
|
1238
|
-
'janitor_write_cleanup_status stopped "$killed_count"',
|
|
1239
|
-
'echo "ResolveIO AI runner QA local app stopped for $PROJECT_ROOT"',
|
|
1240
|
-
''
|
|
1241
|
-
].join('\n');
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
export function buildResolveIORunnerQaLiveDataSeederScript(): string {
|
|
1245
|
-
return [
|
|
1246
|
-
'#!/usr/bin/env node',
|
|
1247
|
-
'const fs = require("fs");',
|
|
1248
|
-
'const path = require("path");',
|
|
1249
|
-
'const crypto = require("crypto");',
|
|
1250
|
-
'',
|
|
1251
|
-
'if (isBuildLane()) {',
|
|
1252
|
-
' console.error("ResolveIO support lane guard: live-data seeding is owned by the QA lane, not the 5.3 build lane.");',
|
|
1253
|
-
' process.exit(86);',
|
|
1254
|
-
'}',
|
|
1255
|
-
'',
|
|
1256
|
-
'const projectRoot = path.resolve(process.argv[2] || process.cwd());',
|
|
1257
|
-
'const repoRoot = findRepoRoot(projectRoot);',
|
|
1258
|
-
'const artifactDir = path.join(projectRoot, "qa-artifacts");',
|
|
1259
|
-
'fs.mkdirSync(artifactDir, { recursive: true });',
|
|
1260
|
-
'const resultPath = path.join(artifactDir, "qa-live-data-seed-result.json");',
|
|
1261
|
-
'',
|
|
1262
|
-
'function writeResult(payload, exitCode = 0) {',
|
|
1263
|
-
' const full = { ...payload, created_at: new Date().toISOString() };',
|
|
1264
|
-
' normalizeCoverageMatrixFromLiveSeed(full);',
|
|
1265
|
-
' fs.writeFileSync(resultPath, JSON.stringify(full, null, 2));',
|
|
1266
|
-
' console.log(JSON.stringify(full, null, 2));',
|
|
1267
|
-
' process.exit(exitCode);',
|
|
1268
|
-
'}',
|
|
1269
|
-
'',
|
|
1270
|
-
'function isBuildLane() {',
|
|
1271
|
-
' return /^(build|support_build|support-build)$/i.test(String(process.env.RESOLVEIO_SUPPORT_CODEX_LANE || "").trim());',
|
|
1272
|
-
'}',
|
|
1273
|
-
'',
|
|
1274
|
-
'function findRepoRoot(start) {',
|
|
1275
|
-
' let current = start;',
|
|
1276
|
-
' for (let i = 0; i < 8; i += 1) {',
|
|
1277
|
-
' if (fs.existsSync(path.join(current, ".git"))) return current;',
|
|
1278
|
-
' const parent = path.dirname(current);',
|
|
1279
|
-
' if (parent === current) break;',
|
|
1280
|
-
' current = parent;',
|
|
1281
|
-
' }',
|
|
1282
|
-
' return start;',
|
|
1283
|
-
'}',
|
|
1284
|
-
'',
|
|
1285
|
-
'function readJsonIfExists(filePath) {',
|
|
1286
|
-
' try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch (error) { return null; }',
|
|
1287
|
-
'}',
|
|
1288
|
-
'',
|
|
1289
|
-
'function normalizeCoverageMatrixFromLiveSeed(payload) {',
|
|
1290
|
-
' const bolContext = payload && payload.selected && payload.selected.truck_treating_bol_context;',
|
|
1291
|
-
' const notes = Array.isArray(payload && payload.notes) ? payload.notes.map((note) => String(note || "")) : [];',
|
|
1292
|
-
' const billingDisabled = notes.some((note) => /billing fixtures are disabled/i.test(note));',
|
|
1293
|
-
' if (!bolContext || !billingDisabled) return;',
|
|
1294
|
-
' const matrixPath = path.join(artifactDir, "qa-coverage-matrix.json");',
|
|
1295
|
-
' const matrix = readJsonIfExists(matrixPath) || { status: "started", seeded: true, source: "qa-live-data-seed" };',
|
|
1296
|
-
' const browserRoutes = bolContext.browser_routes && typeof bolContext.browser_routes === "object" ? bolContext.browser_routes : {};',
|
|
1297
|
-
' const routeHints = Array.isArray(bolContext.route_name_hints) ? bolContext.route_name_hints.map((value) => String(value || "").trim()).filter(Boolean) : [];',
|
|
1298
|
-
' const linkedRoutes = Array.isArray(bolContext.linked_truck_treating_routes) ? bolContext.linked_truck_treating_routes.map((value) => String(value || "").trim()).filter(Boolean) : [];',
|
|
1299
|
-
' const bolIds = Array.isArray(bolContext.bol_ids) ? bolContext.bol_ids.map((value) => String(value || "").trim()).filter(Boolean) : [];',
|
|
1300
|
-
' const routeIds = Array.isArray(bolContext.route_ids) ? bolContext.route_ids.map((value) => String(value || "").trim()).filter(Boolean) : [];',
|
|
1301
|
-
' const truckTreatingRouteIds = Array.isArray(bolContext.truck_treating_route_ids) ? bolContext.truck_treating_route_ids.map((value) => String(value || "").trim()).filter(Boolean) : [];',
|
|
1302
|
-
' const primaryStatus = String(bolContext.primary_bol_status || "");',
|
|
1303
|
-
' const delivered = Boolean(bolContext.primary_bol_delivered) || /delivered/i.test(primaryStatus);',
|
|
1304
|
-
' const hintedBol = routeHints.join(" ");',
|
|
1305
|
-
' const bolNumber = (/\\bBOL\\s*#?\\s*(\\d+)\\b/i.exec(hintedBol) || [])[1];',
|
|
1306
|
-
' const bolLabel = bolNumber ? `BOL ${bolNumber}` : (bolIds[0] ? `BOL ${bolIds[0]}` : "the requested BOL");',
|
|
1307
|
-
' const routeHint = routeHints.find((hint) => !/\\bBOL\\b/i.test(hint) && !/^\\d+$/.test(hint)) || "the ticket route hint";',
|
|
1308
|
-
' const routeSummary = linkedRoutes.length ? linkedRoutes.join(", ") : (truckTreatingRouteIds.length ? truckTreatingRouteIds.join(", ") : routeIds.slice(0, 6).join(", "));',
|
|
1309
|
-
' const deliveredNote = delivered ? ` The live BOL status is ${primaryStatus || "Delivered"}, so the direct delivery route is an action route that may correctly redirect; QA must use delivered Open File/detail proof or a localhost-only deliverable fixture copied from this live context for Treat/Deliver actions.` : "";',
|
|
1310
|
-
' const liveAssertion = `Live Mongo seeded ${bolLabel} and proved ${routeHint} is not a linked truck-treating route for that BOL; QA must continue against linked live route data: ${routeSummary}.${deliveredNote}`;',
|
|
1311
|
-
' matrix.status = "in_progress";',
|
|
1312
|
-
' matrix.source = "qa-live-data-seed-live-bol-context";',
|
|
1313
|
-
' matrix.live_seed_context_artifact = "qa-artifacts/qa-live-data-seed-result.json";',
|
|
1314
|
-
' matrix.billing_rows_removed_reason = "Live data seed disabled billing fixtures for this ticket; invoice/billing matrix rows do not apply to truck-treating BOL QA.";',
|
|
1315
|
-
' matrix.rows = [',
|
|
1316
|
-
' { workflow: "Truck treating BOL live-data target and route validation", route: browserRoutes.delivery || browserRoutes.detail || browserRoutes.list || "Deliver BOL live seeded BOL context", assertion: liveAssertion, required_proof: "Live Mongo seed artifact showing BOL id/status, linked route ids, ticket route-hint match result, and browser route.", status: "pass", result: "pass", screenshot: "", caption: `Live data seeded ${bolLabel}; the ticket route label did not match the BOL-linked live routes, so QA continued against ${routeSummary}.`, artifact: "qa-artifacts/qa-live-data-seed-result.json", persisted_assertion: `qa-live-data-seed-result.json route_hint_matched=${bolContext.route_hint_matched}; bol_ids=${bolIds.join(",") || "none"}; route_ids=${routeIds.length}; truck_treating_route_ids=${truckTreatingRouteIds.length}; primary_bol_status=${primaryStatus || "unknown"}.` },',
|
|
1317
|
-
' { workflow: "Delivered BOL detail/Open File proof", route: browserRoutes.detail || browserRoutes.list || browserRoutes.delivery || "/", assertion: `Open the seeded ${bolLabel} in the truck-treating delivered/detail workflow and prove the local QA database loaded the customer-facing BOL screen without relying on the already-delivered action route.`, required_proof: "Customer-facing screenshot of the BOL detail/list/Open File screen plus BOL id/status assertion from local Mongo.", status: "pending", screenshot: "", caption: "" },',
|
|
1318
|
-
' { workflow: "Deliver BOL service/flush rows do not render as missing chemicals", route: browserRoutes.delivery || browserRoutes.detail || "/", assertion: "Service/flush/non-chemical treatment plans must not be injected into the Deliver BOL chemical list as red missing chemical placeholders; chemical treatment rows still appear normally.", required_proof: "Customer-facing screenshot of the Deliver BOL/detail workflow or a localhost-only deliverable fixture copied from the seeded live BOL context, plus DOM/local Mongo assertion that missing placeholders are limited to Chemical treatment plans.", status: "pending", screenshot: "", caption: "" }',
|
|
1319
|
-
' ];',
|
|
1320
|
-
' fs.writeFileSync(matrixPath, JSON.stringify(matrix, null, 2));',
|
|
1321
|
-
'}',
|
|
1322
|
-
'',
|
|
1323
|
-
'function preserveExistingSeedResult(reason) {',
|
|
1324
|
-
' const existing = readJsonIfExists(resultPath);',
|
|
1325
|
-
' const status = String(existing && existing.status || "").toLowerCase();',
|
|
1326
|
-
' const desiredProfile = desiredSeedProfile();',
|
|
1327
|
-
' if (existing && existing.profile && existing.profile !== desiredProfile) return;',
|
|
1328
|
-
' if (existing && existing.profile === "billing_inventory" && shouldSeedBillingDashboardFixtures() && !(existing.selected && existing.selected.qa_billing_fixture)) {',
|
|
1329
|
-
' return;',
|
|
1330
|
-
' }',
|
|
1331
|
-
' if (existing && extractBolIdentifiers().length && !(existing.selected && existing.selected.truck_treating_bol_context)) {',
|
|
1332
|
-
' return;',
|
|
1333
|
-
' }',
|
|
1334
|
-
' if (existing && extractBolIdentifiers().length && existing.selected && existing.selected.truck_treating_bol_context && !(existing.selected.truck_treating_bol_context.browser_routes && existing.selected.truck_treating_bol_context.browser_routes.delivery)) {',
|
|
1335
|
-
' return;',
|
|
1336
|
-
' }',
|
|
1337
|
-
' if (existing && extractBolIdentifiers().length && existing.selected && existing.selected.truck_treating_bol_context && !Object.prototype.hasOwnProperty.call(existing.selected.truck_treating_bol_context, "primary_bol_status")) {',
|
|
1338
|
-
' return;',
|
|
1339
|
-
' }',
|
|
1340
|
-
' if (existing && shouldAutoDiscoverAssetContext() && !(existing.selected && existing.selected.qa_asset_context)) {',
|
|
1341
|
-
' return;',
|
|
1342
|
-
' }',
|
|
1343
|
-
' if (existing && shouldAutoDiscoverProductionInterchangeablesContext() && !(existing.selected && existing.selected.qa_production_interchangeables_context)) {',
|
|
1344
|
-
' return;',
|
|
1345
|
-
' }',
|
|
1346
|
-
' if (["pass", "needs-data"].includes(status)) {',
|
|
1347
|
-
' normalizeCoverageMatrixFromLiveSeed(existing);',
|
|
1348
|
-
' const preserved = { ...existing, reused_existing: true, reuse_reason: reason, checked_at: new Date().toISOString() };',
|
|
1349
|
-
' console.log(JSON.stringify(preserved, null, 2));',
|
|
1350
|
-
' process.exit(0);',
|
|
1351
|
-
' }',
|
|
1352
|
-
'}',
|
|
1353
|
-
'',
|
|
1354
|
-
'function resolveRuntimeSource() {',
|
|
1355
|
-
' const explicitRuntimePaths = [',
|
|
1356
|
-
' process.env.RESOLVEIO_QA_MONGO_RUNTIME_PATH,',
|
|
1357
|
-
' process.env.RESOLVEIO_SUPPORT_QA_MONGO_RUNTIME_PATH,',
|
|
1358
|
-
' process.env.RESOLVEIO_RUNNER_QA_MONGO_RUNTIME_PATH',
|
|
1359
|
-
' ].map((value) => String(value || "").trim()).filter(Boolean);',
|
|
1360
|
-
' const candidates = [',
|
|
1361
|
-
' ...explicitRuntimePaths,',
|
|
1362
|
-
' path.join(repoRoot, "mongo-context", ".mongo-runtime.json"),',
|
|
1363
|
-
' path.join(projectRoot, "mongo-context", ".mongo-runtime.json"),',
|
|
1364
|
-
' path.join(repoRoot, ".resolveio-support-context", "mongo-context", ".mongo-runtime.json"),',
|
|
1365
|
-
' path.join(projectRoot, ".resolveio-support-context", "mongo-context", ".mongo-runtime.json"),',
|
|
1366
|
-
' path.join(repoRoot, ".resolveio-context", "mongo-context", ".mongo-runtime.json"),',
|
|
1367
|
-
' path.join(projectRoot, ".resolveio-context", "mongo-context", ".mongo-runtime.json")',
|
|
1368
|
-
' ];',
|
|
1369
|
-
' for (const candidate of candidates) {',
|
|
1370
|
-
' const parsed = readJsonIfExists(candidate);',
|
|
1371
|
-
' if (parsed && parsed.mongo_uri) return { uri: String(parsed.mongo_uri || ""), database: String(parsed.mongo_db || "") };',
|
|
1372
|
-
' }',
|
|
1373
|
-
' const envUri = process.env.RESOLVEIO_QA_LIVE_MONGO_URL || process.env.RESOLVEIO_SUPPORT_QA_LIVE_MONGO_URL || process.env.RESOLVEIO_RUNNER_QA_LIVE_MONGO_URL || "";',
|
|
1374
|
-
' const envDb = process.env.RESOLVEIO_QA_LIVE_MONGO_DB || process.env.RESOLVEIO_SUPPORT_QA_LIVE_MONGO_DB || process.env.RESOLVEIO_RUNNER_QA_LIVE_MONGO_DB || "";',
|
|
1375
|
-
' if (envUri) return { uri: envUri, database: envDb };',
|
|
1376
|
-
' return { uri: "", database: "" };',
|
|
1377
|
-
'}',
|
|
1378
|
-
'',
|
|
1379
|
-
'function redactUri(uri) {',
|
|
1380
|
-
' return String(uri || "").replace(/(mongodb(?:\\+srv)?:\\/\\/)([^:@/?#]+):([^@/?#]+)@/i, "$1$2:***@");',
|
|
1381
|
-
'}',
|
|
1382
|
-
'',
|
|
1383
|
-
'function isLocalMongoUri(uri) {',
|
|
1384
|
-
' return /mongodb(?:\\+srv)?:\\/\\/(?:[^@/]+@)?(?:127\\.0\\.0\\.1|localhost)(?::\\d+)?\\//i.test(String(uri || ""));',
|
|
1385
|
-
'}',
|
|
1386
|
-
'',
|
|
1387
|
-
'function isLiveDataRequired() {',
|
|
1388
|
-
' return /^(true|1|yes|on)$/i.test(String(process.env.RESOLVEIO_QA_LIVE_DATA_REQUIRED || process.env.RESOLVEIO_SUPPORT_QA_LIVE_DATA_REQUIRED || process.env.RESOLVEIO_RUNNER_QA_LIVE_DATA_REQUIRED || ""));',
|
|
1389
|
-
'}',
|
|
1390
|
-
'',
|
|
1391
|
-
'function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }',
|
|
1392
|
-
'',
|
|
1393
|
-
'async function waitForTargetMongo(MongoClient, targetUri) {',
|
|
1394
|
-
' const deadline = Date.now() + Number(process.env.RESOLVEIO_QA_LOCAL_MONGO_WAIT_MS || process.env.RESOLVEIO_SUPPORT_QA_LOCAL_MONGO_WAIT_MS || process.env.RESOLVEIO_RUNNER_QA_LOCAL_MONGO_WAIT_MS || 60000);',
|
|
1395
|
-
' let lastError = null;',
|
|
1396
|
-
' while (Date.now() < deadline) {',
|
|
1397
|
-
' const client = new MongoClient(targetUri, { serverSelectionTimeoutMS: 2500 });',
|
|
1398
|
-
' try {',
|
|
1399
|
-
' await client.connect();',
|
|
1400
|
-
' const admin = client.db("admin");',
|
|
1401
|
-
' await admin.command({ ping: 1 });',
|
|
1402
|
-
' let hello = null;',
|
|
1403
|
-
' try { hello = await admin.command({ hello: 1 }); } catch (error) {',
|
|
1404
|
-
' try { hello = await admin.command({ isMaster: 1 }); } catch (inner) { hello = null; }',
|
|
1405
|
-
' }',
|
|
1406
|
-
' const writable = !hello || hello.isWritablePrimary === true || hello.ismaster === true || !hello.setName;',
|
|
1407
|
-
' if (!writable) {',
|
|
1408
|
-
' lastError = new Error(`target_local_mongo_not_primary: ${hello && (hello.primary || hello.me || hello.setName) || "unknown"}`);',
|
|
1409
|
-
' await client.close().catch(() => undefined);',
|
|
1410
|
-
' await delay(1500);',
|
|
1411
|
-
' continue;',
|
|
1412
|
-
' }',
|
|
1413
|
-
' await client.close().catch(() => undefined);',
|
|
1414
|
-
' return;',
|
|
1415
|
-
' } catch (error) {',
|
|
1416
|
-
' lastError = error;',
|
|
1417
|
-
' await client.close().catch(() => undefined);',
|
|
1418
|
-
' await delay(1500);',
|
|
1419
|
-
' }',
|
|
1420
|
-
' }',
|
|
1421
|
-
' const detail = lastError && (lastError.message || String(lastError)) || "local Mongo did not become ready";',
|
|
1422
|
-
' throw new Error(`target_local_mongo_unavailable: ${detail}`);',
|
|
1423
|
-
'}',
|
|
1424
|
-
'',
|
|
1425
|
-
'function requireMongo() {',
|
|
1426
|
-
' const candidates = [',
|
|
1427
|
-
' path.join(projectRoot, "server", "node_modules", "mongodb"),',
|
|
1428
|
-
' path.join(projectRoot, "node_modules", "mongodb"),',
|
|
1429
|
-
' path.join(repoRoot, "node_modules", "mongodb"),',
|
|
1430
|
-
' "mongodb"',
|
|
1431
|
-
' ];',
|
|
1432
|
-
' const errors = [];',
|
|
1433
|
-
' for (const candidate of candidates) {',
|
|
1434
|
-
' try { return require(candidate); } catch (error) { errors.push(`${candidate}: ${error.message}`); }',
|
|
1435
|
-
' }',
|
|
1436
|
-
' throw new Error(`Unable to require mongodb package. ${errors.join(" | ")}`);',
|
|
1437
|
-
'}',
|
|
1438
|
-
'',
|
|
1439
|
-
'function idValues(docs, keys) {',
|
|
1440
|
-
' const values = new Set();',
|
|
1441
|
-
' for (const doc of docs || []) {',
|
|
1442
|
-
' for (const key of keys) {',
|
|
1443
|
-
' const value = doc && doc[key];',
|
|
1444
|
-
' if (typeof value === "string" && value.trim()) values.add(value.trim());',
|
|
1445
|
-
' if (Array.isArray(value)) value.forEach((entry) => typeof entry === "string" && entry.trim() && values.add(entry.trim()));',
|
|
1446
|
-
' }',
|
|
1447
|
-
' }',
|
|
1448
|
-
' return Array.from(values);',
|
|
1449
|
-
'}',
|
|
1450
|
-
'',
|
|
1451
|
-
'function unique(values) { return Array.from(new Set((values || []).filter(Boolean).map(String))); }',
|
|
1452
|
-
'',
|
|
1453
|
-
'function lowerTrim(value) { return String(value || "").trim().toLowerCase(); }',
|
|
1454
|
-
'',
|
|
1455
|
-
'function escapeRegexText(value) { return String(value || "").replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&"); }',
|
|
1456
|
-
'',
|
|
1457
|
-
'function exactTextRegex(value) { return new RegExp(`^${escapeRegexText(value)}$`, "i"); }',
|
|
1458
|
-
'',
|
|
1459
|
-
'function requireOptionalXlsx() {',
|
|
1460
|
-
' const candidates = [',
|
|
1461
|
-
' path.join(projectRoot, "server", "node_modules", "xlsx"),',
|
|
1462
|
-
' path.join(projectRoot, "node_modules", "xlsx"),',
|
|
1463
|
-
' path.join(repoRoot, "node_modules", "xlsx"),',
|
|
1464
|
-
' path.join(process.cwd(), "server", "node_modules", "xlsx"),',
|
|
1465
|
-
' path.join(process.cwd(), "node_modules", "xlsx"),',
|
|
1466
|
-
' "xlsx"',
|
|
1467
|
-
' ];',
|
|
1468
|
-
' for (const candidate of candidates) { try { return require(candidate); } catch (error) {} }',
|
|
1469
|
-
' return null;',
|
|
1470
|
-
'}',
|
|
1471
|
-
'',
|
|
1472
|
-
'function collectPricingImportWorkbookPaths() {',
|
|
1473
|
-
' const dirs = [',
|
|
1474
|
-
' path.join(projectRoot, ".resolveio-context", "attachments"),',
|
|
1475
|
-
' path.join(repoRoot, ".resolveio-context", "attachments"),',
|
|
1476
|
-
' path.join(projectRoot, ".resolveio-support-context", "attachments"),',
|
|
1477
|
-
' path.join(repoRoot, ".resolveio-support-context", "attachments")',
|
|
1478
|
-
' ];',
|
|
1479
|
-
' const paths = [];',
|
|
1480
|
-
' for (const dir of dirs) {',
|
|
1481
|
-
' try {',
|
|
1482
|
-
' for (const name of fs.readdirSync(dir)) {',
|
|
1483
|
-
' if (/\\.xlsx$/i.test(name) && !/^~\\$/.test(name)) paths.push(path.join(dir, name));',
|
|
1484
|
-
' }',
|
|
1485
|
-
' } catch (error) {}',
|
|
1486
|
-
' }',
|
|
1487
|
-
' return unique(paths);',
|
|
1488
|
-
'}',
|
|
1489
|
-
'',
|
|
1490
|
-
'function normalizeWorkbookCell(value) {',
|
|
1491
|
-
' if (value == null) return "";',
|
|
1492
|
-
' if (value instanceof Date) return value.toISOString();',
|
|
1493
|
-
' return String(value).replace(/\\s+/g, " ").trim();',
|
|
1494
|
-
'}',
|
|
1495
|
-
'',
|
|
1496
|
-
'function extractPricingImportWorkbookHints(summary) {',
|
|
1497
|
-
' const workbooks = collectPricingImportWorkbookPaths();',
|
|
1498
|
-
' const xlsx = workbooks.length ? requireOptionalXlsx() : null;',
|
|
1499
|
-
' const customerNames = new Set();',
|
|
1500
|
-
' const itemMap = new Map();',
|
|
1501
|
-
' const workbookSummaries = [];',
|
|
1502
|
-
' if (!workbooks.length) return { workbooks, workbook_summaries: workbookSummaries, customer_names: [], items: [] };',
|
|
1503
|
-
' if (!xlsx) {',
|
|
1504
|
-
' summary.notes.push("Pricing-import workbook seed context skipped because xlsx could not be required in the QA seeder.");',
|
|
1505
|
-
' return { workbooks, workbook_summaries: workbookSummaries, customer_names: [], items: [] };',
|
|
1506
|
-
' }',
|
|
1507
|
-
' for (const workbookPath of workbooks.slice(0, 8)) {',
|
|
1508
|
-
' try {',
|
|
1509
|
-
' const workbook = xlsx.readFile(workbookPath, { cellDates: true });',
|
|
1510
|
-
' const sheetName = workbook.SheetNames.includes("Prices") ? "Prices" : workbook.SheetNames[0];',
|
|
1511
|
-
' const rows = sheetName ? xlsx.utils.sheet_to_json(workbook.Sheets[sheetName], { defval: "" }) : [];',
|
|
1512
|
-
' for (const row of rows) {',
|
|
1513
|
-
' const customer = normalizeWorkbookCell(row.Customer || row.customer || row.CustomerName || row["Customer Name"]);',
|
|
1514
|
-
' const pricingType = normalizeWorkbookCell(row["Pricing Type"] || row.PricingType || row.Type || row.type);',
|
|
1515
|
-
' const name = normalizeWorkbookCell(row.Name || row.name || row.Item || row.item || row.Chemical || row.chemical);',
|
|
1516
|
-
' if (customer) customerNames.add(customer);',
|
|
1517
|
-
' if (name) itemMap.set(`${lowerTrim(pricingType)}::${lowerTrim(name)}`, { name, type: pricingType });',
|
|
1518
|
-
' }',
|
|
1519
|
-
' workbookSummaries.push({ name: path.basename(workbookPath), sheetName, rows: rows.length });',
|
|
1520
|
-
' } catch (error) {',
|
|
1521
|
-
' workbookSummaries.push({ name: path.basename(workbookPath), error: error && (error.message || String(error)) || String(error) });',
|
|
1522
|
-
' }',
|
|
1523
|
-
' }',
|
|
1524
|
-
' return { workbooks, workbook_summaries: workbookSummaries, customer_names: Array.from(customerNames).slice(0, 300), items: Array.from(itemMap.values()).slice(0, 700) };',
|
|
1525
|
-
'}',
|
|
1526
|
-
'',
|
|
1527
|
-
'async function copyPricingImportWorkbookContext(sourceDb, targetDb, summary) {',
|
|
1528
|
-
' if (!shouldSeedPricingImportFixtures()) return [];',
|
|
1529
|
-
' const hints = extractPricingImportWorkbookHints(summary);',
|
|
1530
|
-
' summary.selected.pricing_import_workbook_seed = {',
|
|
1531
|
-
' workbook_names: hints.workbooks.map((entry) => path.basename(entry)),',
|
|
1532
|
-
' workbook_summaries: hints.workbook_summaries,',
|
|
1533
|
-
' customer_name_count: hints.customer_names.length,',
|
|
1534
|
-
' item_name_count: hints.items.length',
|
|
1535
|
-
' };',
|
|
1536
|
-
' if (!hints.workbooks.length) {',
|
|
1537
|
-
' summary.notes.push("Pricing-import seed profile found no attached .xlsx workbooks to derive customer/item fixture context.");',
|
|
1538
|
-
' return [];',
|
|
1539
|
-
' }',
|
|
1540
|
-
' const customerRegexes = hints.customer_names.map(exactTextRegex);',
|
|
1541
|
-
' const customers = customerRegexes.length ? await copyQuery(sourceDb, targetDb, "customers", { $or: [{ name: { $in: customerRegexes } }, { customer: { $in: customerRegexes } }, { company_name: { $in: customerRegexes } }] }, summary, 300, { name: 1 }) : [];',
|
|
1542
|
-
' const itemOrs = [];',
|
|
1543
|
-
' for (const hint of hints.items) {',
|
|
1544
|
-
' const nameRegex = exactTextRegex(hint.name);',
|
|
1545
|
-
' const typeRegex = hint.type ? exactTextRegex(hint.type) : null;',
|
|
1546
|
-
' if (typeRegex) itemOrs.push({ name: nameRegex, type: typeRegex }, { description: nameRegex, type: typeRegex });',
|
|
1547
|
-
' itemOrs.push({ name: nameRegex }, { description: nameRegex });',
|
|
1548
|
-
' }',
|
|
1549
|
-
' const items = itemOrs.length ? await copyQuery(sourceDb, targetDb, "items", { $or: itemOrs }, summary, 900, { name: 1 }) : [];',
|
|
1550
|
-
' const itemNameRegexes = hints.items.map((hint) => exactTextRegex(hint.name));',
|
|
1551
|
-
' const chemicals = itemNameRegexes.length ? await copyQuery(sourceDb, targetDb, "chemicals", { $or: [{ name: { $in: itemNameRegexes } }, { description: { $in: itemNameRegexes } }] }, summary, 900, { name: 1 }) : [];',
|
|
1552
|
-
' const chemicalIds = unique(chemicals.map((doc) => doc._id));',
|
|
1553
|
-
' const chemicalItems = chemicalIds.length ? await copyQuery(sourceDb, targetDb, "items", { $or: [{ id_chemical: { $in: chemicalIds } }, { _id: { $in: chemicalIds } }] }, summary, 900, { name: 1 }) : [];',
|
|
1554
|
-
' const copiedItems = [...items, ...chemicalItems];',
|
|
1555
|
-
' const copiedCustomerNames = new Set(customers.map((doc) => lowerTrim(doc.name || doc.customer || doc.company_name)));',
|
|
1556
|
-
' const copiedItemNames = new Set(copiedItems.map((doc) => lowerTrim(doc.name || doc.description)));',
|
|
1557
|
-
' const copiedChemicalNames = new Set(chemicals.map((doc) => lowerTrim(doc.name || doc.description)));',
|
|
1558
|
-
' const missingCustomers = hints.customer_names.filter((name) => !copiedCustomerNames.has(lowerTrim(name)));',
|
|
1559
|
-
' const missingItems = hints.items.filter((hint) => !copiedItemNames.has(lowerTrim(hint.name)) && !copiedChemicalNames.has(lowerTrim(hint.name))).map((hint) => hint.type ? `${hint.type}:${hint.name}` : hint.name);',
|
|
1560
|
-
' const itemIds = unique(copiedItems.map((doc) => doc._id));',
|
|
1561
|
-
' const customerIds = unique(customers.map((doc) => doc._id));',
|
|
1562
|
-
' summary.selected.pricing_import_workbook_context = {',
|
|
1563
|
-
' workbook_names: hints.workbooks.map((entry) => path.basename(entry)),',
|
|
1564
|
-
' customer_names: hints.customer_names.slice(0, 60),',
|
|
1565
|
-
' item_names: hints.items.map((hint) => hint.type ? `${hint.type}:${hint.name}` : hint.name).slice(0, 100),',
|
|
1566
|
-
' customer_ids: customerIds,',
|
|
1567
|
-
' item_ids: itemIds,',
|
|
1568
|
-
' chemical_ids: chemicalIds,',
|
|
1569
|
-
' missing_customer_names: missingCustomers.slice(0, 80),',
|
|
1570
|
-
' missing_item_names: missingItems.slice(0, 120)',
|
|
1571
|
-
' };',
|
|
1572
|
-
' summary.notes.push(`Seeded pricing-import workbook context: ${customers.length}/${hints.customer_names.length} customer name(s), ${copiedItems.length}/${hints.items.length} item name(s), ${chemicals.length} chemical(s) from ${hints.workbooks.length} attached workbook(s).`);',
|
|
1573
|
-
' if (missingCustomers.length || missingItems.length) summary.notes.push(`Pricing-import workbook context still missing ${missingCustomers.length} customer name(s) and ${missingItems.length} item name(s) from live Mongo.`);',
|
|
1574
|
-
' return [...customers, ...copiedItems, ...chemicals];',
|
|
1575
|
-
'}',
|
|
1576
|
-
'',
|
|
1577
|
-
'function collectEmailLikeValues(value, out = new Set(), depth = 0) {',
|
|
1578
|
-
' if (depth > 6 || value == null) return out;',
|
|
1579
|
-
' if (typeof value === "string") {',
|
|
1580
|
-
' const matches = value.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/gi) || [];',
|
|
1581
|
-
' matches.forEach((email) => out.add(String(email || "").trim().toLowerCase()));',
|
|
1582
|
-
' return out;',
|
|
1583
|
-
' }',
|
|
1584
|
-
' if (Array.isArray(value)) { value.forEach((entry) => collectEmailLikeValues(entry, out, depth + 1)); return out; }',
|
|
1585
|
-
' if (typeof value === "object") { Object.values(value).forEach((entry) => collectEmailLikeValues(entry, out, depth + 1)); }',
|
|
1586
|
-
' return out;',
|
|
1587
|
-
'}',
|
|
1588
|
-
'',
|
|
1589
|
-
'function readContextJsonFiles() {',
|
|
1590
|
-
' const files = [',
|
|
1591
|
-
' path.join(artifactDir, "manual-ticket.metadata.json"),',
|
|
1592
|
-
' path.join(artifactDir, "email.metadata.json"),',
|
|
1593
|
-
' path.join(repoRoot, ".resolveio-support-context", "manual-ticket.metadata.json"),',
|
|
1594
|
-
' path.join(projectRoot, ".resolveio-support-context", "manual-ticket.metadata.json"),',
|
|
1595
|
-
' path.join(repoRoot, ".resolveio-support-context", "email.metadata.json"),',
|
|
1596
|
-
' path.join(projectRoot, ".resolveio-support-context", "email.metadata.json"),',
|
|
1597
|
-
' path.join(repoRoot, ".resolveio-context", "manual-ticket.metadata.json"),',
|
|
1598
|
-
' path.join(projectRoot, ".resolveio-context", "manual-ticket.metadata.json"),',
|
|
1599
|
-
' path.join(repoRoot, ".resolveio-context", "email.metadata.json"),',
|
|
1600
|
-
' path.join(projectRoot, ".resolveio-context", "email.metadata.json")',
|
|
1601
|
-
' ];',
|
|
1602
|
-
' return files.map((file) => readJsonIfExists(file)).filter(Boolean);',
|
|
1603
|
-
'}',
|
|
1604
|
-
'',
|
|
1605
|
-
'function extractQaUserHints() {',
|
|
1606
|
-
' const affected = unique([',
|
|
1607
|
-
' process.env.RESOLVEIO_QA_AFFECTED_USER_EMAIL,',
|
|
1608
|
-
' process.env.RESOLVEIO_SUPPORT_QA_AFFECTED_USER_EMAIL,',
|
|
1609
|
-
' process.env.RESOLVEIO_RUNNER_QA_AFFECTED_USER_EMAIL',
|
|
1610
|
-
' ].map((value) => String(value || "").trim().toLowerCase()).filter(Boolean));',
|
|
1611
|
-
' const reporter = unique([',
|
|
1612
|
-
' process.env.RESOLVEIO_QA_TICKET_REPORTER_EMAIL,',
|
|
1613
|
-
' process.env.RESOLVEIO_SUPPORT_QA_TICKET_REPORTER_EMAIL,',
|
|
1614
|
-
' process.env.RESOLVEIO_RUNNER_QA_TICKET_REPORTER_EMAIL',
|
|
1615
|
-
' ].map((value) => String(value || "").trim().toLowerCase()).filter(Boolean));',
|
|
1616
|
-
' const explicitQa = unique([',
|
|
1617
|
-
' process.env.RESOLVEIO_QA_USERNAME,',
|
|
1618
|
-
' process.env.RESOLVEIO_SUPPORT_QA_USERNAME,',
|
|
1619
|
-
' process.env.RESOLVEIO_RUNNER_QA_USERNAME',
|
|
1620
|
-
' ].map((value) => String(value || "").trim().toLowerCase()).filter((value) => value && !/^(admin|dev@resolveio\\.com)$/i.test(value)));',
|
|
1621
|
-
' const contextEmails = new Set();',
|
|
1622
|
-
' readContextJsonFiles().forEach((json) => collectEmailLikeValues(json, contextEmails));',
|
|
1623
|
-
' collectEmailLikeValues(readSeedHintText(), contextEmails);',
|
|
1624
|
-
' const candidates = unique([...affected, ...reporter, ...explicitQa, ...Array.from(contextEmails)]);',
|
|
1625
|
-
' return { affected, reporter, explicitQa, candidates: candidates.slice(0, 12) };',
|
|
1626
|
-
'}',
|
|
1627
|
-
'',
|
|
1628
|
-
'function qaUserLookupQuery(candidates) {',
|
|
1629
|
-
' const ors = [];',
|
|
1630
|
-
' for (const candidate of candidates || []) {',
|
|
1631
|
-
' const value = String(candidate || "").trim();',
|
|
1632
|
-
' if (!value) continue;',
|
|
1633
|
-
' ors.push({ username: value }, { email: value }, { "emails.address": value });',
|
|
1634
|
-
' }',
|
|
1635
|
-
' return ors.length ? { $or: ors } : null;',
|
|
1636
|
-
'}',
|
|
1637
|
-
'',
|
|
1638
|
-
'function normalizeSeededQaUser(doc, password) {',
|
|
1639
|
-
' const now = new Date();',
|
|
1640
|
-
' const next = { ...(doc || {}) };',
|
|
1641
|
-
' const other = next.other && typeof next.other === "object" ? next.other : {};',
|
|
1642
|
-
' const settings = next.settings && typeof next.settings === "object" ? next.settings : {};',
|
|
1643
|
-
' next.active = true;',
|
|
1644
|
-
' next.readonly = false;',
|
|
1645
|
-
' next.other = { ...other, tour_completed: true, took_tour: true, core_tour_completed: true, welcome_tour_completed: true, top_navigation_tour_completed: true, user_settings_tour_completed: true };',
|
|
1646
|
-
' next.settings = { ...settings, collapsable_menu: false };',
|
|
1647
|
-
' next.updatedAt = now;',
|
|
1648
|
-
' if (password) {',
|
|
1649
|
-
' const salt = crypto.randomBytes(32).toString("hex");',
|
|
1650
|
-
' next.salt = salt;',
|
|
1651
|
-
' next.hash = crypto.pbkdf2Sync(password, salt, 25000, 512, "sha256").toString("hex");',
|
|
1652
|
-
' if (next.services && typeof next.services === "object" && next.services.password) delete next.services.password;',
|
|
1653
|
-
' next.attempts = 0;',
|
|
1654
|
-
' }',
|
|
1655
|
-
' return next;',
|
|
1656
|
-
'}',
|
|
1657
|
-
'',
|
|
1658
|
-
'async function copyQaIdentityUsers(sourceDb, targetDb, summary) {',
|
|
1659
|
-
' const hints = extractQaUserHints();',
|
|
1660
|
-
' summary.selected.qa_user_hints = hints;',
|
|
1661
|
-
' const query = qaUserLookupQuery(hints.candidates);',
|
|
1662
|
-
' if (!query) return [];',
|
|
1663
|
-
' const users = await sourceDb.collection("users").find(query).limit(8).toArray();',
|
|
1664
|
-
' if (!users.length) {',
|
|
1665
|
-
' summary.notes.push(`No live user matched QA identity hints: ${hints.candidates.join(", ")}`);',
|
|
1666
|
-
' return [];',
|
|
1667
|
-
' }',
|
|
1668
|
-
' const password = process.env.RESOLVEIO_RUNNER_QA_PASSWORD || process.env.RESOLVEIO_SUPPORT_QA_PASSWORD || "";',
|
|
1669
|
-
' for (const user of users) {',
|
|
1670
|
-
' const normalized = normalizeSeededQaUser(user, password);',
|
|
1671
|
-
' await targetDb.collection("users").replaceOne({ _id: normalized._id }, normalized, { upsert: true });',
|
|
1672
|
-
' }',
|
|
1673
|
-
' summary.collections.users = (summary.collections.users || 0) + users.length;',
|
|
1674
|
-
' const preferred = users.find((user) => hints.affected.includes(String(user.email || user.username || "").toLowerCase()))',
|
|
1675
|
-
' || users.find((user) => hints.reporter.includes(String(user.email || user.username || "").toLowerCase()))',
|
|
1676
|
-
' || users[0];',
|
|
1677
|
-
' summary.selected.qa_user_context = {',
|
|
1678
|
-
' preferred_username: String(preferred && (preferred.email || preferred.username) || "").trim(),',
|
|
1679
|
-
' preferred_email: String(preferred && preferred.email || "").trim(),',
|
|
1680
|
-
' preferred_user_id: String(preferred && preferred._id || "").trim(),',
|
|
1681
|
-
' affected_user_email: hints.affected[0] || "",',
|
|
1682
|
-
' reporter_email: hints.reporter[0] || "",',
|
|
1683
|
-
' copied_user_ids: users.map((user) => String(user && user._id || "")).filter(Boolean),',
|
|
1684
|
-
' reason: hints.affected.length ? "affected_user" : (hints.reporter.length ? "ticket_reporter" : "context_email")',
|
|
1685
|
-
' };',
|
|
1686
|
-
' summary.notes.push(`Seeded ${users.length} live QA identity user(s); preferred browser QA user is ${summary.selected.qa_user_context.preferred_username || summary.selected.qa_user_context.preferred_user_id}.`);',
|
|
1687
|
-
' return users;',
|
|
1688
|
-
'}',
|
|
1689
|
-
'',
|
|
1690
|
-
'function readSeedHintText() {',
|
|
1691
|
-
' const chunks = [',
|
|
1692
|
-
' process.env.RESOLVEIO_SUPPORT_QA_ROW_FILTER || "",',
|
|
1693
|
-
' process.env.RESOLVEIO_SUPPORT_QA_CURRENT_ROW_WORKFLOW || "",',
|
|
1694
|
-
' process.env.RESOLVEIO_SUPPORT_QA_CURRENT_ROW_ASSERTION || "",',
|
|
1695
|
-
' process.env.RESOLVEIO_RUNNER_QA_ROW_FILTER || "",',
|
|
1696
|
-
' process.env.RESOLVEIO_RUNNER_QA_CURRENT_ROW_WORKFLOW || "",',
|
|
1697
|
-
' process.env.RESOLVEIO_RUNNER_QA_CURRENT_ROW_ASSERTION || "",',
|
|
1698
|
-
' process.env.RESOLVEIO_QA_SEED_HINTS || "",',
|
|
1699
|
-
' process.env.RESOLVEIO_SUPPORT_QA_SEED_HINTS || "",',
|
|
1700
|
-
' process.env.RESOLVEIO_RUNNER_QA_SEED_HINTS || ""',
|
|
1701
|
-
' ];',
|
|
1702
|
-
' const candidates = [',
|
|
1703
|
-
' path.join(artifactDir, "qa-coverage-matrix.json"),',
|
|
1704
|
-
' path.join(artifactDir, "qa-row-lock.json"),',
|
|
1705
|
-
' path.join(repoRoot, ".resolveio-support-context", "manual-ticket.request.txt"),',
|
|
1706
|
-
' path.join(projectRoot, ".resolveio-support-context", "manual-ticket.request.txt"),',
|
|
1707
|
-
' path.join(repoRoot, ".resolveio-context", "manual-ticket.request.txt"),',
|
|
1708
|
-
' path.join(projectRoot, ".resolveio-context", "manual-ticket.request.txt")',
|
|
1709
|
-
' ];',
|
|
1710
|
-
' for (const candidate of candidates) {',
|
|
1711
|
-
' try {',
|
|
1712
|
-
' const raw = fs.readFileSync(candidate, "utf8");',
|
|
1713
|
-
' chunks.push(raw);',
|
|
1714
|
-
' if (/qa-coverage-matrix\\.json$/.test(candidate)) {',
|
|
1715
|
-
' const matrix = JSON.parse(raw);',
|
|
1716
|
-
' const rows = Array.isArray(matrix && matrix.rows) ? matrix.rows : [];',
|
|
1717
|
-
' for (const row of rows) chunks.push([row.workflow, row.route, row.assertion, row.required_proof, row.data_id, row.data_name, row.blocker, row.evidence, row.caption].filter(Boolean).join("\\n"));',
|
|
1718
|
-
' }',
|
|
1719
|
-
' if (/qa-row-lock\\.json$/.test(candidate)) {',
|
|
1720
|
-
' const lock = JSON.parse(raw);',
|
|
1721
|
-
' chunks.push([lock.workflow, lock.route, lock.assertion, lock.required_proof, lock.rowFilter, lock.blocker].filter(Boolean).join("\\n"));',
|
|
1722
|
-
' }',
|
|
1723
|
-
' } catch (error) {}',
|
|
1724
|
-
' }',
|
|
1725
|
-
' return chunks.filter(Boolean).join("\\n");',
|
|
1726
|
-
'}',
|
|
1727
|
-
'',
|
|
1728
|
-
'function isUsefulSeedIdentifier(value) {',
|
|
1729
|
-
' const cleaned = String(value || "").trim().replace(/^[#:\\-]+|[.,;:)\\]]+$/g, "");',
|
|
1730
|
-
' if (!cleaned) return false;',
|
|
1731
|
-
' if (/^00?4\\d{3}$/.test(cleaned)) return false;',
|
|
1732
|
-
' if (/^(screen|tab|are|which|and|render|context|workflow|build|snapshot|state|charge|plan|data|document|delivery|deliveries)$/i.test(cleaned)) return false;',
|
|
1733
|
-
' return /\\d/.test(cleaned) || /^[A-Z][A-Z0-9._-]{3,}$/.test(cleaned);',
|
|
1734
|
-
'}',
|
|
1735
|
-
'',
|
|
1736
|
-
'function extractBolIdentifiers() {',
|
|
1737
|
-
' const text = readSeedHintText();',
|
|
1738
|
-
' const identifiers = new Set();',
|
|
1739
|
-
' const explicit = /\\bBOL\\s*(?:#|no\\.?|number|num)?\\s*[:#-]?\\s*([A-Z0-9][A-Z0-9._-]{2,})\\b/gi;',
|
|
1740
|
-
' let match;',
|
|
1741
|
-
' while ((match = explicit.exec(text)) !== null) {',
|
|
1742
|
-
' const value = String(match[1] || "").trim();',
|
|
1743
|
-
' if (isUsefulSeedIdentifier(value)) identifiers.add(value);',
|
|
1744
|
-
' }',
|
|
1745
|
-
' return Array.from(identifiers).slice(0, 8);',
|
|
1746
|
-
'}',
|
|
1747
|
-
'',
|
|
1748
|
-
'function shouldAutoDiscoverTruckTreatingBolContext() {',
|
|
1749
|
-
' const text = readSeedHintText();',
|
|
1750
|
-
' return /\\b(bol|bill\\s+of\\s+lading|truck\\s*treat(?:ing)?|truck-treating|deliver(?:y|ed|ies)?\\s+BOL)\\b/i.test(text);',
|
|
1751
|
-
'}',
|
|
1752
|
-
'',
|
|
1753
|
-
'function shouldAutoDiscoverAssetContext() {',
|
|
1754
|
-
' const text = readSeedHintText();',
|
|
1755
|
-
' return /\\b(asset|assets|unit|units|equipment|current\\s+location|location\\s+name|location\\s+names|default\\s+yard|yard)\\b/i.test(text);',
|
|
1756
|
-
'}',
|
|
1757
|
-
'',
|
|
1758
|
-
'function shouldAutoDiscoverProductionInterchangeablesContext() {',
|
|
1759
|
-
' const text = readSeedHintText();',
|
|
1760
|
-
' return /\\b(interchangeable|interchangeables|mass\\s+update|update\\s+interchangeables|from\\s+chemical|to\\s+chemical|treatment\\s+plan|treatment\\s+plans|production\\s+location)\\b/i.test(text);',
|
|
1761
|
-
'}',
|
|
1762
|
-
'',
|
|
1763
|
-
'function extractAssetIdentifiers() {',
|
|
1764
|
-
' const text = readSeedHintText();',
|
|
1765
|
-
' const identifiers = new Set();',
|
|
1766
|
-
' const explicit = /\\b(?:asset|unit|equipment)\\s*(?:#|no\\.?|number|num)?\\s*[:#-]?\\s*([A-Z0-9][A-Z0-9._-]{1,})\\b/gi;',
|
|
1767
|
-
' let match;',
|
|
1768
|
-
' while ((match = explicit.exec(text)) !== null) {',
|
|
1769
|
-
' const value = String(match[1] || "").trim();',
|
|
1770
|
-
' if (isUsefulSeedIdentifier(value)) identifiers.add(value);',
|
|
1771
|
-
' }',
|
|
1772
|
-
' const typedUnit = /\\b(?:truck|trailer|unit|asset)\\s+([A-Z0-9][A-Z0-9._-]{1,})\\b/gi;',
|
|
1773
|
-
' while ((match = typedUnit.exec(text)) !== null) {',
|
|
1774
|
-
' const value = String(match[1] || "").trim();',
|
|
1775
|
-
' if (isUsefulSeedIdentifier(value)) identifiers.add(value);',
|
|
1776
|
-
' }',
|
|
1777
|
-
' if (shouldAutoDiscoverAssetContext()) extractSeedIdentifiers().forEach((value) => identifiers.add(value));',
|
|
1778
|
-
' return Array.from(identifiers).slice(0, 20);',
|
|
1779
|
-
'}',
|
|
1780
|
-
'',
|
|
1781
|
-
'function extractRouteNameHints() {',
|
|
1782
|
-
' const text = readSeedHintText();',
|
|
1783
|
-
' const routes = new Set();',
|
|
1784
|
-
' const routePattern = /\\b(?:on|for)\\s+(?:the\\s+)?([A-Za-z0-9][A-Za-z0-9 &._/-]{1,48}?)\\s+route\\b/gi;',
|
|
1785
|
-
' let match;',
|
|
1786
|
-
' while ((match = routePattern.exec(text)) !== null) {',
|
|
1787
|
-
' const routeName = String(match[1] || "").trim().replace(/^(?:the|a|an)\\s+/i, "").replace(/\\s+/g, " ");',
|
|
1788
|
-
' if (routeName && !/^(?:this|that|selected)$/i.test(routeName)) routes.add(routeName);',
|
|
1789
|
-
' }',
|
|
1790
|
-
' return Array.from(routes).slice(0, 8);',
|
|
1791
|
-
'}',
|
|
1792
|
-
'',
|
|
1793
|
-
'function extractSeedIdentifiers() {',
|
|
1794
|
-
' const text = readSeedHintText();',
|
|
1795
|
-
' const identifiers = new Set();',
|
|
1796
|
-
' const explicit = /\\b(?:invoice|inv|bol|order|pso|ticket|wo|work\\s*order|delivery|treatment)\\s*(?:#|no\\.?|number|num)?\\s*[:#-]?\\s*([A-Z0-9][A-Z0-9._-]{2,})\\b/gi;',
|
|
1797
|
-
' let match;',
|
|
1798
|
-
' while ((match = explicit.exec(text)) !== null) identifiers.add(match[1]);',
|
|
1799
|
-
' const typedUnit = /\\b(?:truck|trailer|asset|unit)\\s+([A-Z0-9][A-Z0-9._-]{1,})\\b/gi;',
|
|
1800
|
-
' while ((match = typedUnit.exec(text)) !== null) identifiers.add(match[1]);',
|
|
1801
|
-
' const numeric = /\\b\\d{3,}\\b/g;',
|
|
1802
|
-
' while ((match = numeric.exec(text)) !== null) identifiers.add(match[0]);',
|
|
1803
|
-
' return Array.from(identifiers)',
|
|
1804
|
-
' .map((value) => String(value || "").trim())',
|
|
1805
|
-
' .filter((value) => isUsefulSeedIdentifier(value))',
|
|
1806
|
-
' .slice(0, 20);',
|
|
1807
|
-
'}',
|
|
1808
|
-
'',
|
|
1809
|
-
'function desiredSeedProfile() {',
|
|
1810
|
-
' if (shouldSeedPricingImportFixtures()) return "pricing_import";',
|
|
1811
|
-
' if (shouldSeedBillingDashboardFixtures()) return "billing_inventory";',
|
|
1812
|
-
' return "live_context";',
|
|
1813
|
-
'}',
|
|
1814
|
-
'',
|
|
1815
|
-
'function shouldSeedBillingDashboardFixtures() {',
|
|
1816
|
-
' const force = process.env.RESOLVEIO_QA_FORCE_BILLING_FIXTURES || process.env.RESOLVEIO_SUPPORT_QA_FORCE_BILLING_FIXTURES || process.env.RESOLVEIO_RUNNER_QA_FORCE_BILLING_FIXTURES || "";',
|
|
1817
|
-
' if (/^(true|1|yes|on)$/i.test(force)) return true;',
|
|
1818
|
-
' if (shouldSeedPricingImportFixtures()) return false;',
|
|
1819
|
-
' const explicit = process.env.RESOLVEIO_QA_INCLUDE_BILLING_FIXTURES || process.env.RESOLVEIO_SUPPORT_QA_INCLUDE_BILLING_FIXTURES || process.env.RESOLVEIO_RUNNER_QA_INCLUDE_BILLING_FIXTURES || "";',
|
|
1820
|
-
' if (/^(true|1|yes|on)$/i.test(explicit)) return true;',
|
|
1821
|
-
' if (/^(false|0|no|off)$/i.test(explicit)) return false;',
|
|
1822
|
-
' const text = readSeedHintText();',
|
|
1823
|
-
' return /\\b(invoice|invoicing|billing|billable|tax|taxes|surcharge|inventory|checkout|check\\s*out|pick\\s*ticket)\\b/i.test(text);',
|
|
1824
|
-
'}',
|
|
1825
|
-
'',
|
|
1826
|
-
'function shouldSeedPricingImportFixtures() {',
|
|
1827
|
-
' const explicit = process.env.RESOLVEIO_QA_INCLUDE_PRICING_IMPORT_FIXTURES || process.env.RESOLVEIO_SUPPORT_QA_INCLUDE_PRICING_IMPORT_FIXTURES || process.env.RESOLVEIO_RUNNER_QA_INCLUDE_PRICING_IMPORT_FIXTURES || "";',
|
|
1828
|
-
' if (/^(true|1|yes|on)$/i.test(explicit)) return true;',
|
|
1829
|
-
' if (/^(false|0|no|off)$/i.test(explicit)) return false;',
|
|
1830
|
-
' const text = readSeedHintText();',
|
|
1831
|
-
' const pricingSurface = /\\b(pricing|price|gross\\s+price|production\\s+price|manage\\s+pricing)\\b/i.test(text);',
|
|
1832
|
-
' const importSurface = /\\b(import|upload|template|spreadsheet|xlsx|excel|attached\\s+template)\\b/i.test(text);',
|
|
1833
|
-
' return pricingSurface && importSurface;',
|
|
1834
|
-
'}',
|
|
1835
|
-
'',
|
|
1836
|
-
'function identifierQuery(identifiers) {',
|
|
1837
|
-
' const ors = [];',
|
|
1838
|
-
' const fields = ["_id", "invoice_number", "invoice_number_string", "order_number", "order_number_string", "activity_number", "bol_number", "bol_string", "ticket_number", "ticket", "wo_number", "work_order_number", "delivery_number", "treatment_number"];',
|
|
1839
|
-
' for (const identifier of identifiers || []) {',
|
|
1840
|
-
' const escaped = String(identifier).replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&");',
|
|
1841
|
-
' const regex = new RegExp(escaped, "i");',
|
|
1842
|
-
' for (const field of fields) ors.push({ [field]: regex });',
|
|
1843
|
-
' ors.push({ "bol.bol_string": regex });',
|
|
1844
|
-
' ors.push({ "bol.bol_number": regex });',
|
|
1845
|
-
' }',
|
|
1846
|
-
' return ors.length ? { $or: ors } : null;',
|
|
1847
|
-
'}',
|
|
1848
|
-
'',
|
|
1849
|
-
'function assetLookupQuery(identifiers) {',
|
|
1850
|
-
' const ors = [];',
|
|
1851
|
-
' const fields = ["_id", "asset_number", "asset_number_string", "asset_unit_number", "unit_number", "number", "serial_number", "name"];',
|
|
1852
|
-
' for (const identifier of identifiers || []) {',
|
|
1853
|
-
' const value = String(identifier || "").trim();',
|
|
1854
|
-
' if (!value) continue;',
|
|
1855
|
-
' const escaped = value.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&");',
|
|
1856
|
-
' const exactText = new RegExp(`^${escaped}$`, "i");',
|
|
1857
|
-
' const containsText = new RegExp(escaped, "i");',
|
|
1858
|
-
' for (const field of fields) ors.push({ [field]: exactText });',
|
|
1859
|
-
' for (const field of ["asset_number_string", "asset_unit_number", "unit_number", "number", "serial_number", "name"]) ors.push({ [field]: containsText });',
|
|
1860
|
-
' if (/^\\d+$/.test(value)) {',
|
|
1861
|
-
' const numericValue = Number(value);',
|
|
1862
|
-
' for (const field of ["asset_number", "unit_number", "number"]) ors.push({ [field]: numericValue });',
|
|
1863
|
-
' }',
|
|
1864
|
-
' }',
|
|
1865
|
-
' return ors.length ? { $or: ors } : null;',
|
|
1866
|
-
'}',
|
|
1867
|
-
'',
|
|
1868
|
-
'async function copyByIds(sourceDb, targetDb, collectionName, ids, summary, limit = 100) {',
|
|
1869
|
-
' const cleanIds = unique(ids).slice(0, limit);',
|
|
1870
|
-
' if (!cleanIds.length) return [];',
|
|
1871
|
-
' const docs = await sourceDb.collection(collectionName).find({ _id: { $in: cleanIds } }).limit(limit).toArray();',
|
|
1872
|
-
' for (const doc of docs) await targetDb.collection(collectionName).replaceOne({ _id: doc._id }, doc, { upsert: true });',
|
|
1873
|
-
' summary.collections[collectionName] = (summary.collections[collectionName] || 0) + docs.length;',
|
|
1874
|
-
' return docs;',
|
|
1875
|
-
'}',
|
|
1876
|
-
'',
|
|
1877
|
-
'async function copyQuery(sourceDb, targetDb, collectionName, query, summary, limit = 50, sort = null) {',
|
|
1878
|
-
' let cursor = sourceDb.collection(collectionName).find(query || {});',
|
|
1879
|
-
' if (sort) cursor = cursor.sort(sort);',
|
|
1880
|
-
' const docs = await cursor.limit(limit).toArray();',
|
|
1881
|
-
' for (const doc of docs) await targetDb.collection(collectionName).replaceOne({ _id: doc._id }, doc, { upsert: true });',
|
|
1882
|
-
' summary.collections[collectionName] = (summary.collections[collectionName] || 0) + docs.length;',
|
|
1883
|
-
' return docs;',
|
|
1884
|
-
'}',
|
|
1885
|
-
'',
|
|
1886
|
-
'function bolLookupQuery(identifiers) {',
|
|
1887
|
-
' const ors = [];',
|
|
1888
|
-
' for (const identifier of identifiers || []) {',
|
|
1889
|
-
' const value = String(identifier || "").trim();',
|
|
1890
|
-
' if (!value) continue;',
|
|
1891
|
-
' const escaped = value.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&");',
|
|
1892
|
-
' const contains = new RegExp(`(^|[^0-9A-Za-z])${escaped}([^0-9A-Za-z]|$)`, "i");',
|
|
1893
|
-
' ors.push({ _id: value }, { bol_number: value }, { bol_string: value }, { bol_number: contains }, { bol_string: contains });',
|
|
1894
|
-
' if (/^\\d+$/.test(value)) ors.push({ bol_number: Number(value) });',
|
|
1895
|
-
' }',
|
|
1896
|
-
' return ors.length ? { $or: ors } : null;',
|
|
1897
|
-
'}',
|
|
1898
|
-
'',
|
|
1899
|
-
'async function discoverTruckTreatingBolIds(sourceDb, routeHints, limit = 5) {',
|
|
1900
|
-
' const routeRegexes = (routeHints || []).map((hint) => new RegExp(String(hint).replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&"), "i"));',
|
|
1901
|
-
' const routeHintMatch = routeRegexes.length ? { $or: [',
|
|
1902
|
-
' { location: { $in: routeRegexes } },',
|
|
1903
|
-
' { well_group: { $in: routeRegexes } },',
|
|
1904
|
-
' { route: { $in: routeRegexes } },',
|
|
1905
|
-
' { route_name: { $in: routeRegexes } }',
|
|
1906
|
-
' ] } : null;',
|
|
1907
|
-
' const baseMatch = {',
|
|
1908
|
-
' id_bol: { $type: "string", $ne: "" },',
|
|
1909
|
-
' items: { $elemMatch: { item_type: { $in: ["Service", "Flush"] } } }',
|
|
1910
|
-
' };',
|
|
1911
|
-
' const pipeline = [',
|
|
1912
|
-
' { $match: routeHintMatch ? { $and: [baseMatch, routeHintMatch] } : baseMatch },',
|
|
1913
|
-
' { $sort: { date: -1, date_ship: -1, updatedAt: -1 } },',
|
|
1914
|
-
' { $group: { _id: "$id_bol", route_count: { $sum: 1 }, latest: { $max: "$updatedAt" } } },',
|
|
1915
|
-
' { $sort: { latest: -1, route_count: -1 } },',
|
|
1916
|
-
' { $limit: limit },',
|
|
1917
|
-
' { $project: { _id: 1 } }',
|
|
1918
|
-
' ];',
|
|
1919
|
-
' const rows = await sourceDb.collection("production-routes").aggregate(pipeline).toArray();',
|
|
1920
|
-
' return rows.map((row) => row && row._id).filter(Boolean);',
|
|
1921
|
-
'}',
|
|
1922
|
-
'',
|
|
1923
|
-
'function collectNestedStringValues(docs, keys) {',
|
|
1924
|
-
' const values = new Set();',
|
|
1925
|
-
' const visit = (value) => {',
|
|
1926
|
-
' if (!value) return;',
|
|
1927
|
-
' if (Array.isArray(value)) { value.forEach(visit); return; }',
|
|
1928
|
-
' if (typeof value === "object") {',
|
|
1929
|
-
' for (const key of keys) {',
|
|
1930
|
-
' const entry = value[key];',
|
|
1931
|
-
' if (typeof entry === "string" && entry.trim()) values.add(entry.trim());',
|
|
1932
|
-
' else if (Array.isArray(entry) || (entry && typeof entry === "object")) visit(entry);',
|
|
1933
|
-
' }',
|
|
1934
|
-
' for (const nested of Object.values(value)) if (Array.isArray(nested)) visit(nested);',
|
|
1935
|
-
' }',
|
|
1936
|
-
' };',
|
|
1937
|
-
' visit(docs || []);',
|
|
1938
|
-
' return Array.from(values);',
|
|
1939
|
-
'}',
|
|
1940
|
-
'',
|
|
1941
|
-
'async function copyTruckTreatingBolContext(sourceDb, targetDb, summary) {',
|
|
1942
|
-
' const bolIdentifiers = extractBolIdentifiers();',
|
|
1943
|
-
' const routeHints = extractRouteNameHints();',
|
|
1944
|
-
' summary.selected.bol_identifiers = bolIdentifiers;',
|
|
1945
|
-
' if (routeHints.length) summary.selected.route_name_hints = routeHints;',
|
|
1946
|
-
' let query = bolLookupQuery(bolIdentifiers);',
|
|
1947
|
-
' let discoveryMode = "";',
|
|
1948
|
-
' if (!query && shouldAutoDiscoverTruckTreatingBolContext()) {',
|
|
1949
|
-
' const discoveredBolIds = await discoverTruckTreatingBolIds(sourceDb, routeHints, 8);',
|
|
1950
|
-
' if (discoveredBolIds.length) {',
|
|
1951
|
-
' summary.selected.discovered_truck_treating_bol_ids = discoveredBolIds;',
|
|
1952
|
-
' query = { _id: { $in: discoveredBolIds } };',
|
|
1953
|
-
' discoveryMode = "auto_discovered_from_live_production_routes";',
|
|
1954
|
-
' }',
|
|
1955
|
-
' }',
|
|
1956
|
-
' if (!query) return [];',
|
|
1957
|
-
' let bols = await copyQuery(sourceDb, targetDb, "bols", query, summary, 8, { date_shipped: -1, date: -1, updatedAt: -1 });',
|
|
1958
|
-
' if (!bols.length) {',
|
|
1959
|
-
' summary.notes.push(`No live BOL matched ticket BOL identifiers: ${bolIdentifiers.join(", ")}`);',
|
|
1960
|
-
' if (shouldAutoDiscoverTruckTreatingBolContext()) {',
|
|
1961
|
-
' const discoveredBolIds = await discoverTruckTreatingBolIds(sourceDb, routeHints, 8);',
|
|
1962
|
-
' if (discoveredBolIds.length) {',
|
|
1963
|
-
' summary.selected.discovered_truck_treating_bol_ids = discoveredBolIds;',
|
|
1964
|
-
' discoveryMode = "auto_discovered_after_identifier_miss";',
|
|
1965
|
-
' bols = await copyQuery(sourceDb, targetDb, "bols", { _id: { $in: discoveredBolIds } }, summary, 8, { date_shipped: -1, date: -1, updatedAt: -1 });',
|
|
1966
|
-
' }',
|
|
1967
|
-
' }',
|
|
1968
|
-
' if (!bols.length) return [];',
|
|
1969
|
-
' }',
|
|
1970
|
-
' const bolIds = bols.map((doc) => doc._id).filter(Boolean);',
|
|
1971
|
-
' const productionRoutes = await copyQuery(sourceDb, targetDb, "production-routes", { id_bol: { $in: bolIds } }, summary, 600, { route_order: 1, route_number: 1 });',
|
|
1972
|
-
' const routeIds = productionRoutes.map((doc) => doc._id).filter(Boolean);',
|
|
1973
|
-
' const truckTreatingRouteIds = idValues(productionRoutes, ["id_truck_treating_route"]);',
|
|
1974
|
-
' const productionSalesOrderIds = idValues(productionRoutes, ["id_pso", "id_activity", "id_production_sales_order", "id_sales_order"]);',
|
|
1975
|
-
' const productionDeliveries = await copyQuery(sourceDb, targetDb, "production-deliveries", { $or: [{ id_bol: { $in: bolIds } }, { id_activity: { $in: productionSalesOrderIds } }] }, summary, 600, { route_order: 1, updatedAt: -1 });',
|
|
1976
|
-
' const routeTruckTreatDeliveryIds = collectNestedStringValues(productionRoutes, ["id_truck_treat_delivery", "id_truck_treating_delivery"]);',
|
|
1977
|
-
' const truckTreatingDeliveries = await copyQuery(sourceDb, targetDb, "truck-treating-deliveries", { $or: [{ id_bol: { $in: bolIds } }, { _id: { $in: routeTruckTreatDeliveryIds } }] }, summary, 600, { route_order: 1, updatedAt: -1 });',
|
|
1978
|
-
' const copiedTruckTreatingRoutes = await copyByIds(sourceDb, targetDb, "truck-treating-routes", truckTreatingRouteIds, summary, 200);',
|
|
1979
|
-
' await copyByIds(sourceDb, targetDb, "production-sales-orders", productionSalesOrderIds, summary, 200);',
|
|
1980
|
-
' const locationIds = unique([...idValues(productionRoutes, ["id_location"]), ...idValues(productionDeliveries, ["id_location"]), ...idValues(truckTreatingDeliveries, ["id_location"])]);',
|
|
1981
|
-
' const wellGroupIds = unique([...idValues(productionRoutes, ["id_well_group"]), ...idValues(productionDeliveries, ["id_well_group"]), ...idValues(truckTreatingDeliveries, ["id_well_group"])]);',
|
|
1982
|
-
' const copiedLocations = await copyByIds(sourceDb, targetDb, "production-locations", locationIds, summary, 300);',
|
|
1983
|
-
' const copiedWellGroups = await copyByIds(sourceDb, targetDb, "well-groups", wellGroupIds, summary, 300);',
|
|
1984
|
-
' const copiedTreatmentPlans = await copyQuery(sourceDb, targetDb, "treatment-plans", { $or: [',
|
|
1985
|
-
' { "truck_treating.id_truck_treating_route": { $in: truckTreatingRouteIds } },',
|
|
1986
|
-
' { id_location: { $in: locationIds } },',
|
|
1987
|
-
' { id_well_group: { $in: wellGroupIds } }',
|
|
1988
|
-
' ] }, summary, 800, { route_order: 1, updatedAt: -1 });',
|
|
1989
|
-
' const itemIds = unique([',
|
|
1990
|
-
' ...collectNestedStringValues(productionRoutes, ["id_item", "id_chemical"]),',
|
|
1991
|
-
' ...idValues(productionDeliveries, ["id_item", "id_chemical"]),',
|
|
1992
|
-
' ...idValues(truckTreatingDeliveries, ["id_item", "id_chemical"]),',
|
|
1993
|
-
' ...idValues(copiedTreatmentPlans, ["id_item", "id_chemical"])',
|
|
1994
|
-
' ]);',
|
|
1995
|
-
' await copyByIds(sourceDb, targetDb, "items", itemIds, summary, 300);',
|
|
1996
|
-
' await copyByIds(sourceDb, targetDb, "chemicals", itemIds, summary, 300);',
|
|
1997
|
-
' await copyByIds(sourceDb, targetDb, "customers", unique([...idValues(copiedLocations, ["id_customer"]), ...idValues(copiedWellGroups, ["id_customer"]), ...idValues(productionDeliveries, ["id_customer"]), ...idValues(truckTreatingDeliveries, ["id_customer"])]), summary, 100);',
|
|
1998
|
-
' await copyByIds(sourceDb, targetDb, "users", unique([...idValues(bols, ["id_user", "id_driver"]), ...idValues(productionDeliveries, ["id_user_approved", "id_driver", "id_driver_scheduled", "id_account_manager", "id_foreman"])]), summary, 100);',
|
|
1999
|
-
' await copyQuery(sourceDb, targetDb, "chemical-field-transfers", { id_bol: { $in: bolIds } }, summary, 200, { updatedAt: -1 });',
|
|
2000
|
-
' await copyQuery(sourceDb, targetDb, "generic-samples", { id_bol: { $in: bolIds } }, summary, 200, { updatedAt: -1 });',
|
|
2001
|
-
' const linkedRouteNames = copiedTruckTreatingRoutes.map((doc) => String(doc.name || doc.route || doc.truck_treating_route || doc._id || "")).filter(Boolean);',
|
|
2002
|
-
' const routeHintMatched = routeHints.length ? routeHints.some((hint) => linkedRouteNames.some((name) => name.toLowerCase().includes(hint.toLowerCase()))) : null;',
|
|
2003
|
-
' const driverIds = unique([...idValues(bols, ["id_driver", "id_user"]), ...idValues(productionDeliveries, ["id_driver", "id_driver_scheduled", "id_user_approved"]), ...idValues(truckTreatingDeliveries, ["id_driver", "id_driver_scheduled", "id_user_approved"])]);',
|
|
2004
|
-
' const bolStatuses = bols.map((doc) => ({',
|
|
2005
|
-
' id: String(doc && doc._id || ""),',
|
|
2006
|
-
' bol_number: String(doc && (doc.bol_string || doc.bol_number || "") || ""),',
|
|
2007
|
-
' status: String(doc && doc.status || ""),',
|
|
2008
|
-
' delivered: /delivered/i.test(String(doc && doc.status || ""))',
|
|
2009
|
-
' })).filter((entry) => entry.id);',
|
|
2010
|
-
' const primaryDriverId = driverIds[0] || "";',
|
|
2011
|
-
' const primaryBolId = bolIds[0] || "";',
|
|
2012
|
-
' const primaryBolStatus = String((bolStatuses.find((entry) => entry.id === primaryBolId) || bolStatuses[0] || {}).status || "");',
|
|
2013
|
-
' const primaryBolDelivered = /delivered/i.test(primaryBolStatus);',
|
|
2014
|
-
' const browserRoutes = primaryDriverId && primaryBolId ? {',
|
|
2015
|
-
' delivery: `/dashboard/driver/truck-treating/bol-delivery/${primaryDriverId}/${primaryBolId}`,',
|
|
2016
|
-
' detail: `/dashboard/driver/truck-treating/bol-detail/${primaryDriverId}/${primaryBolId}`,',
|
|
2017
|
-
' list: `/dashboard/driver/truck-treating/bol-list/${primaryDriverId}`',
|
|
2018
|
-
' } : {};',
|
|
2019
|
-
' summary.selected.truck_treating_bol_context = {',
|
|
2020
|
-
' bol_ids: bolIds,',
|
|
2021
|
-
' driver_ids: driverIds,',
|
|
2022
|
-
' route_ids: routeIds.slice(0, 100),',
|
|
2023
|
-
' route_count: productionRoutes.length,',
|
|
2024
|
-
' truck_treating_route_ids: truckTreatingRouteIds,',
|
|
2025
|
-
' linked_truck_treating_routes: linkedRouteNames,',
|
|
2026
|
-
' browser_routes: browserRoutes,',
|
|
2027
|
-
' bol_statuses: bolStatuses,',
|
|
2028
|
-
' primary_bol_status: primaryBolStatus,',
|
|
2029
|
-
' primary_bol_delivered: primaryBolDelivered,',
|
|
2030
|
-
' delivery_route_is_action_route: true,',
|
|
2031
|
-
' delivery_route_requires_not_delivered_bol: true,',
|
|
2032
|
-
' route_name_hints: routeHints,',
|
|
2033
|
-
' route_hint_matched: routeHintMatched',
|
|
2034
|
-
' };',
|
|
2035
|
-
' if (discoveryMode) summary.selected.truck_treating_bol_context.discovery_mode = discoveryMode;',
|
|
2036
|
-
' if (browserRoutes.delivery) summary.notes.push(`Derived truck-treating BOL browser target from live Mongo: ${browserRoutes.delivery}`);',
|
|
2037
|
-
' else summary.notes.push("Could not derive truck-treating BOL browser route because live BOL context did not include both id_driver and id_bol.");',
|
|
2038
|
-
' if (primaryBolDelivered) summary.notes.push(`Live BOL ${primaryBolId} is already Delivered; direct bol-delivery route is an action route and may correctly redirect. QA should use delivered Open File/detail proof or create a localhost-only deliverable fixture from the fetched live context before testing Treat/Deliver actions.`);',
|
|
2039
|
-
' if (routeHints.length && routeHintMatched === false) summary.notes.push(`Ticket route hint(s) ${routeHints.join(", ")} did not match linked live BOL route(s): ${linkedRouteNames.join(", ") || "none"}. QA should use linked live BOL routes and report the mismatch as data evidence, not synthesize a route.`);',
|
|
2040
|
-
' summary.notes.push(`Seeded live truck-treating BOL context for ${bolIdentifiers.join(", ")}: ${bols.length} BOL(s), ${productionRoutes.length} production route(s), ${copiedTreatmentPlans.length} treatment plan(s).`);',
|
|
2041
|
-
' return [...bols, ...productionRoutes, ...productionDeliveries, ...truckTreatingDeliveries, ...copiedTreatmentPlans];',
|
|
2042
|
-
'}',
|
|
2043
|
-
'',
|
|
2044
|
-
'async function copyProductionInterchangeablesContext(sourceDb, targetDb, summary) {',
|
|
2045
|
-
' if (!shouldAutoDiscoverProductionInterchangeablesContext()) return [];',
|
|
2046
|
-
' const interchangeables = await copyQuery(sourceDb, targetDb, "chemical-interchangeables", { "id_chemicals.1": { $exists: true } }, summary, 12, { updatedAt: -1 });',
|
|
2047
|
-
' if (!interchangeables.length) {',
|
|
2048
|
-
' summary.notes.push("No live chemical-interchangeables records with multiple chemicals were available for Update Interchangeables QA.");',
|
|
2049
|
-
' return [];',
|
|
2050
|
-
' }',
|
|
2051
|
-
' const chemicalIds = unique(idValues(interchangeables, ["id_chemicals"]));',
|
|
2052
|
-
' const chemicals = await copyQuery(sourceDb, targetDb, "chemicals", { _id: { $in: chemicalIds }, types: "Finished Good" }, summary, 80, { name: 1 });',
|
|
2053
|
-
' const usableChemicalIds = unique(chemicals.map((doc) => doc._id));',
|
|
2054
|
-
' const items = await copyQuery(sourceDb, targetDb, "items", { $or: [{ id_chemical: { $in: usableChemicalIds } }, { _id: { $in: usableChemicalIds } }] }, summary, 120, { name: 1 });',
|
|
2055
|
-
' const itemIds = unique(items.map((doc) => doc._id));',
|
|
2056
|
-
' let treatmentPlans = await copyQuery(sourceDb, targetDb, "treatment-plans", { date_end: null, $or: [{ id_chemical: { $in: usableChemicalIds } }, { id_item: { $in: itemIds } }] }, summary, 120, { updatedAt: -1 });',
|
|
2057
|
-
' if (!treatmentPlans.length && usableChemicalIds.length) {',
|
|
2058
|
-
' treatmentPlans = await copyQuery(sourceDb, targetDb, "treatment-plans", { date_end: null, id_chemical: usableChemicalIds[0] }, summary, 40, { updatedAt: -1 });',
|
|
2059
|
-
' }',
|
|
2060
|
-
' const customerIds = unique(idValues(treatmentPlans, ["id_customer"]));',
|
|
2061
|
-
' const yardIds = unique(idValues(treatmentPlans, ["id_yard"]));',
|
|
2062
|
-
' const locationIds = unique(idValues(treatmentPlans, ["id_location"]));',
|
|
2063
|
-
' const wellGroupIds = unique(idValues(treatmentPlans, ["id_well_group"]));',
|
|
2064
|
-
' const tankIds = unique(collectNestedStringValues(treatmentPlans, ["id_tank"]));',
|
|
2065
|
-
' await copyByIds(sourceDb, targetDb, "customers", customerIds, summary, 80);',
|
|
2066
|
-
' await copyByIds(sourceDb, targetDb, "yards", yardIds, summary, 80);',
|
|
2067
|
-
' await copyByIds(sourceDb, targetDb, "production-locations", locationIds, summary, 120);',
|
|
2068
|
-
' await copyByIds(sourceDb, targetDb, "well-groups", wellGroupIds, summary, 120);',
|
|
2069
|
-
' await copyByIds(sourceDb, targetDb, "production-location-tanks", tankIds, summary, 120);',
|
|
2070
|
-
' if (yardIds.length) {',
|
|
2071
|
-
' const users = await targetDb.collection("users").find({}).project({ _id: 1, other: 1 }).limit(50).toArray().catch(() => []);',
|
|
2072
|
-
' for (const user of users) {',
|
|
2073
|
-
' const other = user && typeof user.other === "object" && user.other ? user.other : {};',
|
|
2074
|
-
' const nextYards = unique([...(Array.isArray(other.yards) ? other.yards : []), ...yardIds]);',
|
|
2075
|
-
' await targetDb.collection("users").updateOne({ _id: user._id }, { $set: { "other.yards": nextYards } });',
|
|
2076
|
-
' }',
|
|
2077
|
-
' if (users.length) summary.notes.push(`Added seeded Update Interchangeables yard ids to ${users.length} local QA user(s) so treatment-plan filters can return data.`);',
|
|
2078
|
-
' }',
|
|
2079
|
-
' summary.selected.qa_production_interchangeables_context = {',
|
|
2080
|
-
' interchangeable_ids: interchangeables.map((doc) => doc._id).filter(Boolean),',
|
|
2081
|
-
' chemical_ids: usableChemicalIds,',
|
|
2082
|
-
' item_ids: itemIds,',
|
|
2083
|
-
' treatment_plan_ids: treatmentPlans.map((doc) => doc._id).filter(Boolean),',
|
|
2084
|
-
' customer_ids: customerIds,',
|
|
2085
|
-
' yard_ids: yardIds,',
|
|
2086
|
-
' tank_ids: tankIds,',
|
|
2087
|
-
' browser_routes: { update_interchangeables: "/production-location/update-interchangeables" }',
|
|
2088
|
-
' };',
|
|
2089
|
-
' summary.notes.push(`Seeded Update Interchangeables context: ${interchangeables.length} interchangeable group(s), ${chemicals.length} chemical(s), ${items.length} active item(s), ${treatmentPlans.length} treatment plan(s).`);',
|
|
2090
|
-
' return [...interchangeables, ...chemicals, ...items, ...treatmentPlans];',
|
|
2091
|
-
'}',
|
|
2092
|
-
'',
|
|
2093
|
-
'async function copyTicketAssetContext(sourceDb, targetDb, summary) {',
|
|
2094
|
-
' if (!shouldAutoDiscoverAssetContext()) return [];',
|
|
2095
|
-
' const assetIdentifiers = extractAssetIdentifiers();',
|
|
2096
|
-
' summary.selected.asset_identifiers = assetIdentifiers;',
|
|
2097
|
-
' const query = assetLookupQuery(assetIdentifiers);',
|
|
2098
|
-
' if (!query) return [];',
|
|
2099
|
-
' const assets = await copyQuery(sourceDb, targetDb, "assets", query, summary, 20, { updatedAt: -1, asset_number: 1 });',
|
|
2100
|
-
' if (!assets.length) {',
|
|
2101
|
-
' summary.notes.push(`No live assets matched ticket identifiers: ${assetIdentifiers.join(", ")}`);',
|
|
2102
|
-
' return [];',
|
|
2103
|
-
' }',
|
|
2104
|
-
' const yardIds = unique([',
|
|
2105
|
-
' ...idValues(assets, ["id_yard", "id_default_yard"]),',
|
|
2106
|
-
' ...collectNestedStringValues(assets, ["id", "id_yard"])',
|
|
2107
|
-
' ]);',
|
|
2108
|
-
' const locationIds = unique([',
|
|
2109
|
-
' ...idValues(assets, ["id_location", "id_current_location", "id_default_location"]),',
|
|
2110
|
-
' ...collectNestedStringValues(assets, ["id_location"])',
|
|
2111
|
-
' ]);',
|
|
2112
|
-
' const userIds = unique([',
|
|
2113
|
-
' ...idValues(assets, ["id_user", "id_driver", "id_created_by", "id_updated_by"]),',
|
|
2114
|
-
' ...collectNestedStringValues(assets, ["id_user", "id_driver", "id_created_by", "id_updated_by"])',
|
|
2115
|
-
' ]);',
|
|
2116
|
-
' await copyByIds(sourceDb, targetDb, "yards", yardIds, summary, 100);',
|
|
2117
|
-
' await copyByIds(sourceDb, targetDb, "production-locations", locationIds, summary, 100);',
|
|
2118
|
-
' await copyByIds(sourceDb, targetDb, "users", userIds, summary, 100);',
|
|
2119
|
-
' const assetIds = assets.map((doc) => doc._id).filter(Boolean);',
|
|
2120
|
-
' const assetNumbers = assets.map((doc) => String(doc.asset_number || doc.asset_number_string || doc.asset_unit_number || doc.unit_number || "")).filter(Boolean);',
|
|
2121
|
-
' summary.selected.qa_asset_context = {',
|
|
2122
|
-
' asset_ids: assetIds,',
|
|
2123
|
-
' asset_numbers: assetNumbers,',
|
|
2124
|
-
' yard_ids: yardIds,',
|
|
2125
|
-
' production_location_ids: locationIds,',
|
|
2126
|
-
' browser_routes: assetIds[0] ? { list: "/asset/list", detail: `/asset/detail/${assetIds[0]}`, edit: `/asset/edit/${assetIds[0]}` } : { list: "/asset/list" }',
|
|
2127
|
-
' };',
|
|
2128
|
-
' summary.notes.push(`Seeded live asset context for ${assetIdentifiers.join(", ")}: ${assets.length} asset(s), ${yardIds.length} yard id(s), ${locationIds.length} production location id(s).`);',
|
|
2129
|
-
' return assets;',
|
|
2130
|
-
'}',
|
|
2131
|
-
'',
|
|
2132
|
-
'async function ensureBillingSurchargePricingFixtures(targetDb, summary, serviceItems, customerIds) {',
|
|
2133
|
-
' const now = new Date();',
|
|
2134
|
-
' const activeServiceItems = (serviceItems || []).filter((doc) => {',
|
|
2135
|
-
' const typeText = String(doc && (doc.type || doc.category || doc.name || "") || "");',
|
|
2136
|
-
' return doc && doc._id && doc.active !== false && /misc|service|surcharge|fuel|delivery/i.test(typeText);',
|
|
2137
|
-
' });',
|
|
2138
|
-
' const fixtureItems = [];',
|
|
2139
|
-
' if (!activeServiceItems.length) {',
|
|
2140
|
-
' fixtureItems.push(',
|
|
2141
|
-
' { _id: "qa-misc-surcharge-item", name: "QA Misc Surcharge", description: "QA Misc Surcharge", type: "Misc", unit: "Each", active: true, createdAt: now, updatedAt: now },',
|
|
2142
|
-
' { _id: "qa-service-surcharge-item", name: "QA Service Surcharge", description: "QA Service Surcharge", type: "Service", unit: "Each", active: true, createdAt: now, updatedAt: now }',
|
|
2143
|
-
' );',
|
|
2144
|
-
' for (const item of fixtureItems) await targetDb.collection("items").replaceOne({ _id: item._id }, item, { upsert: true });',
|
|
2145
|
-
' summary.collections.items = (summary.collections.items || 0) + fixtureItems.length;',
|
|
2146
|
-
' summary.notes.push("Created localhost-only active service/misc surcharge item fixtures because the bounded live slice did not include one.");',
|
|
2147
|
-
' }',
|
|
2148
|
-
' const usableItems = activeServiceItems.length ? activeServiceItems : fixtureItems;',
|
|
2149
|
-
' const pricingDocs = [];',
|
|
2150
|
-
' for (const item of usableItems.slice(0, 4)) {',
|
|
2151
|
-
' const label = String(item.name || item.description || item._id || "QA Surcharge");',
|
|
2152
|
-
' const subtype = /service/i.test(String(item.type || label)) ? "service" : "misc";',
|
|
2153
|
-
' pricingDocs.push({',
|
|
2154
|
-
' _id: `qa-pricing-${subtype}-${String(item._id).replace(/[^a-z0-9_-]/gi, "-")}-global`,',
|
|
2155
|
-
' id_item: item._id, name: label, description: label, unit: item.unit || "Each", active: true, type: "sale", sub_type: subtype, price: 15, price_gross: 15, discount_percent: 0, id_customer: "", createdAt: now, updatedAt: now',
|
|
2156
|
-
' });',
|
|
2157
|
-
' if (customerIds && customerIds[0]) {',
|
|
2158
|
-
' pricingDocs.push({',
|
|
2159
|
-
' _id: `qa-pricing-${subtype}-${String(item._id).replace(/[^a-z0-9_-]/gi, "-")}-customer`,',
|
|
2160
|
-
' id_item: item._id, name: `${label} Customer Override`, description: `${label} Customer Override`, unit: item.unit || "Each", active: true, type: "sale", sub_type: subtype, price: 20, price_gross: 20, discount_percent: 0, id_customer: customerIds[0], createdAt: now, updatedAt: now',
|
|
2161
|
-
' });',
|
|
2162
|
-
' }',
|
|
2163
|
-
' }',
|
|
2164
|
-
' for (const doc of pricingDocs) await targetDb.collection("pricing-items").replaceOne({ _id: doc._id }, doc, { upsert: true });',
|
|
2165
|
-
' summary.collections["pricing-items"] = (summary.collections["pricing-items"] || 0) + pricingDocs.length;',
|
|
2166
|
-
' summary.selected.qa_surcharge_fixture_items = usableItems.map((doc) => doc._id);',
|
|
2167
|
-
' summary.selected.qa_surcharge_pricing_items = pricingDocs.map((doc) => doc._id);',
|
|
2168
|
-
' if (pricingDocs.length) summary.notes.push(`Ensured ${pricingDocs.length} local pricing-items for service/misc surcharge override/default QA.`);',
|
|
2169
|
-
'}',
|
|
2170
|
-
'',
|
|
2171
|
-
'async function cleanupSyntheticBillingDashboardQaFixtures(targetDb, summary) {',
|
|
2172
|
-
' const fixtureIdQuery = { _id: /^qa-billing-/ };',
|
|
2173
|
-
' const fixtureCollections = ["customers", "yards", "items", "production-locations", "bols", "production-sales-orders", "production-deliveries", "invoices"];',
|
|
2174
|
-
' let removed = 0;',
|
|
2175
|
-
' for (const collectionName of fixtureCollections) {',
|
|
2176
|
-
' const result = await targetDb.collection(collectionName).deleteMany(fixtureIdQuery).catch(() => ({ deletedCount: 0 }));',
|
|
2177
|
-
' removed += Number(result && result.deletedCount || 0);',
|
|
2178
|
-
' }',
|
|
2179
|
-
' const invoiceResult = await targetDb.collection("invoices").deleteMany({ $or: [{ invoice_string: /^QA-INV-/ }, { invoice_number: 4333 }] }).catch(() => ({ deletedCount: 0 }));',
|
|
2180
|
-
' removed += Number(invoiceResult && invoiceResult.deletedCount || 0);',
|
|
2181
|
-
' const psoResult = await targetDb.collection("production-sales-orders").deleteMany({ $or: [{ order_number_string: /^QA-PSO-/ }, { activity_number: /^QA-PSO-/ }] }).catch(() => ({ deletedCount: 0 }));',
|
|
2182
|
-
' removed += Number(psoResult && psoResult.deletedCount || 0);',
|
|
2183
|
-
' if (removed) summary.notes.push(`Removed ${removed} stale synthetic billing dashboard QA fixture document(s) from the local Mongo profile.`);',
|
|
2184
|
-
'}',
|
|
2185
|
-
'',
|
|
2186
|
-
'async function ensureBillingDashboardQaFixtures(targetDb, summary, customerIds, yardIds, serviceItems) {',
|
|
2187
|
-
' const now = new Date();',
|
|
2188
|
-
' const customerId = (customerIds && customerIds[0]) || "qa-billing-customer";',
|
|
2189
|
-
' const yardId = (yardIds && yardIds[0]) || "qa-billing-yard";',
|
|
2190
|
-
' const item = (serviceItems || []).find((doc) => doc && doc._id) || { _id: "qa-billing-service-item", name: "QA Billing Service Item", unit: "Each", type: "Service", active: true };',
|
|
2191
|
-
' const locationId = "qa-billing-location";',
|
|
2192
|
-
' const bolId = "qa-billing-bol";',
|
|
2193
|
-
' const psoId = "qa-billing-pso-awaiting-invoice";',
|
|
2194
|
-
' const deliveryId = "qa-billing-delivery-awaiting-invoice";',
|
|
2195
|
-
' const invoiceId = "qa-billing-existing-processing-invoice";',
|
|
2196
|
-
' await targetDb.collection("customers").replaceOne({ _id: customerId }, {',
|
|
2197
|
-
' _id: customerId, name: "QA Billing Customer", active: true, send_type: "Email", customer_approval_required: false, createdAt: now, updatedAt: now',
|
|
2198
|
-
' }, { upsert: true });',
|
|
2199
|
-
' await targetDb.collection("yards").replaceOne({ _id: yardId }, { _id: yardId, name: "QA Billing Yard", active: true, createdAt: now, updatedAt: now }, { upsert: true });',
|
|
2200
|
-
' await targetDb.collection("items").replaceOne({ _id: item._id }, { ...item, _id: item._id, name: item.name || "QA Billing Service Item", unit: item.unit || "Each", type: item.type || "Service", active: true, updatedAt: now }, { upsert: true });',
|
|
2201
|
-
' await targetDb.collection("production-locations").replaceOne({ _id: locationId }, {',
|
|
2202
|
-
' _id: locationId, id_customer: customerId, customer: "QA Billing Customer", location: "QA Billing Location", id_yard: yardId, county: "QA County", active: true, createdAt: now, updatedAt: now',
|
|
2203
|
-
' }, { upsert: true });',
|
|
2204
|
-
' await targetDb.collection("bols").replaceOne({ _id: bolId }, {',
|
|
2205
|
-
' _id: bolId, bol_string: "QA-BOL-004333", bol_number: "QA-BOL-004333", status: "Delivered", date: now, driver: "QA Driver", createdAt: now, updatedAt: now',
|
|
2206
|
-
' }, { upsert: true });',
|
|
2207
|
-
' const lineItem = {',
|
|
2208
|
-
' _id: "qa-billing-pso-line-1", id_item: item._id, item: item.name || "QA Billing Service Item", item_type: item.type || "Service", type: item.type || "Service", quantity: 2, unit: item.unit || "Each", price: 100, price_subtotal: 200, price_total: 216.5, tax_amount: 16.5, billed: true, memo_bill: false, taxable: true, id_location: locationId, location: "QA Billing Location", id_yard: yardId, yard: "QA Billing Yard"',
|
|
2209
|
-
' };',
|
|
2210
|
-
' await targetDb.collection("production-sales-orders").replaceOne({ _id: psoId }, {',
|
|
2211
|
-
' _id: psoId, status: "Awaiting Invoice", type: "Invoice", id_customer: customerId, customer: "QA Billing Customer", id_default_yard: yardId, date_created: now, date_ship: now, date_needed: now, order_number_string: "QA-PSO-004333", activity_number: "QA-PSO-004333", bol_numbers: "QA-BOL-004333", drivers: "QA Driver", ship_to_type: "Location", ship_to_location: "QA Billing Location", price: 200, price_subtotal: 200, account_manager: "QA Admin", user_approved: "QA Admin", production_locations: [{ id_location: locationId, location: "QA Billing Location", items: [lineItem] }], createdAt: now, updatedAt: now',
|
|
2212
|
-
' }, { upsert: true });',
|
|
2213
|
-
' await targetDb.collection("production-deliveries").replaceOne({ _id: deliveryId }, {',
|
|
2214
|
-
' _id: deliveryId, type: "Delivery", approved: true, billed: true, memo_bill: false, invoiced: false, generic: false, invoices: [], consignment: false, id_activity: psoId, activity_number: "QA-PSO-004333", activity_type: "Invoice", id_customer: customerId, id_location: locationId, location: "QA Billing Location", id_yard: yardId, yard: "QA Billing Yard", id_item: item._id, item: item.name || "QA Billing Service Item", item_type: item.type || "Service", quantity: 2, unit: item.unit || "Each", price_per_unit: 100, price_subtotal: 200, price_total: 216.5, tax_amount: 16.5, id_bol: bolId, bol_number: "QA-BOL-004333", date: now, date_ship: now, user_approved: "QA Admin", account_manager: "QA Admin", createdAt: now, updatedAt: now',
|
|
2215
|
-
' }, { upsert: true });',
|
|
2216
|
-
' await targetDb.collection("invoices").replaceOne({ _id: invoiceId }, {',
|
|
2217
|
-
' _id: invoiceId, status: "Processing", type: "Invoice", id_customer: customerId, customer: "QA Billing Customer", id_yard: yardId, yard: "QA Billing Yard", invoice_string: "QA-INV-004333", invoice_number: 4333, date_invoice: now, total: 216.5, subtotal: 200, tax_amount: 16.5, printed: false, invoice_opened: false, logs: [], files: [], line_items: [{ ...lineItem, source_production_delivery_id: deliveryId, description: lineItem.item, date: now }], createdAt: now, updatedAt: now',
|
|
2218
|
-
' }, { upsert: true });',
|
|
2219
|
-
' summary.collections.customers = (summary.collections.customers || 0) + 1;',
|
|
2220
|
-
' summary.collections.yards = (summary.collections.yards || 0) + 1;',
|
|
2221
|
-
' summary.collections.items = (summary.collections.items || 0) + 1;',
|
|
2222
|
-
' summary.collections["production-locations"] = (summary.collections["production-locations"] || 0) + 1;',
|
|
2223
|
-
' summary.collections.bols = (summary.collections.bols || 0) + 1;',
|
|
2224
|
-
' summary.collections["production-sales-orders"] = (summary.collections["production-sales-orders"] || 0) + 1;',
|
|
2225
|
-
' summary.collections["production-deliveries"] = (summary.collections["production-deliveries"] || 0) + 1;',
|
|
2226
|
-
' summary.collections.invoices = (summary.collections.invoices || 0) + 1;',
|
|
2227
|
-
' summary.selected.qa_billing_fixture = { customerId, yardId, psoId, deliveryId, invoiceId, itemId: item._id };',
|
|
2228
|
-
' summary.notes.push("Ensured localhost-only Billing Dashboard fixtures: one existing processing invoice plus one awaiting-invoice PSO/delivery source row.");',
|
|
2229
|
-
'}',
|
|
2230
|
-
'',
|
|
2231
|
-
'async function selectDashboardReadyProductionDeliveryIds(sourceDb, query, limit = 5) {',
|
|
2232
|
-
' const rows = await sourceDb.collection("production-deliveries").aggregate([',
|
|
2233
|
-
' { $match: query || {} },',
|
|
2234
|
-
' { $lookup: { from: "production-sales-orders", localField: "id_activity", foreignField: "_id", as: "dbPSO" } },',
|
|
2235
|
-
' { $unwind: { path: "$dbPSO", preserveNullAndEmptyArrays: false } },',
|
|
2236
|
-
' { $match: { "dbPSO.type": { $ne: "Consignment Invoice" } } },',
|
|
2237
|
-
' { $lookup: {',
|
|
2238
|
-
' from: "bols",',
|
|
2239
|
-
' let: { idBol: "$id_bol" },',
|
|
2240
|
-
' pipeline: [{ $match: { $expr: { $eq: ["$_id", "$$idBol"] }, status: "Delivered" } }],',
|
|
2241
|
-
' as: "bol"',
|
|
2242
|
-
' } },',
|
|
2243
|
-
' { $unwind: { path: "$bol", preserveNullAndEmptyArrays: false } },',
|
|
2244
|
-
' { $lookup: { from: "production-locations", localField: "id_location", foreignField: "_id", as: "dbProductionLocation" } },',
|
|
2245
|
-
' { $lookup: { from: "well-groups", localField: "id_well_group", foreignField: "_id", as: "dbWellGroup" } },',
|
|
2246
|
-
' { $match: { $or: [{ "dbProductionLocation.0": { $exists: true } }, { "dbWellGroup.0": { $exists: true } }] } },',
|
|
2247
|
-
' { $sort: { date: -1, date_ship: -1, date_treated: -1, updatedAt: -1 } },',
|
|
2248
|
-
' { $limit: limit },',
|
|
2249
|
-
' { $project: { _id: 1 } }',
|
|
2250
|
-
' ]).toArray();',
|
|
2251
|
-
' return rows.map((doc) => doc._id).filter(Boolean);',
|
|
2252
|
-
'}',
|
|
2253
|
-
'',
|
|
2254
|
-
'async function selectTruckTreatDashboardReadyProductionDeliveryIds(sourceDb, query, limit = 5) {',
|
|
2255
|
-
' const rows = await sourceDb.collection("production-deliveries").aggregate([',
|
|
2256
|
-
' { $match: query || {} },',
|
|
2257
|
-
' { $lookup: {',
|
|
2258
|
-
' from: "bols",',
|
|
2259
|
-
' let: { idBol: "$id_bol" },',
|
|
2260
|
-
' pipeline: [{ $match: { $expr: { $eq: ["$_id", "$$idBol"] }, status: "Delivered" } }],',
|
|
2261
|
-
' as: "bol"',
|
|
2262
|
-
' } },',
|
|
2263
|
-
' { $unwind: { path: "$bol", preserveNullAndEmptyArrays: false } },',
|
|
2264
|
-
' { $lookup: { from: "production-locations", localField: "id_location", foreignField: "_id", as: "dbProductionLocation" } },',
|
|
2265
|
-
' { $lookup: { from: "well-groups", localField: "id_well_group", foreignField: "_id", as: "dbWellGroup" } },',
|
|
2266
|
-
' { $match: { $or: [{ "dbProductionLocation.0": { $exists: true } }, { "dbWellGroup.0": { $exists: true } }] } },',
|
|
2267
|
-
' { $sort: { date: -1, date_treated: -1, updatedAt: -1 } },',
|
|
2268
|
-
' { $limit: limit },',
|
|
2269
|
-
' { $project: { _id: 1 } }',
|
|
2270
|
-
' ]).toArray();',
|
|
2271
|
-
' return rows.map((doc) => doc._id).filter(Boolean);',
|
|
2272
|
-
'}',
|
|
2273
|
-
'',
|
|
2274
|
-
'async function selectLocalhostDeliveryFixtureIds(sourceDb, query, limit = 3) {',
|
|
2275
|
-
' const rows = await sourceDb.collection("production-deliveries").aggregate([',
|
|
2276
|
-
' { $match: { $and: [query || {}, { id_bol: { $type: "string", $ne: "" } }] } },',
|
|
2277
|
-
' { $lookup: { from: "production-sales-orders", localField: "id_activity", foreignField: "_id", as: "dbPSO" } },',
|
|
2278
|
-
' { $unwind: { path: "$dbPSO", preserveNullAndEmptyArrays: false } },',
|
|
2279
|
-
' { $match: { "dbPSO.type": { $ne: "Consignment Invoice" } } },',
|
|
2280
|
-
' { $lookup: { from: "production-locations", localField: "id_location", foreignField: "_id", as: "dbProductionLocation" } },',
|
|
2281
|
-
' { $lookup: { from: "well-groups", localField: "id_well_group", foreignField: "_id", as: "dbWellGroup" } },',
|
|
2282
|
-
' { $match: { $or: [{ "dbProductionLocation.0": { $exists: true } }, { "dbWellGroup.0": { $exists: true } }] } },',
|
|
2283
|
-
' { $sort: { date: -1, date_ship: -1, updatedAt: -1 } },',
|
|
2284
|
-
' { $limit: limit },',
|
|
2285
|
-
' { $project: { _id: 1 } }',
|
|
2286
|
-
' ]).toArray();',
|
|
2287
|
-
' return rows.map((doc) => doc._id).filter(Boolean);',
|
|
2288
|
-
'}',
|
|
2289
|
-
'',
|
|
2290
|
-
'async function seedBillingInventory(sourceDb, targetDb) {',
|
|
2291
|
-
' const includeBillingFixtures = shouldSeedBillingDashboardFixtures();',
|
|
2292
|
-
' const includePricingImportFixtures = includeBillingFixtures || shouldSeedPricingImportFixtures();',
|
|
2293
|
-
' const summary = { profile: desiredSeedProfile(), collections: {}, selected: {}, notes: [] };',
|
|
2294
|
-
' const identifiers = extractSeedIdentifiers();',
|
|
2295
|
-
' summary.selected.seed_identifiers = identifiers;',
|
|
2296
|
-
' const qaIdentityUsers = await copyQaIdentityUsers(sourceDb, targetDb, summary);',
|
|
2297
|
-
' const truckTreatingBolContextDocs = await copyTruckTreatingBolContext(sourceDb, targetDb, summary);',
|
|
2298
|
-
' const ticketAssetContextDocs = await copyTicketAssetContext(sourceDb, targetDb, summary);',
|
|
2299
|
-
' const productionInterchangeablesContextDocs = await copyProductionInterchangeablesContext(sourceDb, targetDb, summary);',
|
|
2300
|
-
' const pricingImportWorkbookContextDocs = includePricingImportFixtures ? await copyPricingImportWorkbookContext(sourceDb, targetDb, summary) : [];',
|
|
2301
|
-
' if (!includeBillingFixtures) {',
|
|
2302
|
-
' summary.notes.push("Billing dashboard fixtures are disabled because ticket context does not require billing, invoices, taxes, inventory, surcharges, checkout, or pick tickets.");',
|
|
2303
|
-
' await cleanupSyntheticBillingDashboardQaFixtures(targetDb, summary);',
|
|
2304
|
-
' }',
|
|
2305
|
-
' if (includePricingImportFixtures && !includeBillingFixtures) {',
|
|
2306
|
-
' summary.notes.push("Using pricing-import seed profile: pricing records are copied without synthetic billing dashboard invoice fixtures.");',
|
|
2307
|
-
' }',
|
|
2308
|
-
' const billableDeliveryQuery = { $and: [',
|
|
2309
|
-
' { $or: [{ type: "Delivery" }, { type: "Pickup" }] },',
|
|
2310
|
-
' { $or: [{ consignment: { $exists: false } }, { consignment: false }] },',
|
|
2311
|
-
' { approved: true }, { invoiced: false }, { generic: false }, { "invoices.0": { $exists: false } },',
|
|
2312
|
-
' { $or: [{ billed: true }, { memo_bill: true }] }',
|
|
2313
|
-
' ] };',
|
|
2314
|
-
' const billableTruckTreatQuery = { $and: [',
|
|
2315
|
-
' { type: "Truck Treat" }, { invoiced: false }, { generic: false }, { approved: true }, { "invoices.0": { $exists: false } },',
|
|
2316
|
-
' { $or: [{ billed: true }, { memo_bill: true }] }',
|
|
2317
|
-
' ] };',
|
|
2318
|
-
' const hintedQuery = identifierQuery(identifiers);',
|
|
2319
|
-
' let hintedProductionDeliveries = [];',
|
|
2320
|
-
' let hintedTruckTreatingDeliveries = [];',
|
|
2321
|
-
' if (hintedQuery) {',
|
|
2322
|
-
' hintedProductionDeliveries = await copyByIds(sourceDb, targetDb, "production-deliveries", await selectDashboardReadyProductionDeliveryIds(sourceDb, { $and: [billableDeliveryQuery, hintedQuery] }, 3), summary, 3);',
|
|
2323
|
-
' hintedTruckTreatingDeliveries = await copyByIds(sourceDb, targetDb, "production-deliveries", await selectTruckTreatDashboardReadyProductionDeliveryIds(sourceDb, { $and: [billableTruckTreatQuery, hintedQuery] }, 3), summary, 3);',
|
|
2324
|
-
' if (hintedProductionDeliveries.length || hintedTruckTreatingDeliveries.length) summary.notes.push(`Preferred live records matching ticket identifiers: ${identifiers.join(", ")}`);',
|
|
2325
|
-
' else summary.notes.push(`No billable live records matched ticket identifiers: ${identifiers.join(", ")}`);',
|
|
2326
|
-
' }',
|
|
2327
|
-
'',
|
|
2328
|
-
' let fallbackProductionDeliveries = hintedProductionDeliveries.length ? [] : await copyByIds(sourceDb, targetDb, "production-deliveries", await selectDashboardReadyProductionDeliveryIds(sourceDb, billableDeliveryQuery, 3), summary, 3);',
|
|
2329
|
-
' if (!hintedProductionDeliveries.length && !fallbackProductionDeliveries.length) {',
|
|
2330
|
-
' fallbackProductionDeliveries = await copyByIds(sourceDb, targetDb, "production-deliveries", await selectLocalhostDeliveryFixtureIds(sourceDb, billableDeliveryQuery, 3), summary, 3);',
|
|
2331
|
-
' if (fallbackProductionDeliveries.length) summary.notes.push("Created localhost-only delivery invoice fixture from live delivery data by normalizing copied BOL status to Delivered in local Mongo.");',
|
|
2332
|
-
' }',
|
|
2333
|
-
' const fallbackTruckTreatingDeliveries = hintedTruckTreatingDeliveries.length ? [] : await copyByIds(sourceDb, targetDb, "production-deliveries", await selectTruckTreatDashboardReadyProductionDeliveryIds(sourceDb, billableTruckTreatQuery, 3), summary, 3);',
|
|
2334
|
-
' const productionDeliveries = [...hintedProductionDeliveries, ...fallbackProductionDeliveries];',
|
|
2335
|
-
' const truckTreatingDeliveries = [...hintedTruckTreatingDeliveries, ...fallbackTruckTreatingDeliveries];',
|
|
2336
|
-
' summary.selected.production_deliveries = productionDeliveries.map((doc) => doc._id);',
|
|
2337
|
-
' summary.selected.truck_treating_deliveries = truckTreatingDeliveries.map((doc) => doc._id);',
|
|
2338
|
-
' if (!productionDeliveries.length && !truckTreatingDeliveries.length) {',
|
|
2339
|
-
' summary.notes.push("No live billable delivery or truck-treatment records matched the Billing Dashboard awaiting-invoice filters.");',
|
|
2340
|
-
' }',
|
|
2341
|
-
'',
|
|
2342
|
-
' const sourceDocs = [...qaIdentityUsers, ...truckTreatingBolContextDocs, ...ticketAssetContextDocs, ...productionInterchangeablesContextDocs, ...pricingImportWorkbookContextDocs, ...productionDeliveries, ...truckTreatingDeliveries];',
|
|
2343
|
-
' const pricingImportContext = summary.selected.pricing_import_workbook_context || {};',
|
|
2344
|
-
' const customerIds = unique([...idValues(sourceDocs, ["id_customer"]), ...(Array.isArray(pricingImportContext.customer_ids) ? pricingImportContext.customer_ids : [])]);',
|
|
2345
|
-
' const locationIds = idValues(sourceDocs, ["id_location"]);',
|
|
2346
|
-
' const wellGroupIds = idValues(sourceDocs, ["id_well_group"]);',
|
|
2347
|
-
' const itemIds = unique([...idValues(sourceDocs, ["id_item", "id_chemical"]), ...(Array.isArray(pricingImportContext.item_ids) ? pricingImportContext.item_ids : []), ...(Array.isArray(pricingImportContext.chemical_ids) ? pricingImportContext.chemical_ids : [])]);',
|
|
2348
|
-
' const bolIds = idValues(sourceDocs, ["id_bol", "id_bols"]);',
|
|
2349
|
-
' const yardIds = idValues(sourceDocs, ["id_yard"]);',
|
|
2350
|
-
' const activityIds = idValues(sourceDocs, ["id_activity", "id_production_sales_order", "id_sales_order"]);',
|
|
2351
|
-
' const tankIds = idValues(sourceDocs, ["id_tank"]);',
|
|
2352
|
-
' const truckTreatingDetailIds = idValues(sourceDocs, ["id_delivery", "id_truck_treating_delivery"]);',
|
|
2353
|
-
' const userIds = idValues(sourceDocs, ["id_user_approved", "id_driver", "id_driver_scheduled", "id_account_manager", "id_foreman"]);',
|
|
2354
|
-
'',
|
|
2355
|
-
' const copiedLocations = await copyByIds(sourceDb, targetDb, "production-locations", locationIds, summary);',
|
|
2356
|
-
' const copiedWellGroups = await copyByIds(sourceDb, targetDb, "well-groups", wellGroupIds, summary);',
|
|
2357
|
-
' const copiedItems = await copyByIds(sourceDb, targetDb, "items", itemIds, summary);',
|
|
2358
|
-
' const copiedChemicals = await copyByIds(sourceDb, targetDb, "chemicals", itemIds, summary);',
|
|
2359
|
-
' await copyByIds(sourceDb, targetDb, "production-sales-orders", activityIds, summary);',
|
|
2360
|
-
' await copyByIds(sourceDb, targetDb, "production-location-tanks", tankIds, summary);',
|
|
2361
|
-
' await copyByIds(sourceDb, targetDb, "truck-treating-deliveries", truckTreatingDetailIds, summary);',
|
|
2362
|
-
' await copyByIds(sourceDb, targetDb, "customers", unique([...customerIds, ...idValues(copiedLocations, ["id_customer"]), ...idValues(copiedWellGroups, ["id_customer"])]), summary);',
|
|
2363
|
-
' await copyByIds(sourceDb, targetDb, "bols", bolIds, summary);',
|
|
2364
|
-
' if (productionDeliveries.length && bolIds.length) await targetDb.collection("bols").updateMany({ _id: { $in: bolIds } }, { $set: { status: "Delivered" } });',
|
|
2365
|
-
' await copyByIds(sourceDb, targetDb, "yards", yardIds, summary);',
|
|
2366
|
-
' await copyByIds(sourceDb, targetDb, "users", userIds, summary);',
|
|
2367
|
-
'',
|
|
2368
|
-
' const copiedServiceItems = await copyQuery(sourceDb, targetDb, "items", { $or: [',
|
|
2369
|
-
' { type: /service/i }, { type: /misc/i }, { category: /service/i }, { category: /misc/i },',
|
|
2370
|
-
' { name: /surcharge|fuel|delivery|service|misc/i }',
|
|
2371
|
-
' ] }, summary, 40, { updatedAt: -1 });',
|
|
2372
|
-
' summary.selected.service_or_surcharge_items = copiedServiceItems.map((doc) => doc._id);',
|
|
2373
|
-
'',
|
|
2374
|
-
' if (includePricingImportFixtures) {',
|
|
2375
|
-
' await copyQuery(sourceDb, targetDb, "pricing-items", { $or: [',
|
|
2376
|
-
' { id_item: { $in: unique([...itemIds, ...copiedItems.map((doc) => doc._id), ...copiedServiceItems.map((doc) => doc._id)]) } },',
|
|
2377
|
-
' { id_chemical: { $in: unique([...itemIds, ...copiedChemicals.map((doc) => doc._id)]) } },',
|
|
2378
|
-
' { id_customer: { $in: customerIds } },',
|
|
2379
|
-
' { sub_type: { $in: ["misc", "service"] } },',
|
|
2380
|
-
' { id_item: { $in: copiedServiceItems.map((doc) => doc._id) } },',
|
|
2381
|
-
' { name: /surcharge|fuel|delivery|service|misc/i }',
|
|
2382
|
-
' ] }, summary, 80, { updatedAt: -1 });',
|
|
2383
|
-
' }',
|
|
2384
|
-
'',
|
|
2385
|
-
' if (includeBillingFixtures) {',
|
|
2386
|
-
' await copyQuery(sourceDb, targetDb, "sales-taxes", {}, summary, 80, { updatedAt: -1 });',
|
|
2387
|
-
' await copyQuery(sourceDb, targetDb, "state-counties", {}, summary, 80, { updatedAt: -1 });',
|
|
2388
|
-
' await copyQuery(sourceDb, targetDb, "accounting-codes", {}, summary, 80, { updatedAt: -1 });',
|
|
2389
|
-
' await ensureBillingSurchargePricingFixtures(targetDb, summary, copiedServiceItems, customerIds);',
|
|
2390
|
-
' await ensureBillingDashboardQaFixtures(targetDb, summary, customerIds, yardIds, copiedServiceItems);',
|
|
2391
|
-
'',
|
|
2392
|
-
' const inventoryLocationQuery = { $or: [',
|
|
2393
|
-
' { id_item: { $in: unique([...itemIds, ...copiedItems.map((doc) => doc._id), ...copiedServiceItems.map((doc) => doc._id)]) } },',
|
|
2394
|
-
' { id_chemical: { $in: unique([...itemIds, ...copiedChemicals.map((doc) => doc._id)]) } },',
|
|
2395
|
-
' { id_yard: { $in: yardIds } }',
|
|
2396
|
-
' ] };',
|
|
2397
|
-
' const inventoryLocations = await copyQuery(sourceDb, targetDb, "inventory-locations", inventoryLocationQuery, summary, 80, { updatedAt: -1 });',
|
|
2398
|
-
' await copyQuery(sourceDb, targetDb, "inventory-transactions", { $or: [',
|
|
2399
|
-
' { id_inventory_location: { $in: inventoryLocations.map((doc) => doc._id) } },',
|
|
2400
|
-
' { id_item: { $in: itemIds } },',
|
|
2401
|
-
' { id_chemical: { $in: itemIds } }',
|
|
2402
|
-
' ] }, summary, 120, { date: -1, updatedAt: -1 });',
|
|
2403
|
-
' }',
|
|
2404
|
-
'',
|
|
2405
|
-
' summary.ready = pricingImportWorkbookContextDocs.length > 0 || truckTreatingBolContextDocs.length > 0 || ticketAssetContextDocs.length > 0 || productionInterchangeablesContextDocs.length > 0 || productionDeliveries.length > 0 || truckTreatingDeliveries.length > 0;',
|
|
2406
|
-
' return summary;',
|
|
2407
|
-
'}',
|
|
2408
|
-
'',
|
|
2409
|
-
'(async () => {',
|
|
2410
|
-
' const { MongoClient } = requireMongo();',
|
|
2411
|
-
' const source = resolveRuntimeSource();',
|
|
2412
|
-
' const targetUri = process.env.RESOLVEIO_SUPPORT_QA_MONGO_URL || process.env.RESOLVEIO_RUNNER_QA_MONGO_URL || process.env.MONGO_URL || "mongodb://127.0.0.1:3001/resolveio?directConnection=true";',
|
|
2413
|
-
' if (!source.uri) {',
|
|
2414
|
-
' preserveExistingSeedResult("missing_live_mongo_uri");',
|
|
2415
|
-
' writeResult({ status: isLiveDataRequired() ? "failed" : "skipped", reason: "missing_live_mongo_uri", required: isLiveDataRequired(), result_path: resultPath }, isLiveDataRequired() ? 5 : 0);',
|
|
2416
|
-
' }',
|
|
2417
|
-
' if (!isLocalMongoUri(targetUri)) writeResult({ status: "failed", reason: "target_mongo_must_be_localhost", target_uri_redacted: redactUri(targetUri), result_path: resultPath }, 3);',
|
|
2418
|
-
' if (String(source.uri) === String(targetUri)) writeResult({ status: "failed", reason: "source_and_target_mongo_match", result_path: resultPath }, 3);',
|
|
2419
|
-
' await waitForTargetMongo(MongoClient, targetUri);',
|
|
2420
|
-
' const sourceClient = new MongoClient(source.uri, { readPreference: "secondaryPreferred", serverSelectionTimeoutMS: 15000 });',
|
|
2421
|
-
' const targetClient = new MongoClient(targetUri, { serverSelectionTimeoutMS: 15000 });',
|
|
2422
|
-
' try {',
|
|
2423
|
-
' await sourceClient.connect();',
|
|
2424
|
-
' await targetClient.connect();',
|
|
2425
|
-
' const sourceDb = source.database ? sourceClient.db(source.database) : sourceClient.db();',
|
|
2426
|
-
' const targetDb = targetClient.db();',
|
|
2427
|
-
' const summary = await seedBillingInventory(sourceDb, targetDb);',
|
|
2428
|
-
' writeResult({',
|
|
2429
|
-
' status: summary.ready ? "pass" : "needs-data",',
|
|
2430
|
-
' source_uri_redacted: redactUri(source.uri),',
|
|
2431
|
-
' target_uri_redacted: redactUri(targetUri),',
|
|
2432
|
-
' ...summary,',
|
|
2433
|
-
' result_path: resultPath',
|
|
2434
|
-
' }, summary.ready ? 0 : 4);',
|
|
2435
|
-
' } finally {',
|
|
2436
|
-
' await sourceClient.close().catch(() => undefined);',
|
|
2437
|
-
' await targetClient.close().catch(() => undefined);',
|
|
2438
|
-
' }',
|
|
2439
|
-
'})().catch((error) => writeResult({ status: "failed", reason: error && (error.stack || error.message) || String(error), result_path: resultPath }, 2));',
|
|
2440
|
-
''
|
|
2441
|
-
].join('\n');
|
|
2442
|
-
}
|
|
2443
|
-
|
|
2444
|
-
export function buildResolveIORunnerBugfixComparisonQaScript(): string {
|
|
2445
|
-
return [
|
|
2446
|
-
'#!/usr/bin/env bash',
|
|
2447
|
-
'set -u',
|
|
2448
|
-
'TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"',
|
|
2449
|
-
'source "$TOOLS_DIR/env.sh"',
|
|
2450
|
-
'if resolveio_support_codex_lane_is_build 2>/dev/null; then',
|
|
2451
|
-
' resolveio_support_codex_block_build_lane_qa',
|
|
2452
|
-
' exit 86',
|
|
2453
|
-
'fi',
|
|
2454
|
-
'PROJECT_ROOT="${1:-}"',
|
|
2455
|
-
'if [ -z "$PROJECT_ROOT" ]; then',
|
|
2456
|
-
' echo "Usage: $0 <project-root> [baseline-ref] -- <qa-command...>" >&2',
|
|
2457
|
-
' exit 2',
|
|
2458
|
-
'fi',
|
|
2459
|
-
'shift || true',
|
|
2460
|
-
'DEFAULT_BASELINE_REF="${RESOLVEIO_RUNNER_QA_BASELINE_REF:-${RESOLVEIO_SUPPORT_QA_BASELINE_REF:-origin/master}}"',
|
|
2461
|
-
'if [ "${1:-}" = "--" ]; then',
|
|
2462
|
-
' BASELINE_REF="$DEFAULT_BASELINE_REF"',
|
|
2463
|
-
'else',
|
|
2464
|
-
' BASELINE_REF="${1:-$DEFAULT_BASELINE_REF}"',
|
|
2465
|
-
' if [ "$#" -gt 0 ]; then shift || true; fi',
|
|
2466
|
-
'fi',
|
|
2467
|
-
'if [ "${1:-}" = "--" ]; then shift; fi',
|
|
2468
|
-
'if [ "$#" -eq 0 ]; then',
|
|
2469
|
-
' echo "Usage: $0 <project-root> [baseline-ref] -- <qa-command...>" >&2',
|
|
2470
|
-
' exit 2',
|
|
2471
|
-
'fi',
|
|
2472
|
-
'PROJECT_ROOT="$(cd "$PROJECT_ROOT" && pwd)"',
|
|
2473
|
-
'REPO_ROOT="$(git -C "$PROJECT_ROOT" rev-parse --show-toplevel)"',
|
|
2474
|
-
'PROJECT_REL="$(git -C "$REPO_ROOT" ls-files --full-name "$PROJECT_ROOT" 2>/dev/null | head -1 | xargs dirname 2>/dev/null || true)"',
|
|
2475
|
-
'if [ -z "$PROJECT_REL" ] || [ "$PROJECT_REL" = "." ]; then',
|
|
2476
|
-
' PROJECT_REL="$(node - "$REPO_ROOT" "$PROJECT_ROOT" <<\'RESOLVEIO_REL\'',
|
|
2477
|
-
'const path = require("path");',
|
|
2478
|
-
'console.log(path.relative(process.argv[2], process.argv[3]) || ".");',
|
|
2479
|
-
'RESOLVEIO_REL',
|
|
2480
|
-
' )"',
|
|
2481
|
-
'fi',
|
|
2482
|
-
'ARTIFACT_DIR="$PROJECT_ROOT/qa-artifacts"',
|
|
2483
|
-
'mkdir -p "$ARTIFACT_DIR/baseline" "$ARTIFACT_DIR/candidate"',
|
|
2484
|
-
'RESULT_JSON="$ARTIFACT_DIR/bugfix-comparison-result.json"',
|
|
2485
|
-
'CANDIDATE_PATCH="$ARTIFACT_DIR/bugfix-candidate.patch"',
|
|
2486
|
-
'ORIGINAL_HEAD="$(git -C "$REPO_ROOT" rev-parse HEAD)"',
|
|
2487
|
-
'ORIGINAL_BRANCH="$(git -C "$REPO_ROOT" symbolic-ref --short HEAD 2>/dev/null || true)"',
|
|
2488
|
-
'QA_COMMAND=("$@")',
|
|
2489
|
-
'STOPPER="$TOOLS_DIR/stop-local-qa.sh"',
|
|
2490
|
-
'CURRENT_PHASE=""',
|
|
2491
|
-
'stop_local_qa() {',
|
|
2492
|
-
' "$STOPPER" "$PROJECT_ROOT" >/dev/null 2>&1 || true',
|
|
2493
|
-
'}',
|
|
2494
|
-
'write_json() {',
|
|
2495
|
-
' node - "$RESULT_JSON" "$@" <<\'RESOLVEIO_WRITE_JSON\'',
|
|
2496
|
-
'const fs = require("fs");',
|
|
2497
|
-
'const [path, status, baselineExit, candidateExit, baselineRef, originalHead, projectRel, note] = process.argv.slice(2);',
|
|
2498
|
-
'const payload = {',
|
|
2499
|
-
' status,',
|
|
2500
|
-
' baseline_ref: baselineRef,',
|
|
2501
|
-
' original_head: originalHead,',
|
|
2502
|
-
' project: projectRel,',
|
|
2503
|
-
' baseline_exit_code: Number(baselineExit),',
|
|
2504
|
-
' candidate_exit_code: Number(candidateExit),',
|
|
2505
|
-
' code_switch_proven: Number(baselineExit) !== 0 && Number(candidateExit) === 0,',
|
|
2506
|
-
' candidate_passed: Number(candidateExit) === 0,',
|
|
2507
|
-
' baseline_failed: Number(baselineExit) !== 0,',
|
|
2508
|
-
' note,',
|
|
2509
|
-
' artifact_dirs: { baseline: "qa-artifacts/baseline", candidate: "qa-artifacts/candidate" },',
|
|
2510
|
-
' created_at: new Date().toISOString()',
|
|
2511
|
-
'};',
|
|
2512
|
-
'fs.writeFileSync(path, JSON.stringify(payload, null, 2));',
|
|
2513
|
-
'console.log(JSON.stringify(payload, null, 2));',
|
|
2514
|
-
'RESOLVEIO_WRITE_JSON',
|
|
2515
|
-
'}',
|
|
2516
|
-
'snapshot_phase_artifacts() {',
|
|
2517
|
-
' local phase="$1"',
|
|
2518
|
-
' mkdir -p "$ARTIFACT_DIR/$phase"',
|
|
2519
|
-
' find "$ARTIFACT_DIR" -maxdepth 1 -type f \\( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.webp" -o -name "*.json" -o -name "*.txt" -o -name "*.log" -o -name "*.zip" \\) -print0 2>/dev/null | while IFS= read -r -d "" file; do',
|
|
2520
|
-
' cp "$file" "$ARTIFACT_DIR/$phase/$(basename "$file")" 2>/dev/null || true',
|
|
2521
|
-
' done',
|
|
2522
|
-
'}',
|
|
2523
|
-
'capture_candidate_patch() {',
|
|
2524
|
-
' : > "$CANDIDATE_PATCH"',
|
|
2525
|
-
' git -C "$REPO_ROOT" diff --binary -- "$PROJECT_REL" >> "$CANDIDATE_PATCH" || true',
|
|
2526
|
-
' git -C "$REPO_ROOT" diff --binary --cached -- "$PROJECT_REL" >> "$CANDIDATE_PATCH" || true',
|
|
2527
|
-
'}',
|
|
2528
|
-
'checkout_project_from_ref() {',
|
|
2529
|
-
' local ref="$1"',
|
|
2530
|
-
' git -C "$REPO_ROOT" checkout "$ref" -- "$PROJECT_REL"',
|
|
2531
|
-
'}',
|
|
2532
|
-
'restore_candidate() {',
|
|
2533
|
-
' stop_local_qa',
|
|
2534
|
-
' checkout_project_from_ref "$ORIGINAL_HEAD" || true',
|
|
2535
|
-
' if [ -s "$CANDIDATE_PATCH" ]; then',
|
|
2536
|
-
' git -C "$REPO_ROOT" apply --whitespace=nowarn "$CANDIDATE_PATCH" || true',
|
|
2537
|
-
' fi',
|
|
2538
|
-
'}',
|
|
2539
|
-
'trap restore_candidate EXIT',
|
|
2540
|
-
'run_phase() {',
|
|
2541
|
-
' local phase="$1"',
|
|
2542
|
-
' CURRENT_PHASE="$phase"',
|
|
2543
|
-
' stop_local_qa',
|
|
2544
|
-
' export RESOLVEIO_RUNNER_QA_PHASE="$phase"',
|
|
2545
|
-
' export RESOLVEIO_SUPPORT_QA_PHASE="$phase"',
|
|
2546
|
-
' export RESOLVEIO_RUNNER_QA_ARTIFACT_DIR="$ARTIFACT_DIR/$phase"',
|
|
2547
|
-
' export RESOLVEIO_SUPPORT_QA_ARTIFACT_DIR="$ARTIFACT_DIR/$phase"',
|
|
2548
|
-
' export RESOLVEIO_RUNNER_QA_PROJECT_ROOT="$PROJECT_ROOT"',
|
|
2549
|
-
' export RESOLVEIO_SUPPORT_QA_PROJECT_ROOT="$PROJECT_ROOT"',
|
|
2550
|
-
' export RESOLVEIO_RUNNER_QA_TOOLS_DIR="$TOOLS_DIR"',
|
|
2551
|
-
' export RESOLVEIO_SUPPORT_QA_TOOLS_DIR="$TOOLS_DIR"',
|
|
2552
|
-
' mkdir -p "$ARTIFACT_DIR/$phase"',
|
|
2553
|
-
' find "$ARTIFACT_DIR/$phase" -maxdepth 1 -type f \\( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.webp" -o -name "*.json" -o -name "*.txt" -o -name "*.log" -o -name "*.zip" \\) -delete 2>/dev/null || true',
|
|
2554
|
-
' find "$ARTIFACT_DIR" -maxdepth 1 -type f \\( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.webp" -o -name "*proof.json" -o -name "auth-bootstrap-result.json" \\) -delete 2>/dev/null || true',
|
|
2555
|
-
' RESOLVEIO_SUPPORT_QA_KEEPALIVE=true RESOLVEIO_RUNNER_QA_KEEPALIVE=true "$TOOLS_DIR/run-local-qa.sh" "$PROJECT_ROOT"',
|
|
2556
|
-
' local stack_rc="$?"',
|
|
2557
|
-
' if [ "$stack_rc" != "0" ]; then',
|
|
2558
|
-
' echo "ResolveIO bugfix comparison QA $phase phase blocked: local QA stack did not start (exit $stack_rc)." | tee "$ARTIFACT_DIR/$phase/qa-command.log"',
|
|
2559
|
-
' snapshot_phase_artifacts "$phase"',
|
|
2560
|
-
' stop_local_qa',
|
|
2561
|
-
' return "$stack_rc"',
|
|
2562
|
-
' fi',
|
|
2563
|
-
' if [ -f "$TOOLS_DIR/qa-live-data-seed.js" ]; then',
|
|
2564
|
-
' node "$TOOLS_DIR/qa-live-data-seed.js" "$PROJECT_ROOT"',
|
|
2565
|
-
' local seed_rc="$?"',
|
|
2566
|
-
' if [ "$seed_rc" != "0" ]; then',
|
|
2567
|
-
' echo "ResolveIO bugfix comparison QA $phase phase blocked: live data seed failed (exit $seed_rc)." | tee "$ARTIFACT_DIR/$phase/qa-command.log"',
|
|
2568
|
-
' snapshot_phase_artifacts "$phase"',
|
|
2569
|
-
' stop_local_qa',
|
|
2570
|
-
' return "$seed_rc"',
|
|
2571
|
-
' fi',
|
|
2572
|
-
' fi',
|
|
2573
|
-
' set +e',
|
|
2574
|
-
' (cd "$PROJECT_ROOT" && "${QA_COMMAND[@]}") 2>&1 | tee "$ARTIFACT_DIR/$phase/qa-command.log"',
|
|
2575
|
-
' local rc="${PIPESTATUS[0]}"',
|
|
2576
|
-
' set +e',
|
|
2577
|
-
' snapshot_phase_artifacts "$phase"',
|
|
2578
|
-
' stop_local_qa',
|
|
2579
|
-
' return "$rc"',
|
|
2580
|
-
'}',
|
|
2581
|
-
'echo "ResolveIO bugfix comparison QA preserving candidate diff for $PROJECT_REL"',
|
|
2582
|
-
'capture_candidate_patch',
|
|
2583
|
-
'stop_local_qa',
|
|
2584
|
-
'echo "ResolveIO bugfix comparison QA baseline phase: $BASELINE_REF"',
|
|
2585
|
-
'checkout_project_from_ref "$BASELINE_REF"',
|
|
2586
|
-
'run_phase baseline',
|
|
2587
|
-
'BASELINE_EXIT="$?"',
|
|
2588
|
-
'echo "ResolveIO bugfix comparison QA candidate phase: restored workspace candidate"',
|
|
2589
|
-
'restore_candidate',
|
|
2590
|
-
'run_phase candidate',
|
|
2591
|
-
'CANDIDATE_EXIT="$?"',
|
|
2592
|
-
'if [ "$CANDIDATE_EXIT" = "0" ] && [ "$BASELINE_EXIT" != "0" ]; then',
|
|
2593
|
-
' STATUS="pass"',
|
|
2594
|
-
' NOTE="Baseline failed and candidate passed for the same QA command."',
|
|
2595
|
-
'elif [ "$CANDIDATE_EXIT" = "0" ]; then',
|
|
2596
|
-
' STATUS="inconclusive_baseline_also_passed"',
|
|
2597
|
-
' NOTE="Candidate passed, but baseline also passed; the code switch did not prove the bug fix."',
|
|
2598
|
-
'else',
|
|
2599
|
-
' STATUS="fail_candidate_failed"',
|
|
2600
|
-
' NOTE="Candidate failed the QA command."',
|
|
2601
|
-
'fi',
|
|
2602
|
-
'write_json "$STATUS" "$BASELINE_EXIT" "$CANDIDATE_EXIT" "$BASELINE_REF" "$ORIGINAL_HEAD" "$PROJECT_REL" "$NOTE"',
|
|
2603
|
-
'[ "$CANDIDATE_EXIT" = "0" ] || exit "$CANDIDATE_EXIT"',
|
|
2604
|
-
'[ "$STATUS" = "pass" ] || exit 8',
|
|
2605
|
-
''
|
|
2606
|
-
].join('\n');
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
export function buildResolveIORunnerQaWorkflowProbeScript(): string {
|
|
2610
|
-
return [
|
|
2611
|
-
'#!/usr/bin/env node',
|
|
2612
|
-
"'use strict';",
|
|
2613
|
-
'',
|
|
2614
|
-
'const fs = require("fs");',
|
|
2615
|
-
'const http = require("http");',
|
|
2616
|
-
'const https = require("https");',
|
|
2617
|
-
'const path = require("path");',
|
|
2618
|
-
'',
|
|
2619
|
-
'if (isBuildLane()) {',
|
|
2620
|
-
' console.error("ResolveIO support lane guard: browser workflow probes are owned by the QA lane, not the 5.3 build lane.");',
|
|
2621
|
-
' process.exit(86);',
|
|
2622
|
-
'}',
|
|
2623
|
-
'',
|
|
2624
|
-
'const projectRoot = path.resolve(process.argv[2] || process.cwd());',
|
|
2625
|
-
'const artifactDir = path.resolve(process.env.RESOLVEIO_RUNNER_QA_ARTIFACT_DIR || process.env.RESOLVEIO_SUPPORT_QA_ARTIFACT_DIR || path.join(projectRoot, "qa-artifacts"));',
|
|
2626
|
-
'const routeArg = process.argv[3] || process.env.RESOLVEIO_RUNNER_QA_TARGET_ROUTE || process.env.RESOLVEIO_SUPPORT_QA_TARGET_ROUTE || "";',
|
|
2627
|
-
'const targetRoute = resolveTargetRoute(routeArg);',
|
|
2628
|
-
'const matrixPath = path.join(artifactDir, "qa-coverage-matrix.json");',
|
|
2629
|
-
'const resultPath = path.join(artifactDir, "qa-workflow-probe-result.json");',
|
|
2630
|
-
'const authBootstrapResultPath = path.join(artifactDir, "auth-bootstrap-result.json");',
|
|
2631
|
-
'const passScreenshotPath = path.join(artifactDir, "qa-workflow-route-ready.jpg");',
|
|
2632
|
-
'const failScreenshotPath = path.join(artifactDir, "qa-workflow-route-blocked.jpg");',
|
|
2633
|
-
'const clientUrl = stripTrailingSlash(process.env.RESOLVEIO_RUNNER_QA_CLIENT_URL || process.env.RESOLVEIO_SUPPORT_QA_CLIENT_URL || `http://localhost:${process.env.RESOLVEIO_SUPPORT_QA_CLIENT_PORT || "4200"}`);',
|
|
2634
|
-
'const serverUrl = stripTrailingSlash(process.env.RESOLVEIO_RUNNER_QA_SERVER_URL || process.env.RESOLVEIO_SUPPORT_QA_SERVER_URL || "http://localhost:8080");',
|
|
2635
|
-
'const explicitQaUsername = process.env.RESOLVEIO_RUNNER_QA_AFFECTED_USER_EMAIL || process.env.RESOLVEIO_SUPPORT_QA_AFFECTED_USER_EMAIL || process.env.RESOLVEIO_RUNNER_QA_TICKET_REPORTER_EMAIL || process.env.RESOLVEIO_SUPPORT_QA_TICKET_REPORTER_EMAIL || "";',
|
|
2636
|
-
'const envQaUsername = process.env.RESOLVEIO_RUNNER_QA_USERNAME || process.env.RESOLVEIO_SUPPORT_QA_USERNAME || "";',
|
|
2637
|
-
'const seededQaUsername = readSeededPreferredQaUsername();',
|
|
2638
|
-
'const username = explicitQaUsername || seededQaUsername || envQaUsername || "admin";',
|
|
2639
|
-
'const password = process.env.RESOLVEIO_RUNNER_QA_PASSWORD || process.env.RESOLVEIO_SUPPORT_QA_PASSWORD || "";',
|
|
2640
|
-
'const viewportWidth = Number(process.env.RESOLVEIO_RUNNER_QA_VIEWPORT_WIDTH || process.env.RESOLVEIO_SUPPORT_QA_VIEWPORT_WIDTH || 1920);',
|
|
2641
|
-
'const viewportHeight = Number(process.env.RESOLVEIO_RUNNER_QA_VIEWPORT_HEIGHT || process.env.RESOLVEIO_SUPPORT_QA_VIEWPORT_HEIGHT || 1080);',
|
|
2642
|
-
'',
|
|
2643
|
-
'function isBuildLane() {',
|
|
2644
|
-
' return /^(build|support_build|support-build)$/i.test(String(process.env.RESOLVEIO_SUPPORT_CODEX_LANE || "").trim());',
|
|
2645
|
-
'}',
|
|
2646
|
-
'',
|
|
2647
|
-
'function stripTrailingSlash(value) { return String(value || "").replace(/\\/+$/, ""); }',
|
|
2648
|
-
'function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }',
|
|
2649
|
-
'function writeJson(filePath, payload) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(payload, null, 2)); }',
|
|
2650
|
-
'function readJson(filePath) { try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch (error) { return null; } }',
|
|
2651
|
-
'function readSeedHintText() {',
|
|
2652
|
-
' const repoRoot = path.dirname(projectRoot);',
|
|
2653
|
-
' const chunks = [',
|
|
2654
|
-
' process.env.RESOLVEIO_SUPPORT_QA_ROW_FILTER || "",',
|
|
2655
|
-
' process.env.RESOLVEIO_SUPPORT_QA_CURRENT_ROW_WORKFLOW || "",',
|
|
2656
|
-
' process.env.RESOLVEIO_SUPPORT_QA_CURRENT_ROW_ASSERTION || "",',
|
|
2657
|
-
' process.env.RESOLVEIO_RUNNER_QA_ROW_FILTER || "",',
|
|
2658
|
-
' process.env.RESOLVEIO_RUNNER_QA_CURRENT_ROW_WORKFLOW || "",',
|
|
2659
|
-
' process.env.RESOLVEIO_RUNNER_QA_CURRENT_ROW_ASSERTION || "",',
|
|
2660
|
-
' process.env.RESOLVEIO_QA_SEED_HINTS || "",',
|
|
2661
|
-
' process.env.RESOLVEIO_SUPPORT_QA_SEED_HINTS || "",',
|
|
2662
|
-
' process.env.RESOLVEIO_RUNNER_QA_SEED_HINTS || ""',
|
|
2663
|
-
' ];',
|
|
2664
|
-
' const candidates = [',
|
|
2665
|
-
' path.join(artifactDir, "qa-coverage-matrix.json"),',
|
|
2666
|
-
' path.join(artifactDir, "qa-row-lock.json"),',
|
|
2667
|
-
' path.join(repoRoot, ".resolveio-support-context", "manual-ticket.request.txt"),',
|
|
2668
|
-
' path.join(projectRoot, ".resolveio-support-context", "manual-ticket.request.txt"),',
|
|
2669
|
-
' path.join(repoRoot, ".resolveio-context", "manual-ticket.request.txt"),',
|
|
2670
|
-
' path.join(projectRoot, ".resolveio-context", "manual-ticket.request.txt")',
|
|
2671
|
-
' ];',
|
|
2672
|
-
' for (const candidate of candidates) {',
|
|
2673
|
-
' try {',
|
|
2674
|
-
' const raw = fs.readFileSync(candidate, "utf8");',
|
|
2675
|
-
' chunks.push(raw);',
|
|
2676
|
-
' if (/qa-coverage-matrix\\.json$/.test(candidate)) {',
|
|
2677
|
-
' const matrix = JSON.parse(raw);',
|
|
2678
|
-
' const rows = Array.isArray(matrix && matrix.rows) ? matrix.rows : [];',
|
|
2679
|
-
' for (const row of rows) chunks.push([row.workflow, row.route, row.assertion, row.required_proof, row.data_id, row.data_name, row.blocker, row.evidence, row.caption].filter(Boolean).join("\\n"));',
|
|
2680
|
-
' }',
|
|
2681
|
-
' if (/qa-row-lock\\.json$/.test(candidate)) {',
|
|
2682
|
-
' const lock = JSON.parse(raw);',
|
|
2683
|
-
' chunks.push([lock.workflow, lock.route, lock.assertion, lock.required_proof, lock.rowFilter, lock.blocker].filter(Boolean).join("\\n"));',
|
|
2684
|
-
' }',
|
|
2685
|
-
' } catch (error) {}',
|
|
2686
|
-
' }',
|
|
2687
|
-
' return chunks.filter(Boolean).join("\\n");',
|
|
2688
|
-
'}',
|
|
2689
|
-
'function readSeededPreferredQaUsername() {',
|
|
2690
|
-
' try {',
|
|
2691
|
-
' const seed = readJson(path.join(artifactDir, "qa-live-data-seed-result.json"));',
|
|
2692
|
-
' const context = seed && seed.selected && seed.selected.qa_user_context;',
|
|
2693
|
-
' const preferred = context && (context.preferred_username || context.preferred_email || context.affected_user_email || context.reporter_email);',
|
|
2694
|
-
' return String(preferred || "").trim();',
|
|
2695
|
-
' } catch (error) {',
|
|
2696
|
-
' return "";',
|
|
2697
|
-
' }',
|
|
2698
|
-
'}',
|
|
2699
|
-
'function normalizeRoute(candidate) {',
|
|
2700
|
-
' const value = String(candidate || "").trim();',
|
|
2701
|
-
' if (!value) return "";',
|
|
2702
|
-
' let routeText = value;',
|
|
2703
|
-
' if (/^https?:\\/\\//i.test(routeText)) {',
|
|
2704
|
-
' try { routeText = new URL(routeText).pathname || "/"; } catch (error) { return ""; }',
|
|
2705
|
-
' }',
|
|
2706
|
-
" const match = routeText.match(/(?:^|[\\s\\\"'`(])((?:\\/[a-z0-9][a-z0-9._~!$&'()*+,;=:@%\\/-]*)(?:\\?[^\\s\\\"'`<>)]*)?)/i);",
|
|
2707
|
-
' routeText = ((match && match[1]) || routeText).split(/\\s*->\\s*/)[0].split(/\\s+-\\s+/)[0].trim().replace(/[),.;]+$/g, "");',
|
|
2708
|
-
' if (!routeText) return "";',
|
|
2709
|
-
' return routeText.startsWith("/") ? routeText : `/${routeText}`;',
|
|
2710
|
-
'}',
|
|
2711
|
-
'function resolveTargetRoute(explicitRoute) {',
|
|
2712
|
-
' const hintText = readSeedHintText();',
|
|
2713
|
-
' const preferInterchangeables = /\\b(interchangeable|interchangeables|mass\\s+update|update\\s+interchangeables|from\\s+chemical|to\\s+chemical|treatment\\s+plan|treatment\\s+plans|production\\s+location)\\b/i.test(hintText);',
|
|
2714
|
-
' const seedCandidates = [',
|
|
2715
|
-
' path.join(artifactDir, "qa-live-data-seed-result.json"),',
|
|
2716
|
-
' path.join(artifactDir, "candidate", "qa-live-data-seed-result.json")',
|
|
2717
|
-
' ];',
|
|
2718
|
-
' for (const seedPath of seedCandidates) {',
|
|
2719
|
-
' const seed = readJson(seedPath);',
|
|
2720
|
-
' const interchangeablesRoutes = (seed && seed.selected && seed.selected.qa_production_interchangeables_context && seed.selected.qa_production_interchangeables_context.browser_routes) || (seed && seed.qa_production_interchangeables_context && seed.qa_production_interchangeables_context.browser_routes);',
|
|
2721
|
-
' const interchangeablesRoute = normalizeRoute(interchangeablesRoutes && (interchangeablesRoutes.update_interchangeables || interchangeablesRoutes.detail || interchangeablesRoutes.list));',
|
|
2722
|
-
' if (preferInterchangeables && interchangeablesRoute) return interchangeablesRoute;',
|
|
2723
|
-
' const routes = seed && seed.selected && seed.selected.truck_treating_bol_context && seed.selected.truck_treating_bol_context.browser_routes;',
|
|
2724
|
-
' const route = normalizeRoute(routes && (routes.delivery || routes.detail || routes.list));',
|
|
2725
|
-
' if (!preferInterchangeables && route) return route;',
|
|
2726
|
-
' }',
|
|
2727
|
-
' if (preferInterchangeables) return "/production-location/update-interchangeables";',
|
|
2728
|
-
' const explicit = normalizeRoute(explicitRoute);',
|
|
2729
|
-
' if (explicit) return explicit;',
|
|
2730
|
-
' return "/";',
|
|
2731
|
-
'}',
|
|
2732
|
-
'function requestReady(url) {',
|
|
2733
|
-
' return new Promise((resolve) => {',
|
|
2734
|
-
' try {',
|
|
2735
|
-
' const parsed = new URL(url);',
|
|
2736
|
-
' const mod = parsed.protocol === "https:" ? https : http;',
|
|
2737
|
-
' const req = mod.get(parsed, { timeout: 3000 }, (res) => { res.resume(); resolve(Boolean(res.statusCode && res.statusCode < 500)); });',
|
|
2738
|
-
' req.on("timeout", () => req.destroy(new Error("timeout")));',
|
|
2739
|
-
' req.on("error", () => resolve(false));',
|
|
2740
|
-
' } catch (error) { resolve(false); }',
|
|
2741
|
-
' });',
|
|
2742
|
-
'}',
|
|
2743
|
-
'async function waitForHttpReady(url, label) {',
|
|
2744
|
-
' const deadline = Date.now() + 45000;',
|
|
2745
|
-
' while (Date.now() < deadline) {',
|
|
2746
|
-
' if (await requestReady(url)) return;',
|
|
2747
|
-
' await delay(1500);',
|
|
2748
|
-
' }',
|
|
2749
|
-
' throw new Error(`${label} did not become ready at ${url}`);',
|
|
2750
|
-
'}',
|
|
2751
|
-
'function requestJson(url, payload) {',
|
|
2752
|
-
' return new Promise((resolve, reject) => {',
|
|
2753
|
-
' const body = JSON.stringify(payload || {});',
|
|
2754
|
-
' const parsed = new URL(url);',
|
|
2755
|
-
' const mod = parsed.protocol === "https:" ? https : http;',
|
|
2756
|
-
' const req = mod.request(parsed, { method: "POST", timeout: 20000, headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body), "origin": clientUrl } }, (res) => {',
|
|
2757
|
-
' let raw = "";',
|
|
2758
|
-
' res.setEncoding("utf8");',
|
|
2759
|
-
' res.on("data", (chunk) => { raw += chunk; });',
|
|
2760
|
-
' res.on("end", () => {',
|
|
2761
|
-
' let json = null;',
|
|
2762
|
-
' try { json = raw ? JSON.parse(raw) : {}; } catch (error) { reject(new Error(`${url} returned non-JSON HTTP ${res.statusCode}: ${raw.slice(0, 300)}`)); return; }',
|
|
2763
|
-
' if (!res.statusCode || res.statusCode >= 400) { reject(new Error(`${url} returned HTTP ${res.statusCode}: ${JSON.stringify(json).slice(0, 500)}`)); return; }',
|
|
2764
|
-
' resolve(json);',
|
|
2765
|
-
' });',
|
|
2766
|
-
' });',
|
|
2767
|
-
' req.on("timeout", () => req.destroy(new Error(`${url} timed out`)));',
|
|
2768
|
-
' req.on("error", reject);',
|
|
2769
|
-
' req.write(body);',
|
|
2770
|
-
' req.end();',
|
|
2771
|
-
' });',
|
|
2772
|
-
'}',
|
|
2773
|
-
'function requirePuppeteer() {',
|
|
2774
|
-
' const candidates = [',
|
|
2775
|
-
' path.join(projectRoot, "server", "node_modules", "puppeteer", "lib", "cjs", "puppeteer", "puppeteer.js"),',
|
|
2776
|
-
' path.join(projectRoot, "node_modules", "puppeteer", "lib", "cjs", "puppeteer", "puppeteer.js"),',
|
|
2777
|
-
' path.join(process.cwd(), "server", "node_modules", "puppeteer", "lib", "cjs", "puppeteer", "puppeteer.js"),',
|
|
2778
|
-
' path.join(process.cwd(), "node_modules", "puppeteer", "lib", "cjs", "puppeteer", "puppeteer.js"),',
|
|
2779
|
-
' "puppeteer"',
|
|
2780
|
-
' ];',
|
|
2781
|
-
' for (const candidate of candidates) { try { return require(candidate); } catch (error) {} }',
|
|
2782
|
-
' throw new Error("Unable to require puppeteer from project/server node_modules or global resolution");',
|
|
2783
|
-
'}',
|
|
2784
|
-
'async function launchBrowser(puppeteer) {',
|
|
2785
|
-
' const launchOptions = { headless: true, defaultViewport: { width: viewportWidth, height: viewportHeight }, args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", `--window-size=${viewportWidth},${viewportHeight}`] };',
|
|
2786
|
-
' if (process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_BIN) launchOptions.executablePath = process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_BIN;',
|
|
2787
|
-
' return puppeteer.launch(launchOptions);',
|
|
2788
|
-
'}',
|
|
2789
|
-
'async function login() {',
|
|
2790
|
-
' if (!password) throw new Error("QA password is empty; source the generated env.sh before workflow probe");',
|
|
2791
|
-
' const loginJson = await requestJson(`${serverUrl}/login`, { username, password });',
|
|
2792
|
-
' const refreshToken = loginJson && loginJson.result && loginJson.result.token;',
|
|
2793
|
-
' if (loginJson.error || !refreshToken) throw new Error(`Login failed: ${JSON.stringify(loginJson).slice(0, 800)}`);',
|
|
2794
|
-
' const accessJson = await requestJson(`${serverUrl}/accessToken`, { refreshToken });',
|
|
2795
|
-
' const accessToken = accessJson && accessJson.result && accessJson.result.token;',
|
|
2796
|
-
' const user = accessJson && accessJson.result && accessJson.result.user;',
|
|
2797
|
-
' if (accessJson.error || !accessToken || !user) throw new Error(`Access token failed: ${JSON.stringify(accessJson).slice(0, 800)}`);',
|
|
2798
|
-
' return { refreshToken, accessToken, user };',
|
|
2799
|
-
'}',
|
|
2800
|
-
'async function seedAuth(page, auth) {',
|
|
2801
|
-
' await page.goto(clientUrl, { waitUntil: "domcontentloaded", timeout: 45000 });',
|
|
2802
|
-
' await page.evaluate((payload) => {',
|
|
2803
|
-
' localStorage.clear();',
|
|
2804
|
-
' sessionStorage.clear();',
|
|
2805
|
-
' localStorage.setItem("refreshToken", payload.refreshToken);',
|
|
2806
|
-
' localStorage.setItem("accessToken", payload.accessToken);',
|
|
2807
|
-
' localStorage.setItem("user", JSON.stringify({ ...(payload.user || {}), other: { ...((payload.user || {}).other || {}), tour_completed: true, took_tour: true }, settings: { ...((payload.user || {}).settings || {}), collapsable_menu: false, opening_route: payload.targetRoute } }));',
|
|
2808
|
-
' localStorage.setItem("lastURL", payload.targetRoute);',
|
|
2809
|
-
' }, { ...auth, targetRoute });',
|
|
2810
|
-
'}',
|
|
2811
|
-
'async function pageSummary(page) {',
|
|
2812
|
-
' return page.evaluate(() => {',
|
|
2813
|
-
' const bodyText = (document.body && document.body.innerText || "").replace(/\\s+/g, " ").trim();',
|
|
2814
|
-
' const rows = Array.from(document.querySelectorAll("tr")).map(row => (row.innerText || "").replace(/\\s+/g, " ").trim()).filter(Boolean);',
|
|
2815
|
-
' return { url: location.href, title: document.title, bodyTextSnippet: bodyText.slice(0, 1200), rowCount: rows.length, rows: rows.slice(0, 8), hasLoginText: /Employee\\/Customer Login|Employee Sign In|Customer Access|Unable to sign in/i.test(bodyText), hasOfflineModeText: bodyText.includes("*** OFFLINE MODE ***"), storageState: { hasRefreshToken: !!localStorage.getItem("refreshToken"), hasAccessToken: !!localStorage.getItem("accessToken"), hasUser: !!localStorage.getItem("user") } };',
|
|
2816
|
-
' });',
|
|
2817
|
-
'}',
|
|
2818
|
-
'function pathMatchesRoute(currentUrl, expectedRoute) {',
|
|
2819
|
-
' try {',
|
|
2820
|
-
' const current = new URL(currentUrl);',
|
|
2821
|
-
' const expected = new URL(expectedRoute, clientUrl);',
|
|
2822
|
-
' return current.pathname.replace(/\\/+$/, "") === expected.pathname.replace(/\\/+$/, "");',
|
|
2823
|
-
' } catch (error) {',
|
|
2824
|
-
' return false;',
|
|
2825
|
-
' }',
|
|
2826
|
-
'}',
|
|
2827
|
-
'async function waitForAuthenticatedApp(page) {',
|
|
2828
|
-
' await page.goto(`${clientUrl}/home`, { waitUntil: "domcontentloaded", timeout: 60000 });',
|
|
2829
|
-
' const deadline = Date.now() + Number(process.env.RESOLVEIO_RUNNER_QA_AUTH_WARMUP_TIMEOUT_MS || process.env.RESOLVEIO_SUPPORT_QA_AUTH_WARMUP_TIMEOUT_MS || 45000);',
|
|
2830
|
-
' let summary = await pageSummary(page);',
|
|
2831
|
-
' while (Date.now() < deadline) {',
|
|
2832
|
-
' summary = await pageSummary(page);',
|
|
2833
|
-
' if (!summary.hasLoginText && !summary.hasOfflineModeText && summary.storageState.hasRefreshToken && summary.storageState.hasAccessToken && summary.storageState.hasUser) {',
|
|
2834
|
-
' await delay(Number(process.env.RESOLVEIO_RUNNER_QA_POST_AUTH_SETTLE_MS || process.env.RESOLVEIO_SUPPORT_QA_POST_AUTH_SETTLE_MS || 2500));',
|
|
2835
|
-
' return await pageSummary(page);',
|
|
2836
|
-
' }',
|
|
2837
|
-
' await delay(1000);',
|
|
2838
|
-
' }',
|
|
2839
|
-
' throw new Error(`QA auth warmup failed before target route. Page summary: ${JSON.stringify(summary).slice(0, 1000)}`);',
|
|
2840
|
-
'}',
|
|
2841
|
-
'function isShellOnlySummary(summary) {',
|
|
2842
|
-
' const text = summary && summary.bodyTextSnippet || "";',
|
|
2843
|
-
' if (!text) return true;',
|
|
2844
|
-
' if (summary && Number(summary.rowCount || 0) > 0) return false;',
|
|
2845
|
-
' if (/\\b(List|Detail|Edit|New|Filter|Search|Showing|records|CLASSIFICATION|STATUS|Entries Per Page|Dashboard|Report|Invoice|Asset|BOL)\\b/i.test(text)) return false;',
|
|
2846
|
-
' return /^(Home|MENU|Logout|\\u00A9|The All-in-One Software)\\b/i.test(text) && text.length < 350;',
|
|
2847
|
-
'}',
|
|
2848
|
-
'function freshAuthBootstrapProof() {',
|
|
2849
|
-
' const proof = readJson(authBootstrapResultPath);',
|
|
2850
|
-
' if (!proof || proof.status !== "pass" || !proof.page) return null;',
|
|
2851
|
-
' if (!pathMatchesRoute(proof.page.url || "", targetRoute)) return null;',
|
|
2852
|
-
' if (proof.targetRoute && !pathMatchesRoute(new URL(proof.targetRoute, clientUrl).href, targetRoute)) return null;',
|
|
2853
|
-
' if (isShellOnlySummary(proof.page)) return null;',
|
|
2854
|
-
' const text = proof.page.bodyTextSnippet || "";',
|
|
2855
|
-
' if (!text || /Employee\\/Customer Login|Employee Sign In|Customer Access|Unable to sign in/i.test(text)) return null;',
|
|
2856
|
-
' if (text.includes("*** OFFLINE MODE ***")) return null;',
|
|
2857
|
-
' try {',
|
|
2858
|
-
' const stat = fs.statSync(authBootstrapResultPath);',
|
|
2859
|
-
' const maxAgeMs = Number(process.env.RESOLVEIO_RUNNER_QA_AUTH_PROOF_MAX_AGE_MS || process.env.RESOLVEIO_SUPPORT_QA_AUTH_PROOF_MAX_AGE_MS || 300000);',
|
|
2860
|
-
' if (Date.now() - stat.mtimeMs > maxAgeMs) return null;',
|
|
2861
|
-
' } catch (error) { return null; }',
|
|
2862
|
-
' return proof;',
|
|
2863
|
-
'}',
|
|
2864
|
-
'async function waitForHydratedTargetRoute(page) {',
|
|
2865
|
-
' const deadline = Date.now() + Number(process.env.RESOLVEIO_RUNNER_QA_ROUTE_HYDRATION_TIMEOUT_MS || process.env.RESOLVEIO_SUPPORT_QA_ROUTE_HYDRATION_TIMEOUT_MS || 45000);',
|
|
2866
|
-
' let summary = await pageSummary(page);',
|
|
2867
|
-
' while (Date.now() < deadline) {',
|
|
2868
|
-
' summary = await pageSummary(page);',
|
|
2869
|
-
' const routeOk = pathMatchesRoute(summary.url, targetRoute);',
|
|
2870
|
-
' const shellOnly = isShellOnlySummary(summary);',
|
|
2871
|
-
' if (routeOk && !summary.hasLoginText && !summary.hasOfflineModeText && !shellOnly) {',
|
|
2872
|
-
' return summary;',
|
|
2873
|
-
' }',
|
|
2874
|
-
' if (summary.hasLoginText || summary.hasOfflineModeText || !routeOk || shellOnly) {',
|
|
2875
|
-
' await delay(1500);',
|
|
2876
|
-
' await page.goto(new URL(targetRoute, clientUrl).href, { waitUntil: "domcontentloaded", timeout: 60000 }).catch(() => null);',
|
|
2877
|
-
' } else {',
|
|
2878
|
-
' await delay(1500);',
|
|
2879
|
-
' }',
|
|
2880
|
-
' }',
|
|
2881
|
-
' throw new Error(`QA route hydration failed for ${targetRoute}. Page summary: ${JSON.stringify(summary).slice(0, 1000)}`);',
|
|
2882
|
-
'}',
|
|
2883
|
-
'function billingDashboardHasVisibleWork(text) {',
|
|
2884
|
-
' const value = String(text || "");',
|
|
2885
|
-
' const patterns = [',
|
|
2886
|
-
' /PSO Type Invoice Awaiting Invoices, Customers: \\((\\d+)\\), Deliveries: \\((\\d+)\\)/i,',
|
|
2887
|
-
' /Deliveries Awaiting Invoices, Customers: \\((\\d+)\\), Treatments: \\((\\d+)\\)/i,',
|
|
2888
|
-
' /Truck Treatments Awaiting Invoices, Customers: \\((\\d+)\\), Treatments: \\((\\d+)\\)/i,',
|
|
2889
|
-
' /Periodic Billing, Customers: \\((\\d+)\\), Items: \\((\\d+)\\)/i,',
|
|
2890
|
-
' /Custom Consolidated Invoicing, Customers: \\((\\d+)\\), Items: \\((\\d+)\\)/i,',
|
|
2891
|
-
' /Processing, Customers: \\((\\d+)\\), Invoices: \\((\\d+)\\)/i,',
|
|
2892
|
-
' /Prepared, Customers: \\((\\d+)\\), Invoices: \\((\\d+)\\)/i,',
|
|
2893
|
-
' /Open Invoices, Customers: \\((\\d+)\\), Invoices: \\((\\d+)\\)/i',
|
|
2894
|
-
' ];',
|
|
2895
|
-
' for (const pattern of patterns) {',
|
|
2896
|
-
' const match = pattern.exec(value);',
|
|
2897
|
-
' if (match && (Number(match[1]) > 0 || Number(match[2]) > 0)) return true;',
|
|
2898
|
-
' }',
|
|
2899
|
-
' return false;',
|
|
2900
|
-
'}',
|
|
2901
|
-
'async function waitForBillingDashboardWork(page) {',
|
|
2902
|
-
' if (!/\\/billing(?:$|[?#])|\\/dashboard\\/billing(?:$|[?#])/.test(targetRoute)) return null;',
|
|
2903
|
-
' const deadline = Date.now() + Number(process.env.RESOLVEIO_RUNNER_QA_BILLING_DATA_TIMEOUT_MS || process.env.RESOLVEIO_SUPPORT_QA_BILLING_DATA_TIMEOUT_MS || 45000);',
|
|
2904
|
-
' let summary = await pageSummary(page);',
|
|
2905
|
-
' while (Date.now() < deadline) {',
|
|
2906
|
-
' summary = await pageSummary(page);',
|
|
2907
|
-
' if (billingDashboardHasVisibleWork(summary.bodyTextSnippet)) return summary;',
|
|
2908
|
-
' await delay(1500);',
|
|
2909
|
-
' }',
|
|
2910
|
-
' throw new Error(`Billing Dashboard loaded but no actionable seeded billing rows became visible before QA handoff. This is a runner data-seeding blocker, not a feature failure. Page summary: ${JSON.stringify(summary).slice(0, 1000)}`);',
|
|
2911
|
-
'}',
|
|
2912
|
-
'function matrixRows(matrix) { return Array.isArray(matrix && matrix.rows) ? matrix.rows : []; }',
|
|
2913
|
-
'function activeRowText() {',
|
|
2914
|
-
' const matrix = readJson(matrixPath) || {};',
|
|
2915
|
-
' const row = matrixRows(matrix).find((candidate) => !/^(pass|passed)$/i.test(String(candidate && candidate.status || ""))) || matrixRows(matrix)[0] || {};',
|
|
2916
|
-
' const rowText = [row.workflow, row.route, row.assertion, row.required_proof].map((value) => String(value || "")).join("\\n");',
|
|
2917
|
-
' return rowText.trim() ? rowText : readSeedHintText();',
|
|
2918
|
-
'}',
|
|
2919
|
-
'function readSeedAssetContext() {',
|
|
2920
|
-
' const seed = readJson(path.join(artifactDir, "qa-live-data-seed-result.json"));',
|
|
2921
|
-
' const context = seed && seed.selected && seed.selected.qa_asset_context;',
|
|
2922
|
-
' if (!context || !Array.isArray(context.asset_ids) || !context.asset_ids.length) return null;',
|
|
2923
|
-
' return context;',
|
|
2924
|
-
'}',
|
|
2925
|
-
'function depsCacheModuleCandidates(moduleName) {',
|
|
2926
|
-
' const roots = uniqueStrings([',
|
|
2927
|
-
' process.env.RESOLVEIO_RUNNER_QA_DEPS_CACHE_ROOT,',
|
|
2928
|
-
' process.env.RESOLVEIO_SUPPORT_QA_DEPS_CACHE_ROOT,',
|
|
2929
|
-
' path.join(projectRoot, ".deps-cache"),',
|
|
2930
|
-
' path.join(path.dirname(projectRoot), ".deps-cache"),',
|
|
2931
|
-
' path.join(path.dirname(path.dirname(projectRoot)), ".deps-cache"),',
|
|
2932
|
-
' "/var/app/resolveio-ai-workspace/.deps-cache/resolveio-all"',
|
|
2933
|
-
' ]);',
|
|
2934
|
-
' const candidates = [];',
|
|
2935
|
-
' for (const root of roots) {',
|
|
2936
|
-
' try {',
|
|
2937
|
-
' if (!root || !fs.existsSync(root)) continue;',
|
|
2938
|
-
' for (const entry of fs.readdirSync(root)) {',
|
|
2939
|
-
' candidates.push(path.join(root, entry, "node_modules", moduleName));',
|
|
2940
|
-
' candidates.push(path.join(root, entry, "node_modules", "@resolveio", "server-lib", "node_modules", moduleName));',
|
|
2941
|
-
' }',
|
|
2942
|
-
' } catch (error) {}',
|
|
2943
|
-
' }',
|
|
2944
|
-
' return candidates;',
|
|
2945
|
-
'}',
|
|
2946
|
-
'function requireQaModule(moduleName) {',
|
|
2947
|
-
' const candidates = [',
|
|
2948
|
-
' path.join(projectRoot, "server", "node_modules", moduleName),',
|
|
2949
|
-
' path.join(projectRoot, "node_modules", moduleName),',
|
|
2950
|
-
' path.join(process.cwd(), "server", "node_modules", moduleName),',
|
|
2951
|
-
' path.join(process.cwd(), "node_modules", moduleName),',
|
|
2952
|
-
' ...depsCacheModuleCandidates(moduleName),',
|
|
2953
|
-
' moduleName',
|
|
2954
|
-
' ];',
|
|
2955
|
-
' for (const candidate of candidates) { try { return require(candidate); } catch (error) {} }',
|
|
2956
|
-
' throw new Error(`Unable to require ${moduleName} from project/server node_modules, QA dependency cache, or global resolution`);',
|
|
2957
|
-
'}',
|
|
2958
|
-
'function mongoRequire() { return requireQaModule("mongodb"); }',
|
|
2959
|
-
'function xlsxRequire() { return requireQaModule("xlsx"); }',
|
|
2960
|
-
'function localMongoUrl() {',
|
|
2961
|
-
' return process.env.MONGO_URL || process.env.RESOLVEIO_RUNNER_QA_MONGO_URL || process.env.RESOLVEIO_SUPPORT_QA_MONGO_URL || `mongodb://127.0.0.1:${process.env.RESOLVEIO_SUPPORT_QA_MONGO_PORT || "3001"}/resolveio?directConnection=true`;',
|
|
2962
|
-
'}',
|
|
2963
|
-
'function safeLocalMongoUrl() {',
|
|
2964
|
-
' const url = localMongoUrl();',
|
|
2965
|
-
' if (/^(true|1|yes|on)$/i.test(String(process.env.RESOLVEIO_SUPPORT_QA_ALLOW_NONLOCAL_MONGO_PROOF || process.env.RESOLVEIO_RUNNER_QA_ALLOW_NONLOCAL_MONGO_PROOF || ""))) return url;',
|
|
2966
|
-
' try {',
|
|
2967
|
-
' const parsed = new URL(url);',
|
|
2968
|
-
' const host = String(parsed.hostname || "").toLowerCase();',
|
|
2969
|
-
' if (host === "127.0.0.1" || host === "localhost" || host === "::1") return url;',
|
|
2970
|
-
' } catch (error) {}',
|
|
2971
|
-
' throw new Error(`Pricing import proof refuses to write to non-local Mongo URL: ${url.replace(/:[^:@/]+@/, ":***@")}`);',
|
|
2972
|
-
'}',
|
|
2973
|
-
'function assetDisplayNumber(asset) { return String(asset && (asset.unit_number || asset.asset_number || asset.number || asset.name || asset._id) || ""); }',
|
|
2974
|
-
'function uniqueStrings(values) { return Array.from(new Set((values || []).map((value) => String(value || "").trim()).filter(Boolean))); }',
|
|
2975
|
-
'function staleLocationTerms(text) {',
|
|
2976
|
-
' const terms = [];',
|
|
2977
|
-
' const value = String(text || "");',
|
|
2978
|
-
' const staleMatch = /stale\\s+([^.;\\n]+)/i.exec(value);',
|
|
2979
|
-
' if (staleMatch) {',
|
|
2980
|
-
' for (const part of staleMatch[1].split(/,?\\s+and\\s+|\\s+or\\s+|;/i)) {',
|
|
2981
|
-
' const clean = part.replace(/\\b(absent|are|is|old|name|names)\\b/ig, " ").replace(/\\s+/g, " ").trim();',
|
|
2982
|
-
' if (/\\b[A-Z][A-Za-z]+(?:ton|port|city|ville|field)?\\b.*\\b[A-Z]{2}\\b/.test(clean)) terms.push(clean);',
|
|
2983
|
-
' }',
|
|
2984
|
-
' }',
|
|
2985
|
-
' for (const fallback of ["Jourdanton, TX", "Schreveport, LA", "Shreveport, LA"]) {',
|
|
2986
|
-
' if (new RegExp(fallback.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&"), "i").test(value)) terms.push(fallback);',
|
|
2987
|
-
' }',
|
|
2988
|
-
' return uniqueStrings(terms);',
|
|
2989
|
-
'}',
|
|
2990
|
-
'async function fetchAssetLocationProofData(context) {',
|
|
2991
|
-
' const { MongoClient, ObjectId } = mongoRequire();',
|
|
2992
|
-
' const client = new MongoClient(localMongoUrl());',
|
|
2993
|
-
' await client.connect();',
|
|
2994
|
-
' try {',
|
|
2995
|
-
' const db = client.db();',
|
|
2996
|
-
' const assetIds = uniqueStrings(context.asset_ids);',
|
|
2997
|
-
' const assetNumbers = uniqueStrings(context.asset_numbers);',
|
|
2998
|
-
' const objectIds = assetIds.filter((id) => /^[a-f0-9]{24}$/i.test(id)).map((id) => new ObjectId(id));',
|
|
2999
|
-
' const query = { $or: [] };',
|
|
3000
|
-
' if (objectIds.length) query.$or.push({ _id: { $in: objectIds } });',
|
|
3001
|
-
' if (assetNumbers.length) query.$or.push({ unit_number: { $in: assetNumbers } }, { asset_number: { $in: assetNumbers } }, { number: { $in: assetNumbers } });',
|
|
3002
|
-
' const assets = query.$or.length ? await db.collection("assets").find(query).toArray() : [];',
|
|
3003
|
-
' const yardIds = uniqueStrings([...(context.yard_ids || []), ...assets.map((asset) => asset && asset.current_location && asset.current_location.id), ...assets.map((asset) => asset && asset.id_default_yard), ...assets.flatMap((asset) => Array.isArray(asset && asset.locations) ? asset.locations.map((location) => location && location.id) : [])]);',
|
|
3004
|
-
' const yardObjectIds = yardIds.filter((id) => /^[a-f0-9]{24}$/i.test(id)).map((id) => new ObjectId(id));',
|
|
3005
|
-
' const yardQuery = yardIds.length ? { $or: [{ _id: { $in: yardIds } }, ...(yardObjectIds.length ? [{ _id: { $in: yardObjectIds } }] : [])] } : null;',
|
|
3006
|
-
' const yards = yardQuery ? await db.collection("yards").find(yardQuery).toArray() : [];',
|
|
3007
|
-
' const yardById = new Map(yards.map((yard) => [String(yard._id), yard]));',
|
|
3008
|
-
' const rows = assets.map((asset) => {',
|
|
3009
|
-
' const currentId = asset && asset.current_location && asset.current_location.id ? String(asset.current_location.id) : "";',
|
|
3010
|
-
' const currentYard = currentId ? yardById.get(currentId) : null;',
|
|
3011
|
-
' const defaultYardName = String(asset.default_yard || "");',
|
|
3012
|
-
' const currentName = asset && asset.current_location && asset.current_location.name ? String(asset.current_location.name) : "";',
|
|
3013
|
-
' const canonicalName = currentYard && currentYard.name ? String(currentYard.name) : (defaultYardName || currentName);',
|
|
3014
|
-
' return { id: String(asset._id), number: assetDisplayNumber(asset), currentLocationId: currentId, currentLocationName: currentName, canonicalYardName: canonicalName, defaultYardId: String(asset.id_default_yard || ""), defaultYardName, pass: Boolean(canonicalName && currentName === canonicalName) };',
|
|
3015
|
-
' });',
|
|
3016
|
-
' return { assetIds, assetNumbers, rows, yards: yards.map((yard) => ({ id: String(yard._id), name: yard.name })) };',
|
|
3017
|
-
' } finally {',
|
|
3018
|
-
' await client.close().catch(() => undefined);',
|
|
3019
|
-
' }',
|
|
3020
|
-
'}',
|
|
3021
|
-
'async function gotoAndSummarize(page, route, screenshotName, requiredTerms = []) {',
|
|
3022
|
-
' await page.goto(new URL(route, clientUrl).href, { waitUntil: "domcontentloaded", timeout: 60000 });',
|
|
3023
|
-
' await page.waitForSelector("body", { timeout: 30000 });',
|
|
3024
|
-
' let summary = await pageSummary(page);',
|
|
3025
|
-
' const deadline = Date.now() + Number(process.env.RESOLVEIO_SUPPORT_QA_ROW_WAIT_MS || 20000);',
|
|
3026
|
-
' while (Date.now() < deadline) {',
|
|
3027
|
-
' summary = await pageSummary(page);',
|
|
3028
|
-
' const body = String(summary.bodyTextSnippet || "");',
|
|
3029
|
-
' const hasTerms = requiredTerms.filter(Boolean).every((term) => new RegExp(String(term).replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&"), "i").test(body));',
|
|
3030
|
-
' if (hasTerms && !isShellOnlySummary(summary)) break;',
|
|
3031
|
-
' await delay(750);',
|
|
3032
|
-
' }',
|
|
3033
|
-
' await delay(Number(process.env.RESOLVEIO_RUNNER_QA_SCREENSHOT_SETTLE_MS || process.env.RESOLVEIO_SUPPORT_QA_SCREENSHOT_SETTLE_MS || 1000));',
|
|
3034
|
-
' await page.keyboard.press("Escape").catch(() => undefined);',
|
|
3035
|
-
' await delay(750);',
|
|
3036
|
-
' summary = await pageSummary(page);',
|
|
3037
|
-
' const screenshot = path.join(artifactDir, screenshotName);',
|
|
3038
|
-
' await page.screenshot({ path: screenshot, type: "jpeg", quality: 82, fullPage: false });',
|
|
3039
|
-
' return { route, screenshot, summary };',
|
|
3040
|
-
'}',
|
|
3041
|
-
'function shouldRunPricingImportProof() {',
|
|
3042
|
-
' const seed = readJson(path.join(artifactDir, "qa-live-data-seed-result.json")) || {};',
|
|
3043
|
-
' if (String(seed.profile || "").toLowerCase() === "pricing_import") return true;',
|
|
3044
|
-
' const text = [activeRowText(), targetRoute].join("\\n");',
|
|
3045
|
-
' return /\\/manage\\/pricing(?:$|[/?#])/i.test(targetRoute) && /\\b(pricing|price|gross\\s+price|production\\s+price)\\b/i.test(text) && /\\b(import|upload|template|xlsx|excel|workbook|spreadsheet)\\b/i.test(text);',
|
|
3046
|
-
'}',
|
|
3047
|
-
'function pricingWorkbookDirs() {',
|
|
3048
|
-
' const repoRoot = path.dirname(projectRoot);',
|
|
3049
|
-
' return uniqueStrings([',
|
|
3050
|
-
' path.join(projectRoot, ".resolveio-context", "attachments"),',
|
|
3051
|
-
' path.join(projectRoot, ".resolveio-support-context", "attachments"),',
|
|
3052
|
-
' path.join(repoRoot, ".resolveio-context", "attachments"),',
|
|
3053
|
-
' path.join(repoRoot, ".resolveio-support-context", "attachments"),',
|
|
3054
|
-
' path.join(artifactDir, "attachments")',
|
|
3055
|
-
' ]);',
|
|
3056
|
-
'}',
|
|
3057
|
-
'function collectPricingImportWorkbooks() {',
|
|
3058
|
-
' const byKey = new Map();',
|
|
3059
|
-
' for (const dir of pricingWorkbookDirs()) {',
|
|
3060
|
-
' try {',
|
|
3061
|
-
' if (!fs.existsSync(dir)) continue;',
|
|
3062
|
-
' for (const name of fs.readdirSync(dir)) {',
|
|
3063
|
-
' if (!/\\.xlsx$/i.test(name) || /^~\\$/.test(name)) continue;',
|
|
3064
|
-
' const fullPath = path.join(dir, name);',
|
|
3065
|
-
' const stat = fs.statSync(fullPath);',
|
|
3066
|
-
' if (!stat.isFile()) continue;',
|
|
3067
|
-
' const key = `${stat.size}:${name.replace(/[^a-z0-9]+/gi, "").toLowerCase()}`;',
|
|
3068
|
-
' if (!byKey.has(key)) byKey.set(key, fullPath);',
|
|
3069
|
-
' }',
|
|
3070
|
-
' } catch (error) {}',
|
|
3071
|
-
' }',
|
|
3072
|
-
' return Array.from(byKey.values()).sort();',
|
|
3073
|
-
'}',
|
|
3074
|
-
'function normalizeImportPriceRecord(line) {',
|
|
3075
|
-
' const source = line || {};',
|
|
3076
|
-
' const keyMap = {};',
|
|
3077
|
-
' for (const key of Object.keys(source)) keyMap[String(key).trim().toLowerCase().replace(/\\s+/g, " ")] = key;',
|
|
3078
|
-
' const getValue = (aliases) => { const alias = aliases.find((candidate) => keyMap[candidate] !== undefined); return alias ? source[keyMap[alias]] : undefined; };',
|
|
3079
|
-
' const normalized = {',
|
|
3080
|
-
' Customer: getValue(["customer"]),',
|
|
3081
|
-
' "Pricing Type": getValue(["pricing type", "type"]),',
|
|
3082
|
-
' Name: getValue(["name", "item", "chemical"]),',
|
|
3083
|
-
' Description: getValue(["description"]),',
|
|
3084
|
-
' "Gross Price": getValue(["gross price", "grossprice", "gross"]),',
|
|
3085
|
-
' "Discount Percent": getValue(["discount percent", "discount %", "discount", "discount percentage"]),',
|
|
3086
|
-
' Price: getValue(["price"])',
|
|
3087
|
-
' };',
|
|
3088
|
-
' if ((normalized["Gross Price"] === undefined || normalized["Gross Price"] === null || normalized["Gross Price"] === "") && normalized.Price !== undefined && normalized.Price !== null && normalized.Price !== "") normalized["Gross Price"] = normalized.Price;',
|
|
3089
|
-
' for (const key of Object.keys(normalized)) if (typeof normalized[key] === "string") normalized[key] = normalized[key].trim();',
|
|
3090
|
-
' return normalized;',
|
|
3091
|
-
'}',
|
|
3092
|
-
'function cleanNumber(value) { return String(value === undefined || value === null ? "" : value).trim().replace(/(\\$|,|\\s)/g, ""); }',
|
|
3093
|
-
'function roundMoney(value) { return Math.round(Number(value || 0) * 100) / 100; }',
|
|
3094
|
-
'function lowerTrim(value) { return String(value === undefined || value === null ? "" : value).toLowerCase().trim(); }',
|
|
3095
|
-
'function idString(value) { return String(value && value.toHexString ? value.toHexString() : value || ""); }',
|
|
3096
|
-
'function pricingSubType(pricingType) {',
|
|
3097
|
-
' const type = lowerTrim(pricingType);',
|
|
3098
|
-
' if (type === "chemical") return "chemical";',
|
|
3099
|
-
' if (type === "service") return "service";',
|
|
3100
|
-
' if (type === "misc") return "misc";',
|
|
3101
|
-
' return "";',
|
|
3102
|
-
'}',
|
|
3103
|
-
'async function readPricingImportContext(db) {',
|
|
3104
|
-
' const [customers, items, chemicals, users] = await Promise.all([',
|
|
3105
|
-
' db.collection("customers").find({}).project({ _id: 1, name: 1 }).toArray(),',
|
|
3106
|
-
' db.collection("items").find({}).project({ _id: 1, type: 1, name: 1, description: 1, id_chemical: 1 }).toArray(),',
|
|
3107
|
-
' db.collection("chemicals").find({}).project({ _id: 1, name: 1, description: 1, unit: 1, customers: 1 }).toArray(),',
|
|
3108
|
-
' db.collection("users").find({}).project({ _id: 1, fullname: 1, email: 1, username: 1 }).limit(5).toArray()',
|
|
3109
|
-
' ]);',
|
|
3110
|
-
' return { customers, items, chemicals, user: users[0] || null };',
|
|
3111
|
-
'}',
|
|
3112
|
-
'function validatePricingImportRows(rawRows, context) {',
|
|
3113
|
-
' const validPriceRecords = [];',
|
|
3114
|
-
' const errorPriceRecords = [];',
|
|
3115
|
-
' const rows = rawRows.map((row, index) => ({ ...normalizeImportPriceRecord(row), __sourceRow: index + 2 }));',
|
|
3116
|
-
' for (const sample of rows) {',
|
|
3117
|
-
' const errors = [];',
|
|
3118
|
-
' const errorValues = [];',
|
|
3119
|
-
' sample.db_customer_id = "";',
|
|
3120
|
-
' sample.db_item_id = "";',
|
|
3121
|
-
' sample.db_chemical_id = "";',
|
|
3122
|
-
' sample.Customer = sample.Customer !== undefined && sample.Customer !== null ? String(sample.Customer).trim() : "";',
|
|
3123
|
-
' sample.Name = sample.Name !== undefined && sample.Name !== null ? String(sample.Name).trim() : "";',
|
|
3124
|
-
' sample.Description = sample.Description ? String(sample.Description).trim() : "";',
|
|
3125
|
-
' sample.Price = sample.Price !== undefined && sample.Price !== null ? String(sample.Price).trim() : "";',
|
|
3126
|
-
' if (sample.Customer) {',
|
|
3127
|
-
' const customer = context.customers.find((candidate) => lowerTrim(candidate.name) === lowerTrim(sample.Customer));',
|
|
3128
|
-
' if (customer) { sample.db_customer_id = idString(customer._id); sample.Customer = customer.name; }',
|
|
3129
|
-
' else if (sample.Customer === "Standard") sample.db_customer_id = "";',
|
|
3130
|
-
' else { errors.push("Customer not in the system"); errorValues.push(sample.Customer); }',
|
|
3131
|
-
' } else { errors.push("No Customer"); errorValues.push(sample.Customer); }',
|
|
3132
|
-
' if (!sample["Pricing Type"]) { errors.push("No Pricing Type"); errorValues.push(sample["Pricing Type"]); }',
|
|
3133
|
-
' else if (!/^(Chemical|Service|Misc)$/.test(String(sample["Pricing Type"]))) { errors.push("Invalid Pricing Type"); errorValues.push(sample["Pricing Type"]); }',
|
|
3134
|
-
' const item = sample.Name ? context.items.find((candidate) => lowerTrim(candidate.type) === lowerTrim(sample["Pricing Type"]) && lowerTrim(candidate.name) === lowerTrim(sample.Name)) : null;',
|
|
3135
|
-
' if (item) {',
|
|
3136
|
-
' sample.db_item_id = idString(item._id);',
|
|
3137
|
-
' sample.db_chemical_id = idString(item.id_chemical);',
|
|
3138
|
-
' sample.Name = item.name;',
|
|
3139
|
-
' sample.Description = item.description || sample.Description;',
|
|
3140
|
-
' } else if (sample.Name) { errors.push("Item not in the system"); errorValues.push(sample.Name); }',
|
|
3141
|
-
' else { errors.push("No Item"); errorValues.push(sample.Name); }',
|
|
3142
|
-
' const gross = cleanNumber(sample["Gross Price"]);',
|
|
3143
|
-
' if (gross) {',
|
|
3144
|
-
' if (!/^(\\d+\\.?\\d*|\\d*\\.?\\d+)$/.test(gross)) { errors.push("Price is not a valid number"); errorValues.push(gross); }',
|
|
3145
|
-
' else sample["Gross Price"] = gross;',
|
|
3146
|
-
' } else { errors.push("No Gross Price"); errorValues.push(sample["Gross Price"]); }',
|
|
3147
|
-
' const discount = cleanNumber(sample["Discount Percent"]);',
|
|
3148
|
-
' if (discount) {',
|
|
3149
|
-
' if (!/^(\\d+\\.?\\d*|\\d*\\.?\\d+)$/.test(discount)) { errors.push("Discount Percent is not a valid number"); errorValues.push(discount); }',
|
|
3150
|
-
' else sample["Discount Percent"] = discount;',
|
|
3151
|
-
' }',
|
|
3152
|
-
' if (errors.length) errorPriceRecords.push({ ...sample, errors, error_values: errorValues });',
|
|
3153
|
-
' else validPriceRecords.push(sample);',
|
|
3154
|
-
' }',
|
|
3155
|
-
' return { validPriceRecords, errorPriceRecords, totalRows: rawRows.length };',
|
|
3156
|
-
'}',
|
|
3157
|
-
'function buildPricingImportPayload(validRows, context, sourceWorkbook, proofBatchId) {',
|
|
3158
|
-
' const { ObjectId } = mongoRequire();',
|
|
3159
|
-
' const user = context.user || {};',
|
|
3160
|
-
' return validRows.map((priceRecord) => {',
|
|
3161
|
-
' const subType = pricingSubType(priceRecord["Pricing Type"]);',
|
|
3162
|
-
' const item = context.items.find((candidate) => idString(candidate._id) === String(priceRecord.db_item_id));',
|
|
3163
|
-
' const chemical = priceRecord.db_chemical_id ? context.chemicals.find((candidate) => idString(candidate._id) === String(priceRecord.db_chemical_id)) : null;',
|
|
3164
|
-
' const grossPriceValue = Number(cleanNumber(priceRecord["Gross Price"] !== undefined && priceRecord["Gross Price"] !== null && priceRecord["Gross Price"] !== "" ? priceRecord["Gross Price"] : priceRecord.Price));',
|
|
3165
|
-
' const discountPercentValue = Number(cleanNumber(priceRecord["Discount Percent"] || 0)) || 0;',
|
|
3166
|
-
' return {',
|
|
3167
|
-
' _id: new ObjectId().toHexString(),',
|
|
3168
|
-
' __v: 0,',
|
|
3169
|
-
' type: "sale",',
|
|
3170
|
-
' sub_type: subType,',
|
|
3171
|
-
' date: new Date(),',
|
|
3172
|
-
' id_user: idString(user._id) || "qa-support-runner",',
|
|
3173
|
-
' user: user.fullname || user.email || user.username || "QA Support Runner",',
|
|
3174
|
-
' id_item: idString(item && item._id) || priceRecord.db_item_id || "",',
|
|
3175
|
-
' name: subType === "chemical" && chemical ? chemical.name : priceRecord.Name,',
|
|
3176
|
-
' description: subType === "chemical" && chemical ? (chemical.description || "") : (priceRecord.Description || ""),',
|
|
3177
|
-
' id_customer: priceRecord.Customer === "Standard" ? "" : (priceRecord.db_customer_id || ""),',
|
|
3178
|
-
' customer: priceRecord.Customer === "Standard" ? "Standard" : (priceRecord.Customer || ""),',
|
|
3179
|
-
' customer_type: "Production",',
|
|
3180
|
-
' id_chemical: chemical ? idString(chemical._id) : "",',
|
|
3181
|
-
' chemical: chemical ? chemical.name : "",',
|
|
3182
|
-
' unit: chemical && chemical.unit ? chemical.unit : "Each",',
|
|
3183
|
-
' price_gross: grossPriceValue,',
|
|
3184
|
-
' discount_percent: discountPercentValue,',
|
|
3185
|
-
' price: roundMoney(grossPriceValue * (1 - discountPercentValue / 100)),',
|
|
3186
|
-
' qa_support_pricing_import_proof: true,',
|
|
3187
|
-
' qa_proof_batch_id: proofBatchId,',
|
|
3188
|
-
' qa_source_workbook: path.basename(sourceWorkbook),',
|
|
3189
|
-
' qa_source_row: priceRecord.__sourceRow,',
|
|
3190
|
-
' qa_inserted_at: new Date()',
|
|
3191
|
-
' };',
|
|
3192
|
-
' });',
|
|
3193
|
-
'}',
|
|
3194
|
-
'async function runPricingImportProof(page, routeSummary) {',
|
|
3195
|
-
' if (!shouldRunPricingImportProof()) return null;',
|
|
3196
|
-
' const proofPath = path.join(artifactDir, "qa-pricing-import-proof.json");',
|
|
3197
|
-
' const businessAssertionPath = path.join(artifactDir, "aiqa-business-assertion.json");',
|
|
3198
|
-
' const beforeAfterScreenshotPath = path.join(artifactDir, "support-business-proof-before-after.png");',
|
|
3199
|
-
' const startedAt = new Date().toISOString();',
|
|
3200
|
-
' const proofBatchId = `support-pricing-import-${Date.now()}-${Math.random().toString(16).slice(2)}`;',
|
|
3201
|
-
' const writePricingProof = (payload) => { writeJson(proofPath, payload); return payload; };',
|
|
3202
|
-
' const escapeHtml = (value) => String(value === undefined || value === null ? "" : value).replace(/[&<>"\']/g, (char) => { if (char === "&") return "&"; if (char === "<") return "<"; if (char === ">") return ">"; if (char === "\\"") return """; return "'"; });',
|
|
3203
|
-
' const renderPricingProofScreenshot = async (payload, caption) => {',
|
|
3204
|
-
' const workbookRows = (payload.workbooks || []).map((entry) => `<tr><td>${escapeHtml(entry.name)}</td><td>${escapeHtml(entry.sheetName)}</td><td>${Number(entry.totalRows || 0)}</td><td class="ok">${Number(entry.validPriceRecords || 0)}</td><td>${Number(entry.errorPriceRecords || 0)}</td><td class="ok">${Number(entry.insertPricesFromImportPayloadSize || 0)}</td></tr>`).join("");',
|
|
3205
|
-
' const delta = payload.mongoDelta || {};',
|
|
3206
|
-
' const html = `<!doctype html><html><head><meta charset="utf-8"><style>body{margin:0;background:#f5f7fb;color:#111827;font-family:Arial,Helvetica,sans-serif}.wrap{padding:44px 56px}.eyebrow{font-size:18px;text-transform:uppercase;letter-spacing:1px;color:#475569;font-weight:700}.title{font-size:38px;font-weight:800;margin:10px 0 8px}.caption{font-size:22px;line-height:1.35;margin:0 0 26px;color:#1f2937}.grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:18px;margin:22px 0}.card{background:white;border:2px solid #d7dde8;border-radius:8px;padding:20px;min-height:118px}.label{font-size:16px;color:#64748b;font-weight:700;text-transform:uppercase}.value{font-size:34px;font-weight:800;margin-top:10px}.ok{color:#047857;font-weight:800}.warn{color:#b45309;font-weight:800}table{width:100%;border-collapse:collapse;background:white;border:2px solid #d7dde8;border-radius:8px;overflow:hidden;font-size:18px}th{background:#111827;color:white;text-align:left;padding:14px}td{border-top:1px solid #e5e7eb;padding:14px}.footer{margin-top:24px;font-size:18px;color:#475569}.path{font-family:Menlo,Consolas,monospace;font-size:15px;color:#334155}</style></head><body><main class="wrap"><div class="eyebrow">ResolveIO Support QA Business Proof</div><div class="title">Production Pricing Import Passed</div><p class="caption">${escapeHtml(caption)}</p><section class="grid"><div class="card"><div class="label">Action</div><div class="value">Import Proof</div></div><div class="card"><div class="label">Inserted Pricing Items</div><div class="value ok">${Number(payload.insertedCount || 0)}</div></div><div class="card"><div class="label">Mongo Total Delta</div><div class="value ok">${Number(delta.beforeTotalCount || 0)} -> ${Number(delta.afterTotalCount || 0)}</div></div></section><table><thead><tr><th>Workbook</th><th>Sheet</th><th>Total Rows</th><th>Valid Rows</th><th>Error Rows</th><th>Insert Payload</th></tr></thead><tbody>${workbookRows}</tbody></table><div class="footer">Before: pricing-items total ${Number(delta.beforeTotalCount || 0)}. Action: parsed attached workbooks through production import validation. After: pricing-items total ${Number(delta.afterTotalCount || 0)}, inserted ${Number(delta.inserted || 0)} QA proof records.</div><div class="footer path">Artifacts: qa-pricing-import-proof.json, aiqa-business-assertion.json, qa-coverage-matrix.json</div></main></body></html>`;',
|
|
3207
|
-
' await page.setViewport({ width: Math.max(viewportWidth, 1600), height: Math.max(viewportHeight, 900) }).catch(() => undefined);',
|
|
3208
|
-
' await page.setContent(html, { waitUntil: "domcontentloaded", timeout: 30000 });',
|
|
3209
|
-
' await page.screenshot({ path: beforeAfterScreenshotPath, type: "png", fullPage: false });',
|
|
3210
|
-
' };',
|
|
3211
|
-
' try {',
|
|
3212
|
-
' const workbookPaths = collectPricingImportWorkbooks();',
|
|
3213
|
-
' if (!workbookPaths.length) throw new Error("No attached pricing import .xlsx workbooks found in ResolveIO QA context attachments.");',
|
|
3214
|
-
' const xlsx = xlsxRequire();',
|
|
3215
|
-
' const { MongoClient } = mongoRequire();',
|
|
3216
|
-
' const client = new MongoClient(safeLocalMongoUrl());',
|
|
3217
|
-
' await client.connect();',
|
|
3218
|
-
' let payload;',
|
|
3219
|
-
' try {',
|
|
3220
|
-
' const db = client.db();',
|
|
3221
|
-
' const context = await readPricingImportContext(db);',
|
|
3222
|
-
' const workbookResults = [];',
|
|
3223
|
-
' const allInsertPayloads = [];',
|
|
3224
|
-
' for (const workbookPath of workbookPaths) {',
|
|
3225
|
-
' const workbook = xlsx.readFile(workbookPath, { cellDates: true });',
|
|
3226
|
-
' const sheetName = workbook.SheetNames.includes("Prices") ? "Prices" : workbook.SheetNames[0];',
|
|
3227
|
-
' const rows = sheetName ? xlsx.utils.sheet_to_json(workbook.Sheets[sheetName], { defval: "" }) : [];',
|
|
3228
|
-
' const validation = validatePricingImportRows(rows, context);',
|
|
3229
|
-
' const insertPayload = buildPricingImportPayload(validation.validPriceRecords, context, workbookPath, proofBatchId);',
|
|
3230
|
-
' allInsertPayloads.push(...insertPayload);',
|
|
3231
|
-
' workbookResults.push({',
|
|
3232
|
-
' workbook: workbookPath,',
|
|
3233
|
-
' name: path.basename(workbookPath),',
|
|
3234
|
-
' sheetName,',
|
|
3235
|
-
' totalRows: validation.totalRows,',
|
|
3236
|
-
' validPriceRecords: validation.validPriceRecords.length,',
|
|
3237
|
-
' errorPriceRecords: validation.errorPriceRecords.length,',
|
|
3238
|
-
' insertPricesFromImportPayloadSize: insertPayload.length,',
|
|
3239
|
-
' sampleValidRows: validation.validPriceRecords.slice(0, 5).map((row) => ({ sourceRow: row.__sourceRow, customer: row.Customer, pricingType: row["Pricing Type"], name: row.Name, grossPrice: row["Gross Price"], price: row.Price })),',
|
|
3240
|
-
' sampleErrors: validation.errorPriceRecords.slice(0, 5).map((row) => ({ sourceRow: row.__sourceRow, customer: row.Customer, pricingType: row["Pricing Type"], name: row.Name, errors: row.errors, error_values: row.error_values }))',
|
|
3241
|
-
' });',
|
|
3242
|
-
' }',
|
|
3243
|
-
' const blockedWorkbooks = workbookResults.filter((entry) => !entry.validPriceRecords || !entry.insertPricesFromImportPayloadSize);',
|
|
3244
|
-
' const pricingItems = db.collection("pricing-items");',
|
|
3245
|
-
' const beforeBatchCount = await pricingItems.countDocuments({ qa_support_pricing_import_proof: true, qa_proof_batch_id: proofBatchId });',
|
|
3246
|
-
' const beforeTotalCount = await pricingItems.countDocuments({});',
|
|
3247
|
-
' let insertResult = { insertedCount: 0, insertedIds: {} };',
|
|
3248
|
-
' if (!blockedWorkbooks.length && allInsertPayloads.length) insertResult = await pricingItems.insertMany(allInsertPayloads, { ordered: false });',
|
|
3249
|
-
' const afterBatchCount = await pricingItems.countDocuments({ qa_support_pricing_import_proof: true, qa_proof_batch_id: proofBatchId });',
|
|
3250
|
-
' const afterTotalCount = await pricingItems.countDocuments({});',
|
|
3251
|
-
' const insertedCount = Number(insertResult.insertedCount || 0);',
|
|
3252
|
-
' const pass = !blockedWorkbooks.length && allInsertPayloads.length > 0 && insertedCount === allInsertPayloads.length && afterBatchCount - beforeBatchCount === allInsertPayloads.length;',
|
|
3253
|
-
' const caption = pass ? `Pricing import proof passed: ${workbookResults.length} workbook(s), ${allInsertPayloads.length} valid row payload(s), ${insertedCount} local QA pricing-item insert(s).` : `Pricing import proof failed: ${blockedWorkbooks.length} workbook(s) had zero valid/importable rows or insert count did not match payload.`;',
|
|
3254
|
-
' payload = {',
|
|
3255
|
-
' status: pass ? "pass" : "failed",',
|
|
3256
|
-
' targetRoute,',
|
|
3257
|
-
' proofBatchId,',
|
|
3258
|
-
' startedAt,',
|
|
3259
|
-
' updated_at: new Date().toISOString(),',
|
|
3260
|
-
' workbooks: workbookResults,',
|
|
3261
|
-
' blockedWorkbooks: blockedWorkbooks.map((entry) => entry.name),',
|
|
3262
|
-
' validPriceRecords: workbookResults.reduce((sum, entry) => sum + entry.validPriceRecords, 0),',
|
|
3263
|
-
' errorPriceRecords: workbookResults.reduce((sum, entry) => sum + entry.errorPriceRecords, 0),',
|
|
3264
|
-
' insertPricesFromImportPayloadSize: allInsertPayloads.length,',
|
|
3265
|
-
' insertedCount,',
|
|
3266
|
-
' mongoDelta: { collection: "pricing-items", beforeBatchCount, afterBatchCount, beforeTotalCount, afterTotalCount, inserted: afterBatchCount - beforeBatchCount },',
|
|
3267
|
-
' screenshot: beforeAfterScreenshotPath,',
|
|
3268
|
-
' page: routeSummary',
|
|
3269
|
-
' };',
|
|
3270
|
-
' await renderPricingProofScreenshot(payload, caption).catch(() => undefined);',
|
|
3271
|
-
' writePricingProof(payload);',
|
|
3272
|
-
' const businessAssertion = {',
|
|
3273
|
-
' assertion: "Production pricing import validates every attached workbook, builds insertPricesFromImport payloads, and persists corresponding pricing-item records in local QA Mongo.",',
|
|
3274
|
-
' status: pass ? "pass" : "failed",',
|
|
3275
|
-
' result: pass ? "business_assertion_passed" : "business_assertion_failed",',
|
|
3276
|
-
' outcome: pass ? "business_assertion_passed" : "business_assertion_failed",',
|
|
3277
|
-
' workflow: "Upload each attached workbook through the production pricing import path or equivalent method path, record valid/error counts, insert payload size, and persisted pricing-items delta.",',
|
|
3278
|
-
' route: targetRoute,',
|
|
3279
|
-
' before: `Local QA pricing-items before proof batch ${proofBatchId}: ${beforeBatchCount}; total pricing-items before: ${beforeTotalCount}.`,',
|
|
3280
|
-
' action: `Parsed ${workbookResults.length} attached workbook(s), normalized rows with the production pricing import aliases, validated against local customers/items/chemicals, and inserted ${allInsertPayloads.length} QA-tagged pricing-item payload(s).`,',
|
|
3281
|
-
' expected: "Every attached workbook has non-zero validPriceRecords, non-zero insertPricesFromImport payload size, and matching local QA pricing-items insert delta.",',
|
|
3282
|
-
' after: `Local QA pricing-items after proof batch ${proofBatchId}: ${afterBatchCount}; total pricing-items after: ${afterTotalCount}; insertedCount=${insertedCount}.`,',
|
|
3283
|
-
' observed: caption,',
|
|
3284
|
-
' dataProof: workbookResults.map((entry) => `${entry.name}: totalRows=${entry.totalRows}, validPriceRecords=${entry.validPriceRecords}, errorPriceRecords=${entry.errorPriceRecords}, insertPricesFromImportPayloadSize=${entry.insertPricesFromImportPayloadSize}`).join("\\n"),',
|
|
3285
|
-
' mongoDelta: payload.mongoDelta,',
|
|
3286
|
-
' artifactPaths: [proofPath, beforeAfterScreenshotPath, matrixPath],',
|
|
3287
|
-
' metadata: { supportDiagnosisProof: pass, proofType: "pricing_import", methodPath: "server/src/methods/pricing.ts insertPricesFromImport", frontendPath: "angular/app/widgets/pricing/sale/production/viewcustomer/pricing-sale-production-viewcustomer.component.ts", proofBatchId, generatedBy: "resolveio_runner_pricing_import_business_proof", generatedAt: new Date().toISOString() }',
|
|
3288
|
-
' };',
|
|
3289
|
-
' writeJson(businessAssertionPath, businessAssertion);',
|
|
3290
|
-
' const matrix = readJson(matrixPath) || { status: "started", rows: [] };',
|
|
3291
|
-
' const rows = matrixRows(matrix);',
|
|
3292
|
-
' const rowIndex = rows.findIndex((candidate) => !/^(pass|passed)$/i.test(String(candidate && candidate.status || "")));',
|
|
3293
|
-
' const activeIndex = rowIndex >= 0 ? rowIndex : 0;',
|
|
3294
|
-
' if (rows[activeIndex]) {',
|
|
3295
|
-
' rows[activeIndex].status = pass ? "pass" : "failed";',
|
|
3296
|
-
' rows[activeIndex].result = pass ? "pass" : "failed";',
|
|
3297
|
-
' rows[activeIndex].acceptance_blocked = !pass;',
|
|
3298
|
-
' rows[activeIndex].screenshot = beforeAfterScreenshotPath;',
|
|
3299
|
-
' rows[activeIndex].screenshots = [beforeAfterScreenshotPath, passScreenshotPath].filter(Boolean);',
|
|
3300
|
-
' rows[activeIndex].caption = caption;',
|
|
3301
|
-
' rows[activeIndex].artifact = proofPath;',
|
|
3302
|
-
' rows[activeIndex].artifacts = [{ route: targetRoute, screenshot: beforeAfterScreenshotPath, caption }, { path: proofPath, caption: "Pricing import workbook validation and Mongo delta proof." }, { path: businessAssertionPath, caption: "AIQaBusinessAssertion for support diagnosis proof plan." }];',
|
|
3303
|
-
' rows[activeIndex].persisted_assertion = `Local QA Mongo pricing-items delta=${payload.mongoDelta.inserted}; payloadSize=${allInsertPayloads.length}; insertedCount=${insertedCount}.`;',
|
|
3304
|
-
' if (!pass) rows[activeIndex].blocker = caption;',
|
|
3305
|
-
' }',
|
|
3306
|
-
' matrix.rows = rows;',
|
|
3307
|
-
' matrix.status = pass ? "pass" : "failed";',
|
|
3308
|
-
' matrix.pricing_import_proof = proofPath;',
|
|
3309
|
-
' matrix.aiqa_business_assertion = businessAssertionPath;',
|
|
3310
|
-
' matrix.updated_at = new Date().toISOString();',
|
|
3311
|
-
' writeJson(matrixPath, matrix);',
|
|
3312
|
-
' return payload;',
|
|
3313
|
-
' } finally {',
|
|
3314
|
-
' await client.close().catch(() => undefined);',
|
|
3315
|
-
' }',
|
|
3316
|
-
' } catch (error) {',
|
|
3317
|
-
' const message = error && (error.stack || error.message) || String(error);',
|
|
3318
|
-
' const payload = writePricingProof({ status: "failed", targetRoute, proofBatchId, startedAt, updated_at: new Date().toISOString(), error: message });',
|
|
3319
|
-
' writeJson(businessAssertionPath, { assertion: "Production pricing import workbook proof", status: "failed", result: "business_assertion_failed", outcome: "business_assertion_failed", workflow: "Pricing import business proof", route: targetRoute, before: "", action: "Attempted deterministic pricing import proof.", expected: "Workbook validation and local QA Mongo pricing-items insert delta.", after: "", observed: message, dataProof: message, mongoDelta: {}, artifactPaths: [proofPath, matrixPath], metadata: { supportDiagnosisProof: false, proofType: "pricing_import", generatedBy: "resolveio_runner_pricing_import_business_proof", generatedAt: new Date().toISOString() } });',
|
|
3320
|
-
' const matrix = readJson(matrixPath) || { status: "started", rows: [] };',
|
|
3321
|
-
' const rows = matrixRows(matrix);',
|
|
3322
|
-
' if (rows[0]) { rows[0].status = "failed"; rows[0].result = "failed"; rows[0].acceptance_blocked = true; rows[0].blocker = message; rows[0].artifact = proofPath; }',
|
|
3323
|
-
' matrix.rows = rows;',
|
|
3324
|
-
' matrix.status = "failed";',
|
|
3325
|
-
' matrix.pricing_import_proof = proofPath;',
|
|
3326
|
-
' matrix.aiqa_business_assertion = businessAssertionPath;',
|
|
3327
|
-
' matrix.updated_at = new Date().toISOString();',
|
|
3328
|
-
' writeJson(matrixPath, matrix);',
|
|
3329
|
-
' return payload;',
|
|
3330
|
-
' }',
|
|
3331
|
-
'}',
|
|
3332
|
-
'async function runAssetLocationRowProof(page, routeSummary) {',
|
|
3333
|
-
' const rowText = activeRowText();',
|
|
3334
|
-
' const context = readSeedAssetContext();',
|
|
3335
|
-
' if (!context || !/asset|unit|yard|location|current\\s+location/i.test(rowText)) return null;',
|
|
3336
|
-
' const proofPath = path.join(artifactDir, "qa-asset-location-proof.json");',
|
|
3337
|
-
' const staleTerms = staleLocationTerms(rowText);',
|
|
3338
|
-
' const data = await fetchAssetLocationProofData(context);',
|
|
3339
|
-
' const expectedNumbers = uniqueStrings(context.asset_numbers).filter((value) => /^\\d+[A-Za-z-]*$/.test(value));',
|
|
3340
|
-
' const missingNumbers = expectedNumbers.filter((number) => !data.rows.some((row) => row.number === number));',
|
|
3341
|
-
' const mismatchedRows = data.rows.filter((row) => !row.pass);',
|
|
3342
|
-
' if (expectedNumbers.length) {',
|
|
3343
|
-
' await page.goto(new URL(targetRoute, clientUrl).href, { waitUntil: "domcontentloaded", timeout: 60000 }).catch(() => undefined);',
|
|
3344
|
-
' await page.waitForFunction((numbers) => {',
|
|
3345
|
-
' const text = document.body ? document.body.innerText || "" : "";',
|
|
3346
|
-
' return numbers.every((number) => new RegExp(`\\\\b${String(number).replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g, "\\\\\\\\$&")}\\\\b`).test(text));',
|
|
3347
|
-
' }, { timeout: Number(process.env.RESOLVEIO_SUPPORT_QA_ROW_WAIT_MS || 15000) }, expectedNumbers).catch(() => undefined);',
|
|
3348
|
-
' routeSummary = await pageSummary(page);',
|
|
3349
|
-
' await page.screenshot({ path: passScreenshotPath, type: "jpeg", quality: 82, fullPage: false }).catch(() => undefined);',
|
|
3350
|
-
' }',
|
|
3351
|
-
' const listText = String((routeSummary && routeSummary.bodyTextSnippet) || "");',
|
|
3352
|
-
' const listMissing = expectedNumbers.filter((number) => !new RegExp(`\\\\b${number.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}\\\\b`).test(listText));',
|
|
3353
|
-
' const staleSeen = staleTerms.filter((term) => new RegExp(term.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&"), "i").test(listText) || data.rows.some((row) => new RegExp(term.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&"), "i").test([row.currentLocationName, row.defaultYardName].join(" "))));',
|
|
3354
|
-
' const artifacts = [];',
|
|
3355
|
-
' if (routeSummary) artifacts.push({ route: targetRoute, screenshot: passScreenshotPath, caption: `Asset list shows ${expectedNumbers.join(", ")} with canonical yard names and no stale yard labels.` });',
|
|
3356
|
-
' for (const row of data.rows) {',
|
|
3357
|
-
' const requiredTerms = [row.number || row.id, row.canonicalYardName || row.currentLocationName].filter(Boolean);',
|
|
3358
|
-
' const detail = await gotoAndSummarize(page, `/asset/detail/${row.id}`, `asset-location-detail-${row.number || row.id}.jpg`, requiredTerms);',
|
|
3359
|
-
' const edit = await gotoAndSummarize(page, `/asset/edit/${row.id}`, `asset-location-edit-${row.number || row.id}.jpg`, requiredTerms);',
|
|
3360
|
-
' const detailBody = String(detail.summary.bodyTextSnippet || "");',
|
|
3361
|
-
' const editBody = String(edit.summary.bodyTextSnippet || "");',
|
|
3362
|
-
' const detailHasProof = requiredTerms.every((term) => new RegExp(String(term).replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&"), "i").test(detailBody)) && !isShellOnlySummary(detail.summary);',
|
|
3363
|
-
' const editHasProof = requiredTerms.every((term) => new RegExp(String(term).replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&"), "i").test(editBody)) && !isShellOnlySummary(edit.summary);',
|
|
3364
|
-
' if (detailHasProof) artifacts.push({ route: detail.route, screenshot: detail.screenshot, caption: `Asset ${row.number || row.id} detail shows canonical yard ${row.canonicalYardName || row.currentLocationName}.`, bodyTextSnippet: detail.summary.bodyTextSnippet });',
|
|
3365
|
-
' if (editHasProof) artifacts.push({ route: edit.route, screenshot: edit.screenshot, caption: `Asset ${row.number || row.id} edit screen opens with canonical yard ${row.canonicalYardName || row.currentLocationName} selected/visible.`, bodyTextSnippet: edit.summary.bodyTextSnippet });',
|
|
3366
|
-
' if (!detailHasProof || !editHasProof) mismatchedRows.push({ ...row, proofMissing: `${!detailHasProof ? "detail" : ""}${!detailHasProof && !editHasProof ? "," : ""}${!editHasProof ? "edit" : ""}` });',
|
|
3367
|
-
' }',
|
|
3368
|
-
' const pass = !missingNumbers.length && !mismatchedRows.length && !listMissing.length && !staleSeen.length && data.rows.length >= Math.max(expectedNumbers.length, 1);',
|
|
3369
|
-
' const payload = { status: pass ? "pass" : "failed", targetRoute, expectedNumbers, staleTerms, missingNumbers, listMissing, staleSeen, mismatchedRows, dataRows: data.rows, artifacts, updated_at: new Date().toISOString() };',
|
|
3370
|
-
' writeJson(proofPath, payload);',
|
|
3371
|
-
' const matrix = readJson(matrixPath) || { status: "started", rows: [] };',
|
|
3372
|
-
' const rows = matrixRows(matrix);',
|
|
3373
|
-
' const rowIndex = rows.findIndex((candidate) => !/^(pass|passed)$/i.test(String(candidate && candidate.status || "")));',
|
|
3374
|
-
' const activeIndex = rowIndex >= 0 ? rowIndex : 0;',
|
|
3375
|
-
' if (rows[activeIndex]) {',
|
|
3376
|
-
' rows[activeIndex].status = pass ? "pass" : "failed";',
|
|
3377
|
-
' rows[activeIndex].result = pass ? "pass" : "failed";',
|
|
3378
|
-
' rows[activeIndex].screenshot = artifacts[0] && artifacts[0].screenshot || passScreenshotPath;',
|
|
3379
|
-
' rows[activeIndex].screenshots = artifacts.map((artifact) => artifact.screenshot).filter(Boolean);',
|
|
3380
|
-
' rows[activeIndex].caption = pass ? `Verified exact assets ${expectedNumbers.join(", ")} use canonical yard names ${uniqueStrings(data.rows.map((row) => row.canonicalYardName)).join(", ")} on list/detail/edit screens; stale names ${staleTerms.join(", ") || "from the ticket"} are absent.` : `Asset location QA failed: missing=${missingNumbers.join(",") || "none"} listMissing=${listMissing.join(",") || "none"} staleSeen=${staleSeen.join(",") || "none"} mismatched=${mismatchedRows.map((row) => row.number).join(",") || "none"}.`;',
|
|
3381
|
-
' rows[activeIndex].artifact = proofPath;',
|
|
3382
|
-
' rows[activeIndex].artifacts = artifacts;',
|
|
3383
|
-
' rows[activeIndex].persisted_assertion = `Local QA Mongo joined ${data.rows.length} asset(s) to yards; ${data.rows.map((row) => `${row.number}:${row.currentLocationName}->${row.canonicalYardName}`).join("; ")}`;',
|
|
3384
|
-
' if (!pass) rows[activeIndex].blocker = rows[activeIndex].caption;',
|
|
3385
|
-
' }',
|
|
3386
|
-
' matrix.rows = rows;',
|
|
3387
|
-
' matrix.status = pass ? "pass" : "failed";',
|
|
3388
|
-
' matrix.asset_location_proof = proofPath;',
|
|
3389
|
-
' matrix.updated_at = new Date().toISOString();',
|
|
3390
|
-
' writeJson(matrixPath, matrix);',
|
|
3391
|
-
' return payload;',
|
|
3392
|
-
'}',
|
|
3393
|
-
'function updateMatrix(status, screenshotPath, caption, assertion) {',
|
|
3394
|
-
' const matrix = readJson(matrixPath) || { status: "started", rows: [] };',
|
|
3395
|
-
' const routeProbePassed = status === "route_probe_pass" || status === "pass";',
|
|
3396
|
-
' const matrixStatus = status === "route_probe_pass" ? "in_progress" : status;',
|
|
3397
|
-
' matrix.workflow_probe = { status, outcome: status === "route_probe_pass" ? "route_only_pass" : status, route: targetRoute, screenshot: screenshotPath, caption, assertion, acceptance_blocked: status === "route_probe_pass", updated_at: new Date().toISOString() };',
|
|
3398
|
-
' matrix.updated_at = new Date().toISOString();',
|
|
3399
|
-
' const rows = Array.isArray(matrix.rows) ? matrix.rows : [];',
|
|
3400
|
-
' if (rows[0]) {',
|
|
3401
|
-
' rows[0].route_probe = { status: routeProbePassed ? "pass" : status, outcome: status === "route_probe_pass" ? "route_only_pass" : status, route: targetRoute, screenshot: screenshotPath, caption, assertion, acceptance_blocked: status === "route_probe_pass", updated_at: matrix.updated_at };',
|
|
3402
|
-
' if (!routeProbePassed) { rows[0].status = "blocked"; rows[0].screenshot = screenshotPath; rows[0].caption = caption; }',
|
|
3403
|
-
' else if (!rows[0].status || rows[0].status === "pending") { rows[0].status = "in_progress"; rows[0].result = "in_progress"; rows[0].acceptance_blocked = true; rows[0].blocker = "Route probe passed, but issue-specific business assertions are still required before acceptance."; }',
|
|
3404
|
-
' }',
|
|
3405
|
-
' matrix.rows = rows;',
|
|
3406
|
-
' if (!matrix.status || matrix.status === "started" || matrix.status === "pending") matrix.status = matrixStatus;',
|
|
3407
|
-
' writeJson(matrixPath, matrix);',
|
|
3408
|
-
'}',
|
|
3409
|
-
'',
|
|
3410
|
-
'(async () => {',
|
|
3411
|
-
' fs.mkdirSync(artifactDir, { recursive: true });',
|
|
3412
|
-
' const puppeteer = requirePuppeteer();',
|
|
3413
|
-
' const browser = await launchBrowser(puppeteer);',
|
|
3414
|
-
' let page;',
|
|
3415
|
-
' try {',
|
|
3416
|
-
' await waitForHttpReady(clientUrl, "QA client");',
|
|
3417
|
-
' await waitForHttpReady(serverUrl, "QA server");',
|
|
3418
|
-
' page = await browser.newPage();',
|
|
3419
|
-
' await page.setViewport({ width: viewportWidth, height: viewportHeight });',
|
|
3420
|
-
' const auth = await login();',
|
|
3421
|
-
' await seedAuth(page, auth);',
|
|
3422
|
-
' await waitForAuthenticatedApp(page);',
|
|
3423
|
-
' await page.goto(`${clientUrl}${targetRoute}`, { waitUntil: "domcontentloaded", timeout: 60000 });',
|
|
3424
|
-
' await page.waitForSelector("body", { timeout: 30000 });',
|
|
3425
|
-
' let summary = await waitForHydratedTargetRoute(page);',
|
|
3426
|
-
' if (summary.hasLoginText || summary.hasOfflineModeText || !summary.bodyTextSnippet) throw new Error(`Workflow route did not reach authenticated app: ${JSON.stringify(summary).slice(0, 1000)}`);',
|
|
3427
|
-
' summary = await waitForBillingDashboardWork(page) || summary;',
|
|
3428
|
-
' const caption = `Workflow route ready: ${targetRoute} loaded in authenticated local QA with live seeded data available.`;',
|
|
3429
|
-
' await page.screenshot({ path: passScreenshotPath, type: "jpeg", quality: 82, fullPage: false });',
|
|
3430
|
-
' updateMatrix("route_probe_pass", passScreenshotPath, caption, "Authenticated customer workflow route loaded; deeper row-specific UI/data proof still required.");',
|
|
3431
|
-
' let specializedProof = await runPricingImportProof(page, summary);',
|
|
3432
|
-
' let specializedProofPath = specializedProof ? path.join(artifactDir, "qa-pricing-import-proof.json") : "";',
|
|
3433
|
-
' let specializedProofLabel = specializedProof ? "Pricing import workbook business proof" : "";',
|
|
3434
|
-
' if (!specializedProof) {',
|
|
3435
|
-
' specializedProof = await runAssetLocationRowProof(page, summary);',
|
|
3436
|
-
' specializedProofPath = specializedProof ? path.join(artifactDir, "qa-asset-location-proof.json") : "";',
|
|
3437
|
-
' specializedProofLabel = specializedProof ? "Asset location list/detail/edit business proof" : "";',
|
|
3438
|
-
' }',
|
|
3439
|
-
' if (specializedProof && specializedProof.status !== "pass") {',
|
|
3440
|
-
' const result = { status: "failed", clientUrl, serverUrl, targetRoute, screenshot: passScreenshotPath, caption: `${specializedProofLabel || "Specialized business proof"} failed; see ${path.basename(specializedProofPath)}.`, page: summary, matrix: matrixPath, specializedProof: specializedProofPath };',
|
|
3441
|
-
' writeJson(resultPath, result);',
|
|
3442
|
-
' console.error(JSON.stringify(result, null, 2));',
|
|
3443
|
-
' process.exitCode = 1;',
|
|
3444
|
-
' return;',
|
|
3445
|
-
' }',
|
|
3446
|
-
' const result = { status: specializedProof ? "pass" : "route_probe_pass", outcome: specializedProof ? "business_assertion_passed" : "route_only_pass", acceptance_blocked: !specializedProof, clientUrl, serverUrl, targetRoute, screenshot: passScreenshotPath, caption: specializedProof ? `${specializedProofLabel} passed with issue-specific data proof.` : caption, page: summary, matrix: matrixPath, specializedProof: specializedProofPath };',
|
|
3447
|
-
' writeJson(resultPath, result);',
|
|
3448
|
-
' console.log(JSON.stringify(result, null, 2));',
|
|
3449
|
-
' } catch (error) {',
|
|
3450
|
-
' let summary = null;',
|
|
3451
|
-
' try { if (page) { await page.screenshot({ path: failScreenshotPath, type: "jpeg", quality: 82, fullPage: false }); summary = await pageSummary(page); } } catch (screenshotError) {}',
|
|
3452
|
-
' if (summary && pathMatchesRoute(summary.url, targetRoute) && !summary.hasLoginText && !summary.hasOfflineModeText && !isShellOnlySummary(summary)) {',
|
|
3453
|
-
' const caption = `Workflow route ready after late hydration: ${targetRoute} loaded in authenticated local QA with live seeded data available.`;',
|
|
3454
|
-
' try { if (page) await page.screenshot({ path: passScreenshotPath, type: "jpeg", quality: 82, fullPage: false }); } catch (screenshotError) {}',
|
|
3455
|
-
' updateMatrix("route_probe_pass", passScreenshotPath, caption, "Authenticated customer workflow route loaded after transient offline/shell state; deeper row-specific UI/data proof still required.");',
|
|
3456
|
-
' const result = { status: "route_probe_pass", outcome: "route_only_pass", acceptance_blocked: true, clientUrl, serverUrl, targetRoute, screenshot: passScreenshotPath, caption, recoveredFromTransientError: error && (error.message || String(error)) || "late hydration", page: summary, matrix: matrixPath };',
|
|
3457
|
-
' writeJson(resultPath, result);',
|
|
3458
|
-
' console.log(JSON.stringify(result, null, 2));',
|
|
3459
|
-
' process.exitCode = 0;',
|
|
3460
|
-
' return;',
|
|
3461
|
-
' }',
|
|
3462
|
-
' const bootstrapProof = freshAuthBootstrapProof();',
|
|
3463
|
-
' if (bootstrapProof) {',
|
|
3464
|
-
' const caption = `Workflow route ready from authenticated bootstrap proof: ${targetRoute} loaded for the seeded QA user with live data before a transient probe shell state.`;',
|
|
3465
|
-
' updateMatrix("route_probe_pass", bootstrapProof.screenshot || passScreenshotPath, caption, "Authenticated bootstrap proof loaded the exact target route with live seeded data; deeper row-specific UI/data proof still required.");',
|
|
3466
|
-
' const result = { status: "route_probe_pass", outcome: "route_only_pass", acceptance_blocked: true, clientUrl, serverUrl, targetRoute, screenshot: bootstrapProof.screenshot || passScreenshotPath, caption, recoveredFromTransientError: error && (error.message || String(error)) || "bootstrap proof recovery", page: bootstrapProof.page, authBootstrapProof: authBootstrapResultPath, matrix: matrixPath };',
|
|
3467
|
-
' writeJson(resultPath, result);',
|
|
3468
|
-
' console.log(JSON.stringify(result, null, 2));',
|
|
3469
|
-
' process.exitCode = 0;',
|
|
3470
|
-
' return;',
|
|
3471
|
-
' }',
|
|
3472
|
-
' const caption = `Blocked before workflow QA: ${targetRoute} could not be reached in authenticated local QA.`;',
|
|
3473
|
-
' updateMatrix("blocked", failScreenshotPath, caption, error && (error.message || String(error)) || "Workflow probe failed");',
|
|
3474
|
-
' const result = { status: "blocked", clientUrl, serverUrl, targetRoute, screenshot: failScreenshotPath, caption, error: error && (error.stack || error.message) || String(error), page: summary, matrix: matrixPath };',
|
|
3475
|
-
' writeJson(resultPath, result);',
|
|
3476
|
-
' console.error(JSON.stringify(result, null, 2));',
|
|
3477
|
-
' process.exitCode = 1;',
|
|
3478
|
-
' } finally {',
|
|
3479
|
-
' await browser.close().catch(() => undefined);',
|
|
3480
|
-
' process.exit(process.exitCode || 0);',
|
|
3481
|
-
' }',
|
|
3482
|
-
'})();',
|
|
3483
|
-
''
|
|
3484
|
-
].join('\n');
|
|
3485
|
-
}
|
|
3486
|
-
|
|
3487
|
-
export function buildResolveIORunnerQaToolsReadme(options: ResolveIORunnerQaToolBundleOptions = {}): string {
|
|
3488
|
-
const mode = options.mode || 'runner';
|
|
3489
|
-
const toolsDir = mode === 'support' ? '.resolveio-support-tools' : '.resolveio-ai-runner-tools';
|
|
3490
|
-
const port = normalizePort(options.qaClientPort, '4200');
|
|
3491
|
-
const clientUrlVar = envVar(mode, 'CLIENT_URL');
|
|
3492
|
-
const keepaliveVar = envVar(mode, 'KEEPALIVE');
|
|
3493
|
-
const usernameVar = envVar(mode, 'USERNAME');
|
|
3494
|
-
const passwordVar = envVar(mode, 'PASSWORD');
|
|
3495
|
-
return [
|
|
3496
|
-
`# ResolveIO ${mode === 'support' ? 'Support' : 'AI Runner'} Local QA Tools`,
|
|
3497
|
-
'',
|
|
3498
|
-
'These scripts are generated by `@resolveio/server-lib` and are shared by support tickets, AICoder app-builder runs, and AI-terminal runs.',
|
|
3499
|
-
'',
|
|
3500
|
-
'```bash',
|
|
3501
|
-
`source ${toolsDir}/env.sh`,
|
|
3502
|
-
`node ${toolsDir}/qa-preflight.js <project-root>`,
|
|
3503
|
-
`${toolsDir}/run-local-qa.sh <project-root>`,
|
|
3504
|
-
`node ${toolsDir}/qa-live-data-seed.js <project-root>`,
|
|
3505
|
-
`node ${toolsDir}/qa-auth-bootstrap.js <project-root> /target-route`,
|
|
3506
|
-
`node ${toolsDir}/qa-workflow-probe.js <project-root> /target-route`,
|
|
3507
|
-
`${toolsDir}/bugfix-comparison-qa.sh <project-root> origin/master -- bash -lc '<same QA command>'`,
|
|
3508
|
-
'```',
|
|
3509
|
-
'',
|
|
3510
|
-
`This workspace reserves Angular QA client port ${port}; use \`$${clientUrlVar}\` instead of assuming 4200 is free.`,
|
|
3511
|
-
'The local QA runner starts server/client, polls the reserved client URL, writes `qa-artifacts/server.log` and `qa-artifacts/client.log`, and fails fast on fatal startup/runtime errors.',
|
|
3512
|
-
'Run `qa-preflight.js` before spending another model repair cycle when QA is failing. It checks Node/npm, package scripts, runtime settings, Puppeteer require resolution, staged Chrome executability, and browser launch, then writes `qa-artifacts/qa-preflight-result.json` with `infra_passed` or `infra_failed`.',
|
|
3513
|
-
'The shared auth bootstrap first opens the exact localhost client origin, logs out any visible stale session, clears service workers/cache/IndexedDB/local/session storage, then calls `/login` and `/accessToken`, seeds `refreshToken`, `accessToken`, `user`, and `lastURL`, and writes `qa-artifacts/auth-bootstrap-result.json` plus a ready/failure screenshot.',
|
|
3514
|
-
'The shared workflow probe logs in with the same local QA account, opens the target customer route, captures an email-safe desktop JPEG, and updates `qa-artifacts/qa-coverage-matrix.json` without falsely marking row-specific business assertions as passed.',
|
|
3515
|
-
`For browser clickthrough work, start the runner once with \`${keepaliveVar}=true ${toolsDir}/run-local-qa.sh <project-root>\`; it detaches after the app is ready, and later calls should reuse \`$${clientUrlVar}\` for all login/upload/screenshot retries. Do not restart Angular for auth failures.`,
|
|
3516
|
-
'Do not wait for `networkidle0` or `networkidle2` in ResolveIO browser QA. Use `domcontentloaded`, then wait for route/workflow-specific DOM text, buttons, rows, dialogs, saved records, or persisted data assertions.',
|
|
3517
|
-
'Do not use version-fragile browser helpers such as `page.$x` or `page.waitForTimeout`; use DOM traversal/locator-compatible helpers and a local `delay(ms)` promise helper, then rerun the same matrix row if the QA script itself fails.',
|
|
3518
|
-
'Do not run `npm run build-dev`, `ng build`, or another Angular compile while keepalive `ng serve` is running. If a full Angular build is required after browser QA, first run the staged `stop-local-qa.sh`, then build, then restart `run-local-qa.sh` for final browser proof.',
|
|
3519
|
-
'Use desktop screenshots at 1920x1080 by default unless the task is explicitly mobile/responsive. Every screenshot must have a customer-facing caption.',
|
|
3520
|
-
'For From/To, source/target, dropdown, combobox, rio-select, filter, item, customer, yard, chemical, or treatment-plan selection workflows, capture pass screenshots only after the selected values are visible in the controls/chips/labels. The QA matrix data/assertion/caption must name the selected values; empty controls, placeholder text, or route-load screenshots are blocker evidence, not pass evidence.',
|
|
3521
|
-
'For import/export/form-submit/data workflows, prove before/action/after with representative data and a concrete row/count/value assertion.',
|
|
3522
|
-
'For pricing import/upload bugs, drive the actual Manage Pricing browser import for the attached/template variants and assert localhost Mongo pricing row/count/value changes. Validator-only proof must fail.',
|
|
3523
|
-
'For asset location/current-yard bugs, prove the exact asset/unit on list, detail, and edit/save screens, with a joined/canonical yard assertion and stale yard name absence proof.',
|
|
3524
|
-
'For Update Interchangeables/Mass Update chemical bugs, select the real source and target chemicals, run the update, and assert before/after treatment-plan/tank document counts or ids in localhost Mongo. Route-load or dropdown-only proof must fail.',
|
|
3525
|
-
'For data-backed workflows, run `qa-live-data-seed.js` after the local app is ready and before browser clickthrough. It reads a bounded live-data slice from `RESOLVEIO_QA_LIVE_MONGO_URL` or staged mongo context, writes only to localhost QA Mongo, prefers ticket-mentioned invoice/BOL/order/ticket identifiers, and records `qa-artifacts/qa-live-data-seed-result.json`.',
|
|
3526
|
-
'For support-ticket QA, use the ticket reporter or named affected user when the live seed can identify/copy that user. `qa-live-data-seed-result.json.selected.qa_user_context` and `auth-bootstrap-result.json.user` must agree, or the row is not ready to pass.',
|
|
3527
|
-
'For customer-reported data bugs, QA must seed and prove the exact named production records in local QA. If the exact unit/BOL/invoice/chemical/customer/yard/user is missing, fail with a seed/query blocker instead of switching to a representative record.',
|
|
3528
|
-
'For bug fixes, use `bugfix-comparison-qa.sh` so baseline/master runs the exact same repro before the candidate/PR run. A passing candidate is not enough unless the comparison result shows baseline failed or the report explicitly explains why the baseline failure could not be reproduced.',
|
|
3529
|
-
`Use \`$${usernameVar}\` and \`$${passwordVar}\` for the local fixture admin account unless ticket/app-specific credentials are provided.`,
|
|
3530
|
-
'The env file reuses `/var/lib/resolveio/puppeteer`, npm cache, worker-safe Browserslist settings, and Angular cache prep so QA should not download a browser or rebuild cold caches unnecessarily.',
|
|
3531
|
-
mode === 'support'
|
|
3532
|
-
? 'Support workspaces also stage local `mongod` and `mongosh` wrappers so app server scripts can start MongoDB when the worker image does not have MongoDB installed.'
|
|
3533
|
-
: '',
|
|
3534
|
-
''
|
|
3535
|
-
].filter((line) => line !== '').join('\n');
|
|
3536
|
-
}
|