| ... | ... | @@ -520,6 +520,13 @@ describe("OrchestratorMcpService provider resolution", () => { |
| 520 | 520 | skills: [], |
| 521 | 521 | }); |
| 522 | 522 | |
| 523 | /** A registry whose probes report the same snapshots it already holds. */ |
| 524 | const providerRegistryLayer = (providers: ReadonlyArray<ServerProvider>) => |
| 525 | Layer.mock(ProviderRegistry.ProviderRegistry)({ |
| 526 | getProviders: Effect.succeed(providers), |
| 527 | refreshInstance: () => Effect.succeed(providers), |
| 528 | }); |
| 529 | |
| 523 | 530 | const adapterRegistryLayer = (instanceIds: ReadonlyArray<ProviderInstanceId>) => |
| 524 | 531 | Layer.succeed( |
| 525 | 532 | ProviderAdapterRegistry.ProviderAdapterRegistryV2, |
| ... | ... | @@ -920,16 +927,14 @@ describe("OrchestratorMcpService provider resolution", () => { |
| 920 | 927 | Layer.mock(ThreadManagementService.ThreadManagementService)({ |
| 921 | 928 | getThreadRecords: () => Effect.succeed(parentProjection([])), |
| 922 | 929 | }), |
| 923 | | Layer.mock(ProviderRegistry.ProviderRegistry)({ |
| 924 | | getProviders: Effect.succeed([ |
| 925 | | providerSnapshot({ |
| 926 | | instanceId: codexInstanceId, |
| 927 | | driver: ProviderDriverKind.make("codex"), |
| 928 | | model: "gpt-5.4", |
| 929 | | }), |
| 930 | | forkShadow, |
| 931 | | ]), |
| 932 | | }), |
| 930 | providerRegistryLayer([ |
| 931 | providerSnapshot({ |
| 932 | instanceId: codexInstanceId, |
| 933 | driver: ProviderDriverKind.make("codex"), |
| 934 | model: "gpt-5.4", |
| 935 | }), |
| 936 | forkShadow, |
| 937 | ]), |
| 933 | 938 | adapterRegistryLayer([codexInstanceId]), |
| 934 | 939 | Layer.mock(ProjectService.ProjectService)({}), |
| 935 | 940 | Layer.mock(ScheduledTaskService.ScheduledTaskService)({}), |
| ... | ... | @@ -964,6 +969,107 @@ describe("OrchestratorMcpService provider resolution", () => { |
| 964 | 969 | }), |
| 965 | 970 | ); |
| 966 | 971 | |
| 972 | it.effect("re-probes an unavailable target once before refusing it", () => |
| 973 | Effect.gen(function* () { |
| 974 | const claudeInstanceId = ProviderInstanceId.make("claudeAgent"); |
| 975 | const claudeDriver = ProviderDriverKind.make("claudeAgent"); |
| 976 | const task = { |
| 977 | id: taskId, |
| 978 | threadId: parentThreadId, |
| 979 | runId: parentRunId, |
| 980 | parentNodeId, |
| 981 | origin: "app_owned", |
| 982 | createdBy: "agent", |
| 983 | driver: claudeDriver, |
| 984 | providerInstanceId: claudeInstanceId, |
| 985 | providerThreadId: null, |
| 986 | childThreadId, |
| 987 | nativeTaskRef: null, |
| 988 | prompt: "Review the diff.", |
| 989 | title: null, |
| 990 | model: "claude-opus-5-5", |
| 991 | status: "running", |
| 992 | result: null, |
| 993 | startedAt: null, |
| 994 | completedAt: null, |
| 995 | }; |
| 996 | const healthy = providerSnapshot({ |
| 997 | instanceId: claudeInstanceId, |
| 998 | driver: claudeDriver, |
| 999 | model: "claude-opus-5-5", |
| 1000 | }); |
| 1001 | const missingCli: ServerProvider = { |
| 1002 | ...healthy, |
| 1003 | installed: false, |
| 1004 | status: "error", |
| 1005 | message: "Claude Agent CLI (`claude`) was not found on PATH.", |
| 1006 | }; |
| 1007 | const codex = providerSnapshot({ |
| 1008 | instanceId: codexInstanceId, |
| 1009 | driver: ProviderDriverKind.make("codex"), |
| 1010 | model: "gpt-5.4", |
| 1011 | }); |
| 1012 | // The cache still says the CLI is missing; a probe reports `cliInstalled`. |
| 1013 | let cliInstalled = false; |
| 1014 | const probes = yield* Ref.make(0); |
| 1015 | const dispatched = yield* Ref.make(0); |
| 1016 | const dependencies = Layer.mergeAll( |
| 1017 | NodeServices.layer, |
| 1018 | Layer.mock(ThreadManagementService.ThreadManagementService)({ |
| 1019 | getThreadRecords: (threadId) => |
| 1020 | Effect.succeed( |
| 1021 | threadId === parentThreadId ? parentProjection([task]) : childProjection, |
| 1022 | ), |
| 1023 | dispatch: () => |
| 1024 | Ref.update(dispatched, (count) => count + 1).pipe( |
| 1025 | Effect.as({ |
| 1026 | sequence: 1, |
| 1027 | storedEvents: [ |
| 1028 | { |
| 1029 | sequence: 1, |
| 1030 | commandId: null, |
| 1031 | event: { type: "subagent.updated", payload: task }, |
| 1032 | }, |
| 1033 | ], |
| 1034 | } as never), |
| 1035 | ), |
| 1036 | }), |
| 1037 | Layer.mock(ProviderRegistry.ProviderRegistry)({ |
| 1038 | getProviders: Effect.succeed([codex, missingCli]), |
| 1039 | refreshInstance: () => |
| 1040 | Ref.update(probes, (count) => count + 1).pipe( |
| 1041 | Effect.as([codex, cliInstalled ? healthy : missingCli]), |
| 1042 | ), |
| 1043 | }), |
| 1044 | adapterRegistryLayer([codexInstanceId, claudeInstanceId]), |
| 1045 | Layer.mock(ScheduledTaskService.ScheduledTaskService)({}), |
| 1046 | Layer.mock(ProjectService.ProjectService)({}), |
| 1047 | ); |
| 1048 | |
| 1049 | yield* Effect.gen(function* () { |
| 1050 | const service = yield* OrchestratorMcpService.OrchestratorMcpService; |
| 1051 | const delegate = (clientRequestId: string) => |
| 1052 | service.delegateTask(scope, { |
| 1053 | task: "Review the diff.", |
| 1054 | target: { providerInstanceId: claudeInstanceId, model: "claude-opus-5-5" }, |
| 1055 | mode: "async", |
| 1056 | clientRequestId, |
| 1057 | }); |
| 1058 | |
| 1059 | const error = yield* delegate("delegate-recheck-1").pipe(Effect.flip); |
| 1060 | assert.equal(error.code, "provider_unavailable"); |
| 1061 | assert.equal(yield* Ref.get(probes), 1); |
| 1062 | assert.equal(yield* Ref.get(dispatched), 0); |
| 1063 | |
| 1064 | cliInstalled = true; |
| 1065 | const result = yield* delegate("delegate-recheck-2"); |
| 1066 | assert.equal(result.providerInstanceId, claudeInstanceId); |
| 1067 | assert.equal(yield* Ref.get(probes), 2); |
| 1068 | assert.equal(yield* Ref.get(dispatched), 1); |
| 1069 | }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies)))); |
| 1070 | }), |
| 1071 | ); |
| 1072 | |
| 967 | 1073 | it.effect( |
| 968 | 1074 | "inherits an available parent instance for driver-only targets and otherwise selects a healthy peer", |
| 969 | 1075 | () => |
| ... | ... | @@ -1078,22 +1184,20 @@ describe("OrchestratorMcpService provider resolution", () => { |
| 1078 | 1184 | } as never), |
| 1079 | 1185 | ), |
| 1080 | 1186 | }), |
| 1081 | | Layer.mock(ProviderRegistry.ProviderRegistry)({ |
| 1082 | | getProviders: Effect.succeed([ |
| 1083 | | providerSnapshot({ |
| 1084 | | instanceId: codexInstanceId, |
| 1085 | | driver, |
| 1086 | | model: "gpt-5.4", |
| 1087 | | enabled: testCase.inheritedEnabled, |
| 1088 | | }), |
| 1089 | | providerSnapshot({ |
| 1090 | | instanceId: codexAltInstanceId, |
| 1091 | | driver: testCase.candidateDriver, |
| 1092 | | model: "codex-alt-model", |
| 1093 | | enabled: testCase.peerEnabled, |
| 1094 | | }), |
| 1095 | | ]), |
| 1096 | | }), |
| 1187 | providerRegistryLayer([ |
| 1188 | providerSnapshot({ |
| 1189 | instanceId: codexInstanceId, |
| 1190 | driver, |
| 1191 | model: "gpt-5.4", |
| 1192 | enabled: testCase.inheritedEnabled, |
| 1193 | }), |
| 1194 | providerSnapshot({ |
| 1195 | instanceId: codexAltInstanceId, |
| 1196 | driver: testCase.candidateDriver, |
| 1197 | model: "codex-alt-model", |
| 1198 | enabled: testCase.peerEnabled, |
| 1199 | }), |
| 1200 | ]), |
| 1097 | 1201 | adapterRegistryLayer([codexInstanceId, codexAltInstanceId]), |
| 1098 | 1202 | Layer.mock(ProjectService.ProjectService)({}), |
| 1099 | 1203 | Layer.mock(ScheduledTaskService.ScheduledTaskService)({}), |