When applyCompatibilityAdvisory overwrites a provider's status/message with compatibility information, the original values (e.g. from a genuine probe failure) were permanently lost. When a subsequent advisory removal or replacement occurred, removeExistingCompatibilityAdvisory would reset status to 'ready' instead of the original pre-advisory status. Fix by recording preAdvisoryStatus and preAdvisoryMessage in the advisory object when it is applied. removeExistingCompatibilityAdvisory now restores these saved values instead of unconditionally resetting to 'ready'/'disabled'. For old advisories without these fields, the fallback behavior is unchanged.
Unified · Split
| @@ -327,6 +327,52 @@ describe("provider compatibility", () => { · 326 unmodified lines | |||
| 327 | }); | 327 | }); |
| 328 | }); | 328 | }); |
| 329 | 329 | ||
| 330 | it.effect("preserves genuine probe errors when the remote map relaxes a bundled advisory", () => { | ||
| 331 | const bundledMessage = | ||
| 332 | "This provider harness version 0.130.0 is known to be incompatible with this T3 Code release. Use 0.129.0."; | ||
| 333 | const probeErrorMessage = "CLI health-check failed: process exited with code 1"; | ||
| 334 | const remoteDocument = { | ||
| 335 | version: 1, | ||
| 336 | policies: [ | ||
| 337 | { | ||
| 338 | t3CodeRange: ">=0.0.0", | ||
| 339 | driver: "codex", | ||
| 340 | recommendedRange: ">=0.130.0", | ||
| 341 | recommendedVersion: "0.130.0", | ||
| 342 | ranges: [{ status: "supported", range: ">=0.130.0" }], | ||
| 343 | }, | ||
| 344 | ], | ||
| 345 | }; | ||
| 346 | |||
| 347 | return Effect.gen(function* () { | ||
| 348 | const enriched = yield* enrichProviderSnapshotWithCompatibilityAdvisory({ | ||
| 349 | ...baseProvider, | ||
| 350 | status: "error", | ||
| 351 | message: bundledMessage, | ||
| 352 | compatibilityAdvisory: { | ||
| 353 | status: "broken", | ||
| 354 | severity: "error", | ||
| 355 | currentVersion: "0.130.0", | ||
| 356 | message: bundledMessage, | ||
| 357 | recommendedRange: "<0.130.0", | ||
| 358 | recommendedVersion: "0.129.0", | ||
| 359 | ranges: [{ status: "broken", range: ">=0.130.0" }], | ||
| 360 | preAdvisoryStatus: "error", | ||
| 361 | preAdvisoryMessage: probeErrorMessage, | ||
| 362 | }, | ||
| 363 | }).pipe( | ||
| 364 | Effect.provideService( | ||
| 365 | HttpClient.HttpClient, | ||
| 366 | jsonHttpClient(() => ({ payload: remoteDocument })), | ||
| 367 | ), | ||
| 368 | ); | ||
| 369 | |||
| 370 | expect(enriched.status).toBe("error"); | ||
| 371 | expect(enriched.message).toBe(probeErrorMessage); | ||
| 372 | expect(enriched.compatibilityAdvisory).toMatchObject({ status: "supported" }); | ||
| 373 | }); | ||
| 374 | }); | ||
| 375 | |||
| 330 | it.effect("falls back to the bundled map when the remote compatibility fetch fails", () => | 376 | it.effect("falls back to the bundled map when the remote compatibility fetch fails", () => |
| 331 | Effect.gen(function* () { | 377 | Effect.gen(function* () { |
| 332 | const enriched = yield* enrichProviderSnapshotWithCompatibilityAdvisory({ | 378 | const enriched = yield* enrichProviderSnapshotWithCompatibilityAdvisory({ |
| @@ -325,13 +325,19 @@ function applyCompatibilityAdvisory<Snapshot extends ProviderCompatibilitySnapsh · 324 unmodified lines | |||
| 325 | ? "warning" | 325 | ? "warning" |
| 326 | : baseSnapshot.status; | 326 | : baseSnapshot.status; |
| 327 | 327 | ||
| 328 | const advisoryWithPreState: ServerProviderCompatibilityAdvisory = { | ||
| 329 | ...compatibilityAdvisory, | ||
| 330 | preAdvisoryStatus: baseSnapshot.status, | ||
| 331 | ...(baseSnapshot.message ? { preAdvisoryMessage: baseSnapshot.message } : {}), | ||
| 332 | }; | ||
| 333 | |||
| 328 | return { | 334 | return { |
| 329 | ...baseSnapshot, | 335 | ...baseSnapshot, |
| 330 | status, | 336 | status, |
| 331 | ...(compatibilityMessage || baseSnapshot.message | 337 | ...(compatibilityMessage || baseSnapshot.message |
| 332 | ? { message: compatibilityMessage ?? baseSnapshot.message } | 338 | ? { message: compatibilityMessage ?? baseSnapshot.message } |
| 333 | : {}), | 339 | : {}), |
| 334 | compatibilityAdvisory, | 340 | compatibilityAdvisory: advisoryWithPreState, |
| 335 | } as Snapshot; | 341 | } as Snapshot; |
| 336 | } | 342 | } |
| 337 | 343 | ||
| @@ -349,11 +355,19 @@ function removeExistingCompatibilityAdvisory<Snapshot extends ProviderCompatibil · 11 unmodified lines | |||
| 349 | : undefined; | 355 | : undefined; |
| 350 | if (compatibilityMessage && baseSnapshot.message === compatibilityMessage) { | 356 | if (compatibilityMessage && baseSnapshot.message === compatibilityMessage) { |
| 351 | const { message: _message, ...snapshotWithoutCompatibilityMessage } = baseSnapshot; | 357 | const { message: _message, ...snapshotWithoutCompatibilityMessage } = baseSnapshot; |
| 358 | const restoredStatus = | ||
| 359 | existingCompatibilityAdvisory.preAdvisoryStatus ?? (snapshot.enabled ? "ready" : "disabled"); | ||
| 352 | return { | 360 | return { |
| 353 | ...snapshotWithoutCompatibilityMessage, | 361 | ...snapshotWithoutCompatibilityMessage, |
| 354 | status: snapshot.enabled ? "ready" : "disabled", | 362 | status: restoredStatus, |
| 363 | ...(existingCompatibilityAdvisory.preAdvisoryMessage | ||
| 364 | ? { message: existingCompatibilityAdvisory.preAdvisoryMessage } | ||
| 365 | : {}), | ||
| 355 | } as Snapshot; | 366 | } as Snapshot; |
| 356 | } | 367 | } |
| 368 | if (existingCompatibilityAdvisory.preAdvisoryStatus !== undefined) { | ||
| 369 | return { ...baseSnapshot, status: existingCompatibilityAdvisory.preAdvisoryStatus } as Snapshot; | ||
| 370 | } | ||
| 357 | return baseSnapshot as Snapshot; | 371 | return baseSnapshot as Snapshot; |
| 358 | } | 372 | } |
| 359 | 373 | ||
| @@ -163,6 +163,8 @@ export const ServerProviderCompatibilityAdvisory = Schema.Struct({ · 162 unmodified lines | |||
| 163 | ranges: Schema.Array(ServerProviderCompatibilityRange).pipe( | 163 | ranges: Schema.Array(ServerProviderCompatibilityRange).pipe( |
| 164 | Schema.withDecodingDefault(Effect.succeed([])), | 164 | Schema.withDecodingDefault(Effect.succeed([])), |
| 165 | ), | 165 | ), |
| 166 | preAdvisoryStatus: Schema.optionalKey(ServerProviderState), | ||
| 167 | preAdvisoryMessage: Schema.optionalKey(TrimmedNonEmptyString), | ||
| 166 | }); | 168 | }); |
| 167 | export type ServerProviderCompatibilityAdvisory = typeof ServerProviderCompatibilityAdvisory.Type; | 169 | export type ServerProviderCompatibilityAdvisory = typeof ServerProviderCompatibilityAdvisory.Type; |
| 168 | 170 | ||