@yancyyu/openhermit 1.6.42 → 1.6.43
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/README.md +98 -89
- package/bin/hermit.mjs +96 -0
- package/dist-renderer/assets/{ProjectEditorOverlay-DlFQ6mai.js → ProjectEditorOverlay-C98qSs7-.js} +1 -1
- package/dist-renderer/assets/{TeamGraphOverlay-D2TPMPGR.js → TeamGraphOverlay-CsBbZwcL.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-Cmd0RHLQ.js → _basePickBy-ZOyLWjMK.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-BI_iy8ea.js → _baseUniq-DBb726rt.js} +1 -1
- package/dist-renderer/assets/{arc-NzW2mjTP.js → arc-CdiTaR_R.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-Bzq85AYv.js → architectureDiagram-VXUJARFQ-Cz3sc5TH.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-D1PvYS-b.js → blockDiagram-VD42YOAC-DE4c-KJ3.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-D49RKzPC.js → c4Diagram-YG6GDRKO-CmTMDTrV.js} +1 -1
- package/dist-renderer/assets/channel-KTpqi9eT.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-fmI_MQmQ.js → chunk-4BX2VUAB-rhHy3tFl.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-Xsv9RCXZ.js → chunk-55IACEB6-fLZBzuo_.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-BE1KO8Um.js → chunk-B4BG7PRW-DOzxQhim.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-tqJ7Mv7f.js → chunk-DI55MBZ5-COQCcXC5.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-DMD45MVJ.js → chunk-FMBD7UC4-IKU9U_Y4.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-DOhGrz-q.js → chunk-QN33PNHL-D6WV154X.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-D8yDgJdD.js → chunk-QZHKN3VN-D90_2DQp.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BcsEDu7A.js → chunk-TZMSLE5B-BQEil57G.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-lpzulY5X.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-lpzulY5X.js +1 -0
- package/dist-renderer/assets/clone-CriGymY9.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-DlSqGHMX.js → cose-bilkent-S5V4N54A-6WiK6U2P.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-BTT9tSAx.js → dagre-6UL2VRFP-DF4MMuTn.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-Du-U-mK2.js → diagram-PSM6KHXK-CcF1eZ7E.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-jFdHeKas.js → diagram-QEK2KX5R-DYlOVPQB.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-DKLNK2bu.js → diagram-S2PKOQOG-BHXWsZOP.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-CZxHgIIo.js → erDiagram-Q2GNP2WA-GjmuBx8d.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-v4XStCD0.js → flowDiagram-NV44I4VS-BuS7YVHk.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-DJjD_BEL.js → ganttDiagram-JELNMOA3-3Teu5tAa.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-BNy-jr03.js → gitGraphDiagram-V2S2FVAM-BiLdCYu5.js} +1 -1
- package/dist-renderer/assets/{graph-DDTrn6je.js → graph-CDP_R8ct.js} +1 -1
- package/dist-renderer/assets/{index-BBp78BAu.js → index-BSZdT-g-.js} +1 -1
- package/dist-renderer/assets/{index-eotrJaYy.js → index-BhWvMqsz.js} +1 -1
- package/dist-renderer/assets/{index-D8_B-cfs.js → index-C2_AupSj.js} +1 -1
- package/dist-renderer/assets/{index-BQrwHZ-k.js → index-C5ujiwAR.js} +580 -588
- package/dist-renderer/assets/index-CIS2CTK9.css +1 -0
- package/dist-renderer/assets/{index-CRKQSG9S.js → index-CVNjLwkq.js} +1 -1
- package/dist-renderer/assets/{index-DR6Wz52b.js → index-CwG3se0q.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DqnOsuza.js → infoDiagram-HS3SLOUP-DLHUFo72.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DTobaO1d.js → journeyDiagram-XKPGCS4Q-BE07RpJD.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-HbwVOvWc.js → kanban-definition-3W4ZIXB7-DDHZy4NB.js} +1 -1
- package/dist-renderer/assets/{layout--VYmTcw2.js → layout-5nA5wUxO.js} +1 -1
- package/dist-renderer/assets/{linear-BsJh89Mr.js → linear-BtF1i2qN.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-BZqUZePd.js → mindmap-definition-VGOIOE7T-Z1Ui9Sqy.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-B1q_nH6P.js → pieDiagram-ADFJNKIX-LCjxckWv.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-UD8QhSEu.js → quadrantDiagram-AYHSOK5B-BOwKjSco.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-BA_i7Nw8.js → requirementDiagram-UZGBJVZJ-pChP8Znd.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CMTnX-2d.js → sankeyDiagram-TZEHDZUN-DifZ2qpo.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BQXDB615.js → sequenceDiagram-WL72ISMW-CJg-WYyY.js} +1 -1
- package/dist-renderer/assets/{splashScene-D0YB9uxm.js → splashScene-94xWCzLA.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BAsPXy6X.js → stateDiagram-FKZM4ZOC-DWHOoFdv.js} +1 -1
- package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-CGYZOoMb.js +1 -0
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BdasmVkC.js → timeline-definition-IT6M3QCI-CPgokIo8.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-BkKQqIui.js → treemap-GDKQZRPO-DAVqSR9L.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-EAlPHOdx.js → xychartDiagram-PRI3JC2R-CCOcGbrD.js} +1 -1
- package/dist-renderer/chat-community-qr.jpg +0 -0
- package/dist-renderer/fonts/Agave-Bold.ttf +0 -0
- package/dist-renderer/fonts/Agave-Regular.ttf +0 -0
- package/dist-renderer/icon.png +0 -0
- package/dist-renderer/icon.rar +0 -0
- package/dist-renderer/index.html +3 -3
- package/package.json +21 -26
- package/src/features/worker-society/core/application/WorkerSocietyService.test.ts +802 -0
- package/src/features/worker-society/core/application/WorkerSocietyService.ts +428 -0
- package/src/features/worker-society/core/application/fakes.ts +101 -0
- package/src/features/worker-society/core/application/ports.ts +70 -0
- package/src/features/worker-society/core/domain/models/society.ts +141 -0
- package/src/features/worker-society/core/domain/policies/societyPolicies.test.ts +739 -0
- package/src/features/worker-society/core/domain/policies/societyPolicies.ts +496 -0
- package/src/features/worker-society/main/adapters/input/societyMcp.test.ts +317 -0
- package/src/features/worker-society/main/adapters/input/societyMcp.ts +257 -0
- package/src/features/worker-society/main/adapters/input/societyRoutes.test.ts +695 -0
- package/src/features/worker-society/main/adapters/input/societyRoutes.ts +194 -0
- package/src/features/worker-society/main/composition/societyComposition.test.ts +74 -0
- package/src/features/worker-society/main/composition/societyComposition.ts +70 -0
- package/src/features/worker-society/main/composition/workerSocietyPlugin.test.ts +69 -0
- package/src/features/worker-society/main/composition/workerSocietyPlugin.ts +67 -0
- package/src/features/worker-society/main/infrastructure/crossTeamMessageGateway.test.ts +132 -0
- package/src/features/worker-society/main/infrastructure/crossTeamMessageGateway.ts +84 -0
- package/src/features/worker-society/main/infrastructure/fsStores.test.ts +216 -0
- package/src/features/worker-society/main/infrastructure/fsStores.ts +113 -0
- package/src/features/worker-society/main/infrastructure/mergingProfileStore.test.ts +195 -0
- package/src/features/worker-society/main/infrastructure/mergingProfileStore.ts +96 -0
- package/src/features/worker-society/renderer/SocietyGraph.tsx +166 -0
- package/src/features/worker-society/renderer/SocietyNodeLabels.tsx +139 -0
- package/src/features/worker-society/renderer/SocietyNodeOverlay.tsx +339 -0
- package/src/features/worker-society/renderer/SocietyView.tsx +437 -0
- package/src/features/worker-society/renderer/index.ts +11 -0
- package/src/features/worker-society/renderer/societyApi.test.ts +259 -0
- package/src/features/worker-society/renderer/societyApi.ts +144 -0
- package/src/features/worker-society/renderer/societyGraphAdapter.test.ts +321 -0
- package/src/features/worker-society/renderer/societyGraphAdapter.ts +240 -0
- package/src/features/worker-society/renderer/societyOverlayActions.test.ts +57 -0
- package/src/features/worker-society/renderer/societyOverlayActions.ts +49 -0
- package/src/features/worker-society/renderer/societyStore.test.ts +218 -0
- package/src/features/worker-society/renderer/societyStore.ts +146 -0
- package/src/features/worker-society/renderer/societyViewUtils.test.ts +81 -0
- package/src/features/worker-society/renderer/societyViewUtils.ts +68 -0
- package/src/main/ipc/extensions.ts +27 -0
- package/src/main/server.ts +1709 -534
- package/src/main/services/ccConnect/CcConnectBridge.ts +26 -11
- package/src/main/services/ccConnect/CcConnectClient.ts +9 -2
- package/src/main/services/ccConnect/workDirReconcile.test.ts +57 -0
- package/src/main/services/ccConnect/workDirReconcile.ts +36 -0
- package/src/main/services/direct-cli/DirectCliSessionManager.test.ts +397 -0
- package/src/main/services/direct-cli/DirectCliSessionManager.ts +508 -0
- package/src/main/services/direct-cli/DirectCliSessionStore.test.ts +79 -0
- package/src/main/services/direct-cli/DirectCliSessionStore.ts +97 -0
- package/src/main/services/direct-cli/__tests__/directCliMessageId.test.ts +40 -0
- package/src/main/services/direct-cli/directCliMessageId.ts +21 -0
- package/src/main/services/direct-cli/index.ts +17 -0
- package/src/main/services/extensions/capability-packs/CapabilityPackLoaderService.ts +637 -0
- package/src/main/services/extensions/catalog/PluginCatalogService.ts +2 -2
- package/src/main/services/loop-assets/LoopAssetsScannerService.ts +657 -0
- package/src/main/services/runtime/providerAwareCliEnv.ts +33 -5
- package/src/main/services/session-intelligence/LocalSessionScanner.ts +156 -71
- package/src/main/services/session-intelligence/SessionUsageParser.ts +103 -8
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +11 -0
- package/src/main/services/session-intelligence/__tests__/teamSessionListMapper.test.ts +104 -0
- package/src/main/services/session-intelligence/teamSessionListMapper.ts +78 -0
- package/src/main/services/system-manager/AdminLoopInitializer.ts +95 -0
- package/src/main/services/system-manager/BuiltinWorkflowSeeder.ts +679 -74
- package/src/main/services/system-manager/SystemManagerConfigService.ts +19 -1
- package/src/main/services/system-manager/WorkflowPromptService.ts +58 -5
- package/src/main/services/system-manager/__tests__/AdminLoopInitializer.test.ts +129 -0
- package/src/main/services/system-manager/__tests__/SystemManagerConfigService.test.ts +60 -0
- package/src/main/services/teams-mvp/CollaborationBoardService.ts +2 -0
- package/src/main/services/teams-mvp/OpsRunbookContext.ts +60 -0
- package/src/main/services/teams-mvp/TaskDispatchService.test.ts +305 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +250 -131
- package/src/main/services/teams-mvp/TeamProvisioningService.ts +12 -2
- package/src/main/services/teams-mvp/TeamWorkspaceService.test.ts +207 -0
- package/src/main/services/teams-mvp/TeamWorkspaceService.ts +104 -51
- package/src/main/services/teams-mvp/index.ts +6 -0
- package/src/main/utils/externalPlatformSessionRouting.ts +92 -0
- package/src/main/utils/toolApprovalRules.ts +151 -0
- package/src/renderer/App.tsx +24 -89
- package/src/renderer/api/httpClient.ts +115 -37
- package/src/renderer/api/providers.ts +5 -16
- package/src/renderer/components/chat/CommunityChatView.tsx +81 -0
- package/src/renderer/components/dashboard/DashboardView.tsx +130 -84
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +39 -5
- package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +2 -1
- package/src/renderer/components/extensions/capability-packs/CapabilityPacksPanel.tsx +170 -0
- package/src/renderer/components/layout/PaneContent.tsx +10 -2
- package/src/renderer/components/layout/SortableTab.tsx +4 -0
- package/src/renderer/components/layout/TabBarActions.tsx +13 -16
- package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +4 -135
- package/src/renderer/components/schedules/SchedulesView.tsx +22 -14
- package/src/renderer/components/schedules/calendar/CalendarEventBlock.tsx +7 -6
- package/src/renderer/components/settings/SettingsTabs.tsx +24 -21
- package/src/renderer/components/settings/SettingsView.tsx +22 -13
- package/src/renderer/components/settings/components/SettingRow.tsx +13 -5
- package/src/renderer/components/settings/components/SettingsSectionCard.tsx +53 -0
- package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +10 -6
- package/src/renderer/components/settings/components/SettingsSelect.tsx +12 -9
- package/src/renderer/components/settings/components/SettingsToggle.tsx +6 -5
- package/src/renderer/components/settings/components/index.ts +1 -0
- package/src/renderer/components/settings/sections/AdvancedSection.tsx +78 -59
- package/src/renderer/components/settings/sections/CliStatusSection.tsx +32 -44
- package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +1 -1
- package/src/renderer/components/settings/sections/GeneralSection.tsx +216 -186
- package/src/renderer/components/settings/sections/PlatformsSection.tsx +25 -17
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +63 -22
- package/src/renderer/components/sidebar/SidebarSessions.tsx +120 -80
- package/src/renderer/components/sidebar/SidebarTaskItem.tsx +1 -1
- package/src/renderer/components/splash/splashScene.ts +6 -2
- package/src/renderer/components/system-manager/SystemManagerView.tsx +169 -255
- package/src/renderer/components/tasks/TasksView.tsx +63 -37
- package/src/renderer/components/team/CcSessionsSection.tsx +124 -89
- package/src/renderer/components/team/HarnessBrandLogos.tsx +318 -0
- package/src/renderer/components/team/HarnessSelect.tsx +25 -26
- package/src/renderer/components/team/TeamDetailView.tsx +137 -153
- package/src/renderer/components/team/TeamEmptyState.tsx +9 -37
- package/src/renderer/components/team/TeamListView.tsx +143 -30
- package/src/renderer/components/team/__tests__/CcSessionsSection.hasLocalFile.test.tsx +128 -0
- package/src/renderer/components/team/activity/ActivityItem.tsx +21 -9
- package/src/renderer/components/team/activity/ActivityTimeline.tsx +2 -2
- package/src/renderer/components/team/dialogs/AdvancedCliSection.tsx +1 -1
- package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +13 -10
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +156 -83
- package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +9 -157
- package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +19 -15
- package/src/renderer/components/team/dialogs/PlatformBindingDialog.tsx +48 -10
- package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +11 -12
- package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +39 -37
- package/src/renderer/components/team/dialogs/RuntimeConfigDialog.tsx +434 -64
- package/src/renderer/components/team/dialogs/SendMessageDialog.tsx +12 -10
- package/src/renderer/components/team/dialogs/TaskDetailDialog.tsx +2 -2
- package/src/renderer/components/team/dialogs/__tests__/CreateTeamDialog.bindProject.test.tsx +399 -0
- package/src/renderer/components/team/dialogs/__tests__/CreateTeamDialog.chineseRepro.test.tsx +253 -0
- package/src/renderer/components/team/dialogs/platformAllowUtils.ts +91 -0
- package/src/renderer/components/team/dialogs/teammateRuntimeCompatibility.tsx +1 -1
- package/src/renderer/components/team/kanban/KanbanTaskCard.test.tsx +41 -0
- package/src/renderer/components/team/kanban/KanbanTaskCard.tsx +41 -86
- package/src/renderer/components/team/loop-console/LoopCommandComposer.tsx +310 -0
- package/src/renderer/components/team/loop-console/LoopConsolePanel.tsx +372 -0
- package/src/renderer/components/team/loop-console/loopSendIntent.test.ts +85 -0
- package/src/renderer/components/team/loop-console/loopSendIntent.ts +221 -0
- package/src/renderer/components/team/loop-console/useLeadSessionToolActivity.ts +74 -0
- package/src/renderer/components/team/loop-console/useLoopCommandSuggestions.ts +165 -0
- package/src/renderer/components/team/loop-console/useLoopConsoleController.ts +266 -0
- package/src/renderer/components/team/members/LeadModelRow.test.tsx +1 -1
- package/src/renderer/components/team/members/LeadModelRow.tsx +5 -3
- package/src/renderer/components/team/members/MemberDetailDialog.tsx +11 -0
- package/src/renderer/components/team/members/MemberDetailStats.tsx +13 -3
- package/src/renderer/components/team/members/MemberDraftRow.test.tsx +1 -1
- package/src/renderer/components/team/members/MemberDraftRow.tsx +1 -1
- package/src/renderer/components/team/members/MemberMessagesTab.tsx +2 -2
- package/src/renderer/components/team/members/MemberStatsTab.tsx +1 -1
- package/src/renderer/components/team/messages/MessageComposer.tsx +150 -44
- package/src/renderer/components/team/messages/MessagesFilterPopover.tsx +2 -2
- package/src/renderer/components/team/messages/MessagesPanel.tsx +34 -28
- package/src/renderer/components/team/schedule/CcCronScheduleDialog.tsx +6 -6
- package/src/renderer/components/team/taskLogs/ExactTaskLogCard.tsx +2 -2
- package/src/renderer/components/team/taskLogs/TaskLogStreamSection.tsx +1 -1
- package/src/renderer/components/terminal/TerminalPanel.tsx +2 -3
- package/src/renderer/components/ui/MentionableTextarea.tsx +5 -1
- package/src/renderer/constants/teamColors.ts +5 -5
- package/src/renderer/hooks/useExtensionsTabState.ts +1 -1
- package/src/renderer/hooks/useMentionDetection.ts +5 -1
- package/src/renderer/hooks/useProjectWorkflowCommands.ts +57 -0
- package/src/renderer/hooks/useTeamSuggestions.ts +2 -0
- package/src/renderer/index.css +19 -2
- package/src/renderer/main.tsx +7 -1
- package/src/renderer/store/index.ts +18 -1
- package/src/renderer/store/slices/extensionsSlice.ts +83 -0
- package/src/renderer/store/slices/tabSlice.ts +61 -0
- package/src/renderer/store/slices/teamSlice.ts +138 -9
- package/src/renderer/types/mention.ts +8 -0
- package/src/renderer/types/tabs.ts +3 -1
- package/src/renderer/utils/__tests__/bindProjectSlug.test.ts +69 -0
- package/src/renderer/utils/__tests__/groupTransformer.test.ts +148 -0
- package/src/renderer/utils/__tests__/initialRoute.test.ts +101 -0
- package/src/renderer/utils/__tests__/leadToolActivity.test.ts +124 -0
- package/src/renderer/utils/__tests__/mergeTeamMessages.test.ts +81 -0
- package/src/renderer/utils/__tests__/teamMessageFiltering.test.ts +213 -0
- package/src/renderer/utils/__tests__/teamMessageKey.test.ts +75 -0
- package/src/renderer/utils/__tests__/workflowCommandExecution.test.ts +173 -0
- package/src/renderer/utils/__tests__/workflowCommandSuggestions.test.ts +59 -0
- package/src/renderer/utils/bindProjectSlug.ts +57 -0
- package/src/renderer/utils/capabilityCommandExecution.ts +113 -0
- package/src/renderer/utils/initialRoute.ts +89 -0
- package/src/renderer/utils/leadToolActivity.ts +117 -0
- package/src/renderer/utils/loopShortcutSuggestions.ts +106 -0
- package/src/renderer/utils/mentionSuggestions.ts +1 -1
- package/src/renderer/utils/slashCommandRegistry.ts +231 -0
- package/src/renderer/utils/teamMentionDirective.ts +31 -0
- package/src/renderer/utils/workflowCommandExecution.ts +96 -0
- package/src/renderer/utils/workflowCommandSuggestions.ts +49 -0
- package/src/shared/types/api.ts +79 -4
- package/src/shared/types/ccConnect.ts +1 -0
- package/src/shared/types/extensions/api.ts +19 -0
- package/src/shared/types/extensions/capabilityPack.ts +118 -0
- package/src/shared/types/extensions/index.ts +29 -1
- package/src/shared/types/index.ts +6 -0
- package/src/shared/types/loopAssets.ts +54 -0
- package/src/shared/types/providers.ts +0 -16
- package/src/shared/types/systemManager.ts +26 -1
- package/src/shared/types/team.ts +41 -5
- package/src/shared/types/terminal.ts +2 -36
- package/src/shared/types/worker.test.ts +28 -0
- package/src/shared/types/worker.ts +3 -0
- package/src/shared/utils/__tests__/effortLevels.test.ts +88 -0
- package/src/shared/utils/__tests__/providerBackend.test.ts +88 -0
- package/src/shared/utils/__tests__/providerLaunchArgs.test.ts +220 -0
- package/src/shared/utils/claudeStreamJson.test.ts +187 -0
- package/src/shared/utils/claudeStreamJson.ts +153 -0
- package/src/shared/utils/providerLaunchArgs.ts +217 -0
- package/src/shared/utils/slashCommands.ts +10 -0
- package/src/types/node-pty.d.ts +8 -0
- package/dist-renderer/assets/channel-Ch7JrfUu.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-z9I4AnFy.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-z9I4AnFy.js +0 -1
- package/dist-renderer/assets/clone-Dfi1Jx6l.js +0 -1
- package/dist-renderer/assets/index-iyjkpSus.css +0 -32
- package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-DTUIBfce.js +0 -1
- package/src/main/services/system-manager/SystemManagerPtyService.ts +0 -233
- package/src/renderer/components/common/TerminalPane.tsx +0 -213
- package/src/renderer/components/team/dialogs/useTeamEditForm.ts +0 -292
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
applyReputationDelta,
|
|
5
|
+
autonomousVolunteers,
|
|
6
|
+
canVolunteer,
|
|
7
|
+
capabilityMatchScore,
|
|
8
|
+
classifyOpenNeedStall,
|
|
9
|
+
computeFitScore,
|
|
10
|
+
DEFAULT_FIT_WEIGHTS,
|
|
11
|
+
DEFAULT_REPUTATION,
|
|
12
|
+
discoverWorkers,
|
|
13
|
+
interestOverlap,
|
|
14
|
+
isAtCapacity,
|
|
15
|
+
recordCollaboration,
|
|
16
|
+
requestRevision,
|
|
17
|
+
reputationDeltaForOutcome,
|
|
18
|
+
selectAssignee,
|
|
19
|
+
transitionNeed,
|
|
20
|
+
volunteerFor,
|
|
21
|
+
} from './societyPolicies';
|
|
22
|
+
import type {
|
|
23
|
+
AgentCapability,
|
|
24
|
+
PublishedNeed,
|
|
25
|
+
Relationship,
|
|
26
|
+
WorkerProfile,
|
|
27
|
+
} from '../models/society';
|
|
28
|
+
|
|
29
|
+
const NOW = '2026-06-13T10:00:00.000Z';
|
|
30
|
+
const LATER = '2026-06-13T11:00:00.000Z';
|
|
31
|
+
|
|
32
|
+
function cap(skill: string): AgentCapability {
|
|
33
|
+
return { skill, description: `${skill} capability` };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function profile(overrides: Partial<WorkerProfile> = {}): WorkerProfile {
|
|
37
|
+
return {
|
|
38
|
+
workerId: 'w-a',
|
|
39
|
+
name: 'Worker A',
|
|
40
|
+
kind: 'composite',
|
|
41
|
+
capabilities: [cap('design')],
|
|
42
|
+
interests: [],
|
|
43
|
+
maxConcurrent: 3,
|
|
44
|
+
activeTaskCount: 0,
|
|
45
|
+
reputation: DEFAULT_REPUTATION,
|
|
46
|
+
status: 'online',
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function openNeed(overrides: Partial<PublishedNeed> = {}): PublishedNeed {
|
|
52
|
+
return {
|
|
53
|
+
needId: 'need-1',
|
|
54
|
+
postedBy: 'user',
|
|
55
|
+
subject: 'Design a hero banner',
|
|
56
|
+
requiredCapabilities: ['design'],
|
|
57
|
+
priority: 5,
|
|
58
|
+
status: 'open',
|
|
59
|
+
volunteers: [],
|
|
60
|
+
createdAt: NOW,
|
|
61
|
+
revisionCount: 0,
|
|
62
|
+
...overrides,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── 能力 / 兴趣 ─────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
describe('capabilityMatchScore', () => {
|
|
69
|
+
it('returns 1 when no capabilities are required', () => {
|
|
70
|
+
expect(capabilityMatchScore([], profile())).toBe(1);
|
|
71
|
+
});
|
|
72
|
+
it('returns 1 when worker has all required skills', () => {
|
|
73
|
+
const w = profile({ capabilities: [cap('design'), cap('frontend')] });
|
|
74
|
+
expect(capabilityMatchScore(['design', 'frontend'], w)).toBe(1);
|
|
75
|
+
});
|
|
76
|
+
it('returns fractional coverage for partial match', () => {
|
|
77
|
+
const w = profile({ capabilities: [cap('design')] });
|
|
78
|
+
expect(capabilityMatchScore(['design', 'frontend', 'backend'], w)).toBeCloseTo(1 / 3);
|
|
79
|
+
});
|
|
80
|
+
it('returns 0 when worker has none of the required skills', () => {
|
|
81
|
+
const w = profile({ capabilities: [cap('devops')] });
|
|
82
|
+
expect(capabilityMatchScore(['design'], w)).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
it('is case-insensitive', () => {
|
|
85
|
+
const w = profile({ capabilities: [cap('Design')] });
|
|
86
|
+
expect(capabilityMatchScore(['DESIGN'], w)).toBe(1);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('interestOverlap', () => {
|
|
91
|
+
it('returns 0 when nothing required', () => {
|
|
92
|
+
expect(interestOverlap([], ['design'])).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
it('returns fraction of required skills the worker is interested in', () => {
|
|
95
|
+
expect(interestOverlap(['design', 'frontend'], ['design'])).toBe(0.5);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ── 容量 / 自荐门槛 ────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
describe('isAtCapacity', () => {
|
|
102
|
+
it('false when below max', () => {
|
|
103
|
+
expect(isAtCapacity(profile({ maxConcurrent: 3, activeTaskCount: 2 }))).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
it('true at max', () => {
|
|
106
|
+
expect(isAtCapacity(profile({ maxConcurrent: 3, activeTaskCount: 3 }))).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
it('true above max', () => {
|
|
109
|
+
expect(isAtCapacity(profile({ maxConcurrent: 1, activeTaskCount: 5 }))).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('canVolunteer', () => {
|
|
114
|
+
it('true for open need, capable, idle worker', () => {
|
|
115
|
+
expect(canVolunteer(openNeed(), profile())).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
it('false when need is not open', () => {
|
|
118
|
+
expect(canVolunteer(openNeed({ status: 'assigned' }), profile())).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
it('false when worker is at capacity', () => {
|
|
121
|
+
expect(canVolunteer(openNeed(), profile({ activeTaskCount: 3, maxConcurrent: 3 }))).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
it('false when worker is the poster (no self-assign)', () => {
|
|
124
|
+
expect(canVolunteer(openNeed({ postedBy: 'w-a' }), profile())).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
it('false when worker lacks all required capabilities', () => {
|
|
127
|
+
expect(canVolunteer(openNeed({ requiredCapabilities: ['backend'] }), profile())).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── 适配度 ──────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
describe('computeFitScore', () => {
|
|
134
|
+
it('weights sum to 1', () => {
|
|
135
|
+
const sum = Object.values(DEFAULT_FIT_WEIGHTS).reduce((a, b) => a + b, 0);
|
|
136
|
+
expect(sum).toBeCloseTo(1, 5);
|
|
137
|
+
});
|
|
138
|
+
it('idle capable worker scores higher than loaded one', () => {
|
|
139
|
+
const idle = profile({ activeTaskCount: 0 });
|
|
140
|
+
const loaded = profile({ workerId: 'w-b', activeTaskCount: 3 });
|
|
141
|
+
const need = openNeed();
|
|
142
|
+
expect(computeFitScore(need, idle).score).toBeGreaterThan(computeFitScore(need, loaded).score);
|
|
143
|
+
});
|
|
144
|
+
it('higher reputation yields higher score', () => {
|
|
145
|
+
const lo = profile({ reputation: 10 });
|
|
146
|
+
const hi = profile({ workerId: 'w-hi', reputation: 95 });
|
|
147
|
+
const need = openNeed();
|
|
148
|
+
expect(computeFitScore(need, hi).score).toBeGreaterThan(computeFitScore(need, lo).score);
|
|
149
|
+
});
|
|
150
|
+
it('relationship bonus increases score toward the poster', () => {
|
|
151
|
+
const rels: Relationship[] = [
|
|
152
|
+
{
|
|
153
|
+
fromWorker: 'w-a',
|
|
154
|
+
toWorker: 'user',
|
|
155
|
+
collaborations: 4,
|
|
156
|
+
successes: 4,
|
|
157
|
+
trust: 1,
|
|
158
|
+
lastInteractedAt: NOW,
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
const withRel = computeFitScore(openNeed({ postedBy: 'user' }), profile(), rels);
|
|
162
|
+
const noRel = computeFitScore(openNeed({ postedBy: 'user' }), profile(), []);
|
|
163
|
+
expect(withRel.score).toBeGreaterThan(noRel.score);
|
|
164
|
+
expect(withRel.relationshipBonus).toBe(1);
|
|
165
|
+
});
|
|
166
|
+
it('interest overlap adds to the score', () => {
|
|
167
|
+
const bored = profile();
|
|
168
|
+
const keen = profile({ workerId: 'w-keen', interests: ['design'] });
|
|
169
|
+
const need = openNeed();
|
|
170
|
+
expect(computeFitScore(need, keen).score).toBeGreaterThan(computeFitScore(need, bored).score);
|
|
171
|
+
});
|
|
172
|
+
it('breakdown factors are in [0,1] and score in [0,1]', () => {
|
|
173
|
+
const b = computeFitScore(openNeed(), profile());
|
|
174
|
+
for (const f of [
|
|
175
|
+
b.capability,
|
|
176
|
+
b.loadFairness,
|
|
177
|
+
b.reputation,
|
|
178
|
+
b.relationshipBonus,
|
|
179
|
+
b.interest,
|
|
180
|
+
b.score,
|
|
181
|
+
]) {
|
|
182
|
+
expect(f).toBeGreaterThanOrEqual(0);
|
|
183
|
+
expect(f).toBeLessThanOrEqual(1);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
it('maxConcurrent <= 0 → loadFairness=0(除零守卫;公开纯函数的真实边界)', () => {
|
|
187
|
+
// L101-102 `worker.maxConcurrent > 0 ? Math.max(0, 1 - active/max) : 0` 的 **: 0 臂**。computeFitScore
|
|
188
|
+
// 是导出的纯函数(WorkerSocietyService L426 为外部调用方/前端预估暴露便捷包装),契约 = 任意
|
|
189
|
+
// WorkerProfile → 合法 FitBreakdown、不崩。外部调用方可传 maxConcurrent<=0(未经 registerProfile
|
|
190
|
+
// 的 Math.max(1,…) 夹取),故此分支是真实可达边界,**非** TS-不可达防御。无守卫则 `1 - 0/0` = NaN
|
|
191
|
+
// → loadFairness/score 被 NaN 污染。registerProfile 的夹取测(service 侧)与本测(纯函数侧)各锁一处。
|
|
192
|
+
const zero = computeFitScore(openNeed(), profile({ maxConcurrent: 0, activeTaskCount: 0 }));
|
|
193
|
+
expect(zero.loadFairness).toBe(0); // 0/0 被守卫兜底为 0(非 NaN)
|
|
194
|
+
expect(Number.isFinite(zero.score)).toBe(true); // score 不被 NaN 污染
|
|
195
|
+
expect(zero.score).toBeGreaterThanOrEqual(0);
|
|
196
|
+
expect(zero.score).toBeLessThanOrEqual(1);
|
|
197
|
+
|
|
198
|
+
const neg = computeFitScore(openNeed(), profile({ maxConcurrent: -3, activeTaskCount: 2 }));
|
|
199
|
+
expect(neg.loadFairness).toBe(0); // 负 maxConcurrent 同样兜底 0(不误用脏值)
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ── 选择执行者 ─────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
describe('selectAssignee', () => {
|
|
206
|
+
function workers(...list: WorkerProfile[]): Map<string, WorkerProfile> {
|
|
207
|
+
return new Map(list.map((w) => [w.workerId, w]));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
it('returns null when no volunteers', () => {
|
|
211
|
+
expect(selectAssignee(openNeed(), workers(profile()))).toBeNull();
|
|
212
|
+
});
|
|
213
|
+
it('returns null when the only volunteer is at capacity', () => {
|
|
214
|
+
const w = profile({ activeTaskCount: 3, maxConcurrent: 3 });
|
|
215
|
+
const need = openNeed({
|
|
216
|
+
volunteers: [{ workerId: 'w-a', needId: 'need-1', fitScore: 0.9, volunteeredAt: NOW }],
|
|
217
|
+
});
|
|
218
|
+
expect(selectAssignee(need, workers(w))).toBeNull();
|
|
219
|
+
});
|
|
220
|
+
it('picks the volunteer with the highest fit score', () => {
|
|
221
|
+
const lo = profile({ workerId: 'w-lo', reputation: 10 });
|
|
222
|
+
const hi = profile({ workerId: 'w-hi', reputation: 95 });
|
|
223
|
+
const need = openNeed({
|
|
224
|
+
volunteers: [
|
|
225
|
+
{ workerId: 'w-lo', needId: 'need-1', fitScore: 0.3, volunteeredAt: NOW },
|
|
226
|
+
{ workerId: 'w-hi', needId: 'need-1', fitScore: 0.8, volunteeredAt: NOW },
|
|
227
|
+
],
|
|
228
|
+
});
|
|
229
|
+
expect(selectAssignee(need, workers(lo, hi))?.workerId).toBe('w-hi');
|
|
230
|
+
});
|
|
231
|
+
it('breaks a genuine recomputed-score tie (identical workers) by workerId — stable order', () => {
|
|
232
|
+
// 决胜链最深一层 L157:fitScore 等 → 声誉等 → 负载等 → workerId 字典序。
|
|
233
|
+
// selectAssignee 用 computeFitScore **重算** score(完全忽略 volunteer.fitScore 存值),
|
|
234
|
+
// 故真正「同分」要求 computeFitScore 输入全等;两份 profile 仅 workerId 不同 → 重算 score 逐位相等
|
|
235
|
+
// → L152(false)→L153-154→L155(false 声誉等)→L156(false 负载等)→L157 workerId 决定。
|
|
236
|
+
// 存值 fitScore 0.9/0.1 是故意干扰项:证明走重算而非读存值(旧测就是误以为读存值才失效)。
|
|
237
|
+
const zeta = profile({ workerId: 'w-zeta' });
|
|
238
|
+
const alpha = profile({ workerId: 'w-alpha' });
|
|
239
|
+
const need = openNeed({
|
|
240
|
+
volunteers: [
|
|
241
|
+
{ workerId: 'w-zeta', needId: 'need-1', fitScore: 0.9, volunteeredAt: NOW },
|
|
242
|
+
{ workerId: 'w-alpha', needId: 'need-1', fitScore: 0.1, volunteeredAt: NOW },
|
|
243
|
+
],
|
|
244
|
+
});
|
|
245
|
+
expect(selectAssignee(need, workers(zeta, alpha))?.workerId).toBe('w-alpha');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('at equal score, breaks the tie by higher reputation', () => {
|
|
249
|
+
// 文档契约「同分按声誉高者」。难点:声誉(0.2)本身进 score,默认不同声誉必不同分 →
|
|
250
|
+
// L155 真分支默认不可达。解耦(动态规划:把维度从 score 摘出):传 reputation 权重=0 的 weights,
|
|
251
|
+
// 声誉不进 score,两个仅声誉不同的 worker 重算严格相等 → L152(false)→L155(真,声誉高者胜)。
|
|
252
|
+
const hi = profile({ workerId: 'w-hi', reputation: 90 });
|
|
253
|
+
const lo = profile({ workerId: 'w-lo', reputation: 40 });
|
|
254
|
+
const need = openNeed({
|
|
255
|
+
volunteers: [
|
|
256
|
+
{ workerId: 'w-hi', needId: 'need-1', fitScore: 0.5, volunteeredAt: NOW },
|
|
257
|
+
{ workerId: 'w-lo', needId: 'need-1', fitScore: 0.5, volunteeredAt: NOW },
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
const zeroedReputation = { ...DEFAULT_FIT_WEIGHTS, reputation: 0 };
|
|
261
|
+
expect(selectAssignee(need, workers(hi, lo), [], zeroedReputation)?.workerId).toBe('w-hi');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('at equal score and reputation, breaks the tie by lower load', () => {
|
|
265
|
+
// 文档契约「再同分按负载低者」。同理 loadFairness(0.2)本身进 score,默认不同负载必不同分;
|
|
266
|
+
// 传 loadFairness 权重=0 的 weights 把负载摘出 score → 两 worker 仅负载不同、重算相等 →
|
|
267
|
+
// L152(false)→L155(假 声誉等)→L156(真,activeTaskCount 小者=负载低者胜)。
|
|
268
|
+
const idle = profile({ workerId: 'w-idle', activeTaskCount: 0 });
|
|
269
|
+
const busy = profile({ workerId: 'w-busy', activeTaskCount: 2 }); // maxConcurrent 默认 3 → 未超载,仍可选
|
|
270
|
+
const need = openNeed({
|
|
271
|
+
volunteers: [
|
|
272
|
+
{ workerId: 'w-idle', needId: 'need-1', fitScore: 0.5, volunteeredAt: NOW },
|
|
273
|
+
{ workerId: 'w-busy', needId: 'need-1', fitScore: 0.5, volunteeredAt: NOW },
|
|
274
|
+
],
|
|
275
|
+
});
|
|
276
|
+
const zeroedLoad = { ...DEFAULT_FIT_WEIGHTS, loadFairness: 0 };
|
|
277
|
+
expect(selectAssignee(need, workers(idle, busy), [], zeroedLoad)?.workerId).toBe('w-idle');
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// ── 发现 ────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
describe('discoverWorkers', () => {
|
|
284
|
+
const designer = profile({ workerId: 'designer', capabilities: [cap('design')], reputation: 70 });
|
|
285
|
+
const dev = profile({
|
|
286
|
+
workerId: 'dev',
|
|
287
|
+
capabilities: [cap('backend')],
|
|
288
|
+
reputation: 90,
|
|
289
|
+
status: 'online',
|
|
290
|
+
});
|
|
291
|
+
const offline = profile({
|
|
292
|
+
workerId: 'ghost',
|
|
293
|
+
capabilities: [cap('design')],
|
|
294
|
+
reputation: 99,
|
|
295
|
+
status: 'offline',
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('filters by capability (OR semantics)', () => {
|
|
299
|
+
const res = discoverWorkers([designer, dev], { anyCapability: ['design'] });
|
|
300
|
+
expect(res.map((w) => w.workerId)).toEqual(['designer']);
|
|
301
|
+
});
|
|
302
|
+
it('excludes offline workers by default', () => {
|
|
303
|
+
const res = discoverWorkers([designer, offline], { anyCapability: ['design'] });
|
|
304
|
+
expect(res.map((w) => w.workerId)).toEqual(['designer']);
|
|
305
|
+
});
|
|
306
|
+
it('includes offline when onlineOnly=false', () => {
|
|
307
|
+
const res = discoverWorkers([designer, offline], {
|
|
308
|
+
anyCapability: ['design'],
|
|
309
|
+
onlineOnly: false,
|
|
310
|
+
});
|
|
311
|
+
expect(res.map((w) => w.workerId)).toContain('ghost');
|
|
312
|
+
});
|
|
313
|
+
it('ranks by reputation desc then load asc', () => {
|
|
314
|
+
const busyHi = profile({
|
|
315
|
+
workerId: 'busy',
|
|
316
|
+
capabilities: [cap('design')],
|
|
317
|
+
reputation: 80,
|
|
318
|
+
activeTaskCount: 3,
|
|
319
|
+
});
|
|
320
|
+
const idleMid = profile({
|
|
321
|
+
workerId: 'idle',
|
|
322
|
+
capabilities: [cap('design')],
|
|
323
|
+
reputation: 80,
|
|
324
|
+
activeTaskCount: 0,
|
|
325
|
+
});
|
|
326
|
+
const res = discoverWorkers([busyHi, idleMid], { anyCapability: ['design'] });
|
|
327
|
+
expect(res.map((w) => w.workerId)).toEqual(['idle', 'busy']);
|
|
328
|
+
});
|
|
329
|
+
it('breaks a full reputation+load tie by workerId — stable deterministic order', () => {
|
|
330
|
+
// L270 第三级决胜:声誉等 + 负载等 → workerId 字典序(稳定)。discoverWorkers 直接按 profile
|
|
331
|
+
// 字段排序(无 score 重算、无权重),故两份仅 workerId 不同的全等 worker 必触达 L270——
|
|
332
|
+
// L268(假 声誉等)→L269(假 负载等)→L270 workerId 决定。
|
|
333
|
+
const zeta = profile({
|
|
334
|
+
workerId: 'w-zeta',
|
|
335
|
+
capabilities: [cap('design')],
|
|
336
|
+
reputation: 80,
|
|
337
|
+
activeTaskCount: 1,
|
|
338
|
+
});
|
|
339
|
+
const alpha = profile({
|
|
340
|
+
workerId: 'w-alpha',
|
|
341
|
+
capabilities: [cap('design')],
|
|
342
|
+
reputation: 80,
|
|
343
|
+
activeTaskCount: 1,
|
|
344
|
+
});
|
|
345
|
+
const res = discoverWorkers([zeta, alpha], { anyCapability: ['design'] });
|
|
346
|
+
expect(res.map((w) => w.workerId)).toEqual(['w-alpha', 'w-zeta']);
|
|
347
|
+
});
|
|
348
|
+
it('respects limit', () => {
|
|
349
|
+
const res = discoverWorkers([designer, dev], { limit: 1 });
|
|
350
|
+
expect(res).toHaveLength(1);
|
|
351
|
+
});
|
|
352
|
+
it('empty capability query returns all online workers', () => {
|
|
353
|
+
const res = discoverWorkers([designer, dev, offline]);
|
|
354
|
+
expect(res.map((w) => w.workerId).sort()).toEqual(['designer', 'dev']);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// ── 自荐守卫 ───────────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
describe('volunteerFor', () => {
|
|
361
|
+
it('appends a volunteer with a snapshot fitScore', () => {
|
|
362
|
+
const out = volunteerFor(openNeed(), profile(), NOW);
|
|
363
|
+
expect(out.ok).toBe(true);
|
|
364
|
+
expect(out.volunteer?.workerId).toBe('w-a');
|
|
365
|
+
expect(out.volunteer?.fitScore).toBeGreaterThan(0);
|
|
366
|
+
expect(out.need.volunteers).toHaveLength(1);
|
|
367
|
+
expect(out.need.volunteers[0].volunteeredAt).toBe(NOW);
|
|
368
|
+
});
|
|
369
|
+
it('does not mutate the original need', () => {
|
|
370
|
+
const need = openNeed();
|
|
371
|
+
volunteerFor(need, profile(), NOW);
|
|
372
|
+
expect(need.volunteers).toHaveLength(0);
|
|
373
|
+
});
|
|
374
|
+
it('rejects self-assign', () => {
|
|
375
|
+
const out = volunteerFor(openNeed({ postedBy: 'w-a' }), profile(), NOW);
|
|
376
|
+
expect(out.ok).toBe(false);
|
|
377
|
+
expect(out.reason).toBe('self_assign');
|
|
378
|
+
});
|
|
379
|
+
it('rejects at-capacity worker', () => {
|
|
380
|
+
const out = volunteerFor(openNeed(), profile({ activeTaskCount: 3, maxConcurrent: 3 }), NOW);
|
|
381
|
+
expect(out.ok).toBe(false);
|
|
382
|
+
expect(out.reason).toBe('at_capacity');
|
|
383
|
+
});
|
|
384
|
+
it('rejects duplicate volunteer', () => {
|
|
385
|
+
const need = openNeed({
|
|
386
|
+
volunteers: [{ workerId: 'w-a', needId: 'need-1', fitScore: 0.5, volunteeredAt: NOW }],
|
|
387
|
+
});
|
|
388
|
+
const out = volunteerFor(need, profile(), NOW);
|
|
389
|
+
expect(out.ok).toBe(false);
|
|
390
|
+
expect(out.reason).toBe('already_volunteered');
|
|
391
|
+
});
|
|
392
|
+
it('rejects when worker has no matching capability', () => {
|
|
393
|
+
const out = volunteerFor(openNeed({ requiredCapabilities: ['backend'] }), profile(), NOW);
|
|
394
|
+
expect(out.ok).toBe(false);
|
|
395
|
+
expect(out.reason).toBe('no_capability');
|
|
396
|
+
});
|
|
397
|
+
it('rejects when need is not open', () => {
|
|
398
|
+
const out = volunteerFor(openNeed({ status: 'assigned' }), profile(), NOW);
|
|
399
|
+
expect(out.ok).toBe(false);
|
|
400
|
+
expect(out.reason).toBe('not_open');
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// ── Need 状态机 ────────────────────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
describe('transitionNeed', () => {
|
|
407
|
+
it('open -> assigned sets assignee and assignedAt', () => {
|
|
408
|
+
const out = transitionNeed(openNeed(), 'assigned', NOW, { assignee: 'w-a' });
|
|
409
|
+
expect(out.ok).toBe(true);
|
|
410
|
+
expect(out.need.assignee).toBe('w-a');
|
|
411
|
+
expect(out.need.assignedAt).toBe(NOW);
|
|
412
|
+
});
|
|
413
|
+
it('assigned requires an assignee', () => {
|
|
414
|
+
const out = transitionNeed(openNeed(), 'assigned', NOW);
|
|
415
|
+
expect(out.ok).toBe(false);
|
|
416
|
+
expect(out.reason).toBe('missing_assignee');
|
|
417
|
+
});
|
|
418
|
+
it('assigned -> in_progress sets startedAt', () => {
|
|
419
|
+
const started = transitionNeed(
|
|
420
|
+
openNeed({ status: 'assigned', assignee: 'w-a' }),
|
|
421
|
+
'in_progress',
|
|
422
|
+
NOW
|
|
423
|
+
);
|
|
424
|
+
expect(started.ok).toBe(true);
|
|
425
|
+
expect(started.need.startedAt).toBe(NOW);
|
|
426
|
+
});
|
|
427
|
+
it('in_progress -> delivered captures result and deliveredAt', () => {
|
|
428
|
+
const ip = openNeed({ status: 'in_progress', assignee: 'w-a' });
|
|
429
|
+
const out = transitionNeed(ip, 'delivered', LATER, { result: 'banner v1' });
|
|
430
|
+
expect(out.ok).toBe(true);
|
|
431
|
+
expect(out.need.result).toBe('banner v1');
|
|
432
|
+
expect(out.need.deliveredAt).toBe(LATER);
|
|
433
|
+
});
|
|
434
|
+
it('delivered without a new result patch preserves the prior result', () => {
|
|
435
|
+
// L370 的 `patch.result ?? updated.result` 降级臂:转 delivered 不传 patch.result 时
|
|
436
|
+
// (如「退回重做 in_progress → 重新交付」不改结果的场景),patch 默认 {} → patch.result
|
|
437
|
+
// undefined → 退回 updated.result(即 need 既有的 result),保留而非清空。
|
|
438
|
+
// 既有 delivered 用例恒传 { result }(真臂),本测补降级臂。
|
|
439
|
+
const ip = openNeed({ status: 'in_progress', assignee: 'w-a', result: 'banner v1' });
|
|
440
|
+
const out = transitionNeed(ip, 'delivered', LATER); // 无 patch → 走 ?? 降级臂
|
|
441
|
+
expect(out.ok).toBe(true);
|
|
442
|
+
expect(out.need.result).toBe('banner v1'); // 既有 result 被保留(非 undefined)
|
|
443
|
+
expect(out.need.deliveredAt).toBe(LATER);
|
|
444
|
+
});
|
|
445
|
+
it('delivered -> closed sets closedAt', () => {
|
|
446
|
+
const del = openNeed({ status: 'delivered', result: 'done' });
|
|
447
|
+
const out = transitionNeed(del, 'closed', LATER);
|
|
448
|
+
expect(out.ok).toBe(true);
|
|
449
|
+
expect(out.need.closedAt).toBe(LATER);
|
|
450
|
+
expect(out.need.status).toBe('closed');
|
|
451
|
+
});
|
|
452
|
+
it('rejects illegal transitions and leaves need unchanged', () => {
|
|
453
|
+
const need = openNeed({ status: 'delivered' });
|
|
454
|
+
const out = transitionNeed(need, 'assigned', NOW);
|
|
455
|
+
expect(out.ok).toBe(false);
|
|
456
|
+
expect(out.need).toBe(need);
|
|
457
|
+
expect(out.reason).toContain('illegal');
|
|
458
|
+
});
|
|
459
|
+
it('rejects open -> delivered (must go through assigned/in_progress)', () => {
|
|
460
|
+
expect(transitionNeed(openNeed(), 'delivered', NOW).ok).toBe(false);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
describe('requestRevision', () => {
|
|
465
|
+
it('moves delivered -> in_progress and bumps revisionCount', () => {
|
|
466
|
+
const out = requestRevision(openNeed({ status: 'delivered' }), NOW);
|
|
467
|
+
expect(out.ok).toBe(true);
|
|
468
|
+
expect(out.need.status).toBe('in_progress');
|
|
469
|
+
expect(out.need.revisionCount).toBe(1);
|
|
470
|
+
});
|
|
471
|
+
it('flags when revision limit exceeded', () => {
|
|
472
|
+
const out = requestRevision(openNeed({ status: 'delivered', revisionCount: 3 }), NOW);
|
|
473
|
+
expect(out.ok).toBe(true);
|
|
474
|
+
expect(out.reason).toBe('revision_limit_exceeded');
|
|
475
|
+
});
|
|
476
|
+
it('rejects revision from non-delivered state', () => {
|
|
477
|
+
expect(requestRevision(openNeed({ status: 'open' }), NOW).ok).toBe(false);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// ── 关系 ────────────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
describe('recordCollaboration', () => {
|
|
484
|
+
it('creates a new directed relationship on first collaboration', () => {
|
|
485
|
+
const rels = recordCollaboration([], 'w-a', 'w-b', true, NOW);
|
|
486
|
+
expect(rels).toHaveLength(1);
|
|
487
|
+
expect(rels[0]).toMatchObject({
|
|
488
|
+
fromWorker: 'w-a',
|
|
489
|
+
toWorker: 'w-b',
|
|
490
|
+
collaborations: 1,
|
|
491
|
+
successes: 1,
|
|
492
|
+
trust: 1,
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
it('accumulates and recomputes trust', () => {
|
|
496
|
+
let rels = recordCollaboration([], 'w-a', 'w-b', true, NOW);
|
|
497
|
+
rels = recordCollaboration(rels, 'w-a', 'w-b', false, LATER);
|
|
498
|
+
expect(rels[0].collaborations).toBe(2);
|
|
499
|
+
expect(rels[0].successes).toBe(1);
|
|
500
|
+
expect(rels[0].trust).toBe(0.5);
|
|
501
|
+
});
|
|
502
|
+
it('does not mutate original array', () => {
|
|
503
|
+
const orig = recordCollaboration([], 'w-a', 'w-b', true, NOW);
|
|
504
|
+
const next = recordCollaboration(orig, 'w-a', 'w-b', true, LATER);
|
|
505
|
+
expect(orig[0].collaborations).toBe(1);
|
|
506
|
+
expect(next[0].collaborations).toBe(2);
|
|
507
|
+
});
|
|
508
|
+
it('keeps a->b and b->a independent (directional)', () => {
|
|
509
|
+
let rels = recordCollaboration([], 'w-a', 'w-b', true, NOW);
|
|
510
|
+
rels = recordCollaboration(rels, 'w-b', 'w-a', false, NOW);
|
|
511
|
+
expect(rels).toHaveLength(2);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// ── 声誉 ────────────────────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
describe('reputation', () => {
|
|
518
|
+
it('applyReputationDelta clamps to [0,100]', () => {
|
|
519
|
+
expect(applyReputationDelta(profile({ reputation: 99 }), 10).reputation).toBe(100);
|
|
520
|
+
expect(applyReputationDelta(profile({ reputation: 1 }), -10).reputation).toBe(0);
|
|
521
|
+
expect(applyReputationDelta(profile({ reputation: 50 }), 5).reputation).toBe(55);
|
|
522
|
+
});
|
|
523
|
+
it('does not mutate the input profile', () => {
|
|
524
|
+
const w = profile({ reputation: 50 });
|
|
525
|
+
applyReputationDelta(w, 10);
|
|
526
|
+
expect(w.reputation).toBe(50);
|
|
527
|
+
});
|
|
528
|
+
it('success yields positive delta, failure negative', () => {
|
|
529
|
+
expect(reputationDeltaForOutcome(true)).toBeGreaterThan(0);
|
|
530
|
+
expect(reputationDeltaForOutcome(false)).toBeLessThan(0);
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// ── 自组织驱动(自治大脑)──────────────────────────────────────────
|
|
535
|
+
|
|
536
|
+
describe('autonomousVolunteers (self-organization driver)', () => {
|
|
537
|
+
it('a matching online worker with capacity self-volunteers for an open need', () => {
|
|
538
|
+
const w = profile({ workerId: 'dev', capabilities: [cap('code')] });
|
|
539
|
+
const need = openNeed({ needId: 'n1', requiredCapabilities: ['code'] });
|
|
540
|
+
const out = autonomousVolunteers([need], [w]);
|
|
541
|
+
expect(out).toEqual([{ needId: 'n1', workerId: 'dev', fitScore: expect.any(Number) }]);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('skips workers at capacity, lacking capability, offline, or already volunteered', () => {
|
|
545
|
+
const busy = profile({
|
|
546
|
+
workerId: 'busy',
|
|
547
|
+
capabilities: [cap('code')],
|
|
548
|
+
activeTaskCount: 3,
|
|
549
|
+
maxConcurrent: 3,
|
|
550
|
+
});
|
|
551
|
+
const unskilled = profile({ workerId: 'unskilled', capabilities: [cap('design')] });
|
|
552
|
+
const ghost = profile({ workerId: 'ghost', capabilities: [cap('code')], status: 'offline' });
|
|
553
|
+
const already = profile({ workerId: 'already', capabilities: [cap('code')] });
|
|
554
|
+
const need = openNeed({
|
|
555
|
+
needId: 'n1',
|
|
556
|
+
requiredCapabilities: ['code'],
|
|
557
|
+
volunteers: [{ workerId: 'already', needId: 'n1', fitScore: 0.5, volunteeredAt: NOW }],
|
|
558
|
+
});
|
|
559
|
+
expect(autonomousVolunteers([need], [busy, unskilled, ghost, already])).toEqual([]);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('a worker volunteers for at most one need per tick, choosing the higher-fit need', () => {
|
|
563
|
+
// 该 worker 同时匹配两个需求,但 full(能力全覆盖)fit 高于 partial。
|
|
564
|
+
const w = profile({ workerId: 'dev', capabilities: [cap('code'), cap('design')] });
|
|
565
|
+
const fullMatch = openNeed({ needId: 'full', requiredCapabilities: ['code'] });
|
|
566
|
+
const partialMatch = openNeed({
|
|
567
|
+
needId: 'partial',
|
|
568
|
+
requiredCapabilities: ['code', 'design', 'frontend'],
|
|
569
|
+
});
|
|
570
|
+
const out = autonomousVolunteers([partialMatch, fullMatch], [w]);
|
|
571
|
+
expect(out).toHaveLength(1);
|
|
572
|
+
expect(out[0].needId).toBe('full');
|
|
573
|
+
expect(out[0].workerId).toBe('dev');
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('respects the max-volunteers-per-need cap and fills with the highest-fit workers', () => {
|
|
577
|
+
const a = profile({ workerId: 'a', capabilities: [cap('code')], reputation: 60 });
|
|
578
|
+
const b = profile({ workerId: 'b', capabilities: [cap('code')], reputation: 50 });
|
|
579
|
+
const c = profile({ workerId: 'c', capabilities: [cap('code')], reputation: 40 });
|
|
580
|
+
const need = openNeed({ needId: 'n1', requiredCapabilities: ['code'] });
|
|
581
|
+
const out = autonomousVolunteers([need], [c, b, a], [], { maxVolunteersPerNeed: 2 });
|
|
582
|
+
const chosen = out
|
|
583
|
+
.filter((d) => d.needId === 'n1')
|
|
584
|
+
.map((d) => d.workerId)
|
|
585
|
+
.sort();
|
|
586
|
+
expect(chosen).toEqual(['a', 'b']); // fit 由 reputation 决定 → 取最高的两个
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('respects pre-existing volunteers against the per-need cap', () => {
|
|
590
|
+
const a = profile({ workerId: 'a', capabilities: [cap('code')], reputation: 60 });
|
|
591
|
+
const need = openNeed({
|
|
592
|
+
needId: 'n1',
|
|
593
|
+
requiredCapabilities: ['code'],
|
|
594
|
+
volunteers: [
|
|
595
|
+
{ workerId: 'x', needId: 'n1', fitScore: 0.5, volunteeredAt: NOW },
|
|
596
|
+
{ workerId: 'y', needId: 'n1', fitScore: 0.4, volunteeredAt: NOW },
|
|
597
|
+
],
|
|
598
|
+
});
|
|
599
|
+
// 已有 2 个自荐者,cap=2 → 不再新增。
|
|
600
|
+
expect(autonomousVolunteers([need], [a], [], { maxVolunteersPerNeed: 2 })).toEqual([]);
|
|
601
|
+
// cap=3 → 还能加 1 个。
|
|
602
|
+
const out = autonomousVolunteers([need], [a], [], { maxVolunteersPerNeed: 3 });
|
|
603
|
+
expect(out).toEqual([{ needId: 'n1', workerId: 'a', fitScore: expect.any(Number) }]);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('ignores needs that are not open', () => {
|
|
607
|
+
const w = profile({ workerId: 'dev', capabilities: [cap('code')] });
|
|
608
|
+
const assigned = openNeed({ needId: 'n1', requiredCapabilities: ['code'], status: 'assigned' });
|
|
609
|
+
expect(autonomousVolunteers([assigned], [w])).toEqual([]);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('does not let a worker volunteer for a need they themselves posted', () => {
|
|
613
|
+
const w = profile({ workerId: 'self', capabilities: [cap('code')] });
|
|
614
|
+
const own = openNeed({ needId: 'n1', postedBy: 'self', requiredCapabilities: ['code'] });
|
|
615
|
+
expect(autonomousVolunteers([own], [w])).toEqual([]);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('lets a worker volunteer for multiple needs when maxNeedsPerWorker > 1', () => {
|
|
619
|
+
const w = profile({ workerId: 'dev', capabilities: [cap('code')] });
|
|
620
|
+
const a = openNeed({ needId: 'na', requiredCapabilities: ['code'] });
|
|
621
|
+
const b = openNeed({ needId: 'nb', requiredCapabilities: ['code'] });
|
|
622
|
+
const out = autonomousVolunteers([a, b], [w], [], { maxNeedsPerWorker: 2 });
|
|
623
|
+
expect(out).toHaveLength(2);
|
|
624
|
+
expect(out.map((d) => d.needId).sort()).toEqual(['na', 'nb']);
|
|
625
|
+
expect(out.every((d) => d.workerId === 'dev')).toBe(true);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it('returns [] for empty inputs', () => {
|
|
629
|
+
expect(autonomousVolunteers([], [])).toEqual([]);
|
|
630
|
+
expect(autonomousVolunteers([openNeed()], [])).toEqual([]);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('prefers a worker with a prior trust relationship to the poster (social feedback loop)', () => {
|
|
634
|
+
// 两个能力/声誉完全相同的 worker,只有 friend 与 poster 有信任关系。
|
|
635
|
+
const stranger = profile({ workerId: 'stranger', capabilities: [cap('code')], reputation: 50 });
|
|
636
|
+
const friend = profile({ workerId: 'friend', capabilities: [cap('code')], reputation: 50 });
|
|
637
|
+
const need = openNeed({ needId: 'n1', postedBy: 'poster', requiredCapabilities: ['code'] });
|
|
638
|
+
const rels: Relationship[] = [
|
|
639
|
+
{
|
|
640
|
+
fromWorker: 'friend',
|
|
641
|
+
toWorker: 'poster',
|
|
642
|
+
collaborations: 2,
|
|
643
|
+
successes: 2,
|
|
644
|
+
trust: 1,
|
|
645
|
+
lastInteractedAt: NOW,
|
|
646
|
+
},
|
|
647
|
+
];
|
|
648
|
+
// per-need cap=1 → 仅最高适配者入选;friend 因关系加分胜出。
|
|
649
|
+
const out = autonomousVolunteers([need], [stranger, friend], rels, { maxVolunteersPerNeed: 1 });
|
|
650
|
+
expect(out).toHaveLength(1);
|
|
651
|
+
expect(out[0].workerId).toBe('friend');
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('is deterministic and pure: does not mutate its inputs', () => {
|
|
655
|
+
const w = profile({ workerId: 'dev', capabilities: [cap('code')] });
|
|
656
|
+
const need = openNeed({ needId: 'n1', requiredCapabilities: ['code'] });
|
|
657
|
+
const needsSnap = JSON.parse(JSON.stringify([need]));
|
|
658
|
+
const workersSnap = JSON.parse(JSON.stringify([w]));
|
|
659
|
+
autonomousVolunteers([need], [w]);
|
|
660
|
+
expect([need]).toEqual(needsSnap);
|
|
661
|
+
expect([w]).toEqual(workersSnap);
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// ── 开放需求停滞归因 ────────────────────────────────────────────────
|
|
666
|
+
|
|
667
|
+
describe('classifyOpenNeedStall', () => {
|
|
668
|
+
const vol = (workerId: string) => ({
|
|
669
|
+
workerId,
|
|
670
|
+
needId: 'need-1',
|
|
671
|
+
fitScore: 0.8,
|
|
672
|
+
volunteeredAt: NOW,
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('non-open or already has volunteers → not stalled', () => {
|
|
676
|
+
const w = profile({ workerId: 'dev', capabilities: [cap('code')] });
|
|
677
|
+
expect(classifyOpenNeedStall(openNeed({ status: 'assigned' }), [w])).toBeNull();
|
|
678
|
+
expect(classifyOpenNeedStall(openNeed({ volunteers: [vol('dev')] }), [w])).toBeNull();
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('open + 0 volunteers + a capable available worker → not stalled (just not triggered yet)', () => {
|
|
682
|
+
const w = profile({
|
|
683
|
+
workerId: 'dev',
|
|
684
|
+
capabilities: [cap('code')],
|
|
685
|
+
activeTaskCount: 0,
|
|
686
|
+
maxConcurrent: 2,
|
|
687
|
+
});
|
|
688
|
+
expect(classifyOpenNeedStall(openNeed({ requiredCapabilities: ['code'] }), [w])).toBeNull();
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('open + 0 volunteers + capable worker but at capacity → workers_at_capacity', () => {
|
|
692
|
+
const w = profile({
|
|
693
|
+
workerId: 'dev',
|
|
694
|
+
capabilities: [cap('code')],
|
|
695
|
+
activeTaskCount: 2,
|
|
696
|
+
maxConcurrent: 2,
|
|
697
|
+
});
|
|
698
|
+
expect(classifyOpenNeedStall(openNeed({ requiredCapabilities: ['code'] }), [w])).toBe(
|
|
699
|
+
'workers_at_capacity'
|
|
700
|
+
);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('open + 0 volunteers + no worker has the capability → no_matching_worker', () => {
|
|
704
|
+
const w = profile({ workerId: 'dev', capabilities: [cap('design')] });
|
|
705
|
+
expect(classifyOpenNeedStall(openNeed({ requiredCapabilities: ['code'] }), [w])).toBe(
|
|
706
|
+
'no_matching_worker'
|
|
707
|
+
);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('the only capable worker is the poster → no_matching_worker (cannot self-assign)', () => {
|
|
711
|
+
const poster = profile({ workerId: 'user', capabilities: [cap('code')] });
|
|
712
|
+
expect(
|
|
713
|
+
classifyOpenNeedStall(openNeed({ postedBy: 'user', requiredCapabilities: ['code'] }), [
|
|
714
|
+
poster,
|
|
715
|
+
])
|
|
716
|
+
).toBe('no_matching_worker');
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it('mix: some lack capability, the only capable one is full → workers_at_capacity', () => {
|
|
720
|
+
const designer = profile({ workerId: 'd', capabilities: [cap('design')] });
|
|
721
|
+
const fullDev = profile({
|
|
722
|
+
workerId: 'dev',
|
|
723
|
+
capabilities: [cap('code')],
|
|
724
|
+
activeTaskCount: 3,
|
|
725
|
+
maxConcurrent: 3,
|
|
726
|
+
});
|
|
727
|
+
expect(
|
|
728
|
+
classifyOpenNeedStall(openNeed({ requiredCapabilities: ['code'] }), [designer, fullDev])
|
|
729
|
+
).toBe('workers_at_capacity');
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it('does not mutate its inputs', () => {
|
|
733
|
+
const w = profile({ workerId: 'dev', capabilities: [cap('code')] });
|
|
734
|
+
const need = openNeed({ requiredCapabilities: ['rust'] });
|
|
735
|
+
const needSnap = JSON.parse(JSON.stringify(need));
|
|
736
|
+
classifyOpenNeedStall(need, [w]);
|
|
737
|
+
expect(need).toEqual(needSnap);
|
|
738
|
+
});
|
|
739
|
+
});
|