



















@@ -143,6 +143,23 @@ function expectPrimaryProbeSuccess(
143143});
144144}
145145146+function requireRecord(value: unknown, label: string): Record<string, unknown> {
147+if (!value || typeof value !== "object") {
148+throw new Error(`expected ${label}`);
149+}
150+return value as Record<string, unknown>;
151+}
152+153+function expectRecordWithFields(
154+records: Array<Record<string, unknown>>,
155+expected: Record<string, unknown>,
156+) {
157+const matching = records.find((record) =>
158+Object.entries(expected).every(([key, value]) => record[key] === value),
159+);
160+expect(matching, `expected matching record for ${JSON.stringify(expected)}`).toBeDefined();
161+}
162+146163async function expectProbeFailureFallsBack({
147164 reason,
148165 probeError,
@@ -398,77 +415,73 @@ describe("runWithModelFallback – probe logic", () => {
398415399416const decisionPayloads = logCapture.records
400417.filter((record) => record.message === "model fallback decision")
401-.map((record) => record.attributes ?? {});
402-403-expect(decisionPayloads).toEqual(
404-expect.arrayContaining([
405-expect.objectContaining({
406-event: "model_fallback_decision",
407-decision: "probe_cooldown_candidate",
408-candidateProvider: "openai",
409-candidateModel: "gpt-4.1-mini",
410-allowTransientCooldownProbe: true,
411-}),
412-expect.objectContaining({
413-event: "model_fallback_decision",
414-decision: "candidate_succeeded",
415-candidateProvider: "openai",
416-candidateModel: "gpt-4.1-mini",
417-isPrimary: true,
418-requestedModelMatched: true,
419-}),
420-expect.objectContaining({
421-event: "model_fallback_decision",
422-decision: "candidate_failed",
423-candidateProvider: "openai",
424-candidateModel: "gpt-4.1-mini",
425-isPrimary: true,
426-requestedModelMatched: true,
427-nextCandidateProvider: "anthropic",
428-nextCandidateModel: "claude-haiku-3-5",
429-fallbackStepType: "fallback_step",
430-fallbackStepFromModel: "openai/gpt-4.1-mini",
431-fallbackStepToModel: "anthropic/claude-haiku-3-5",
432-fallbackStepFromFailureReason: "rate_limit",
433-fallbackStepChainPosition: 1,
434-fallbackStepFinalOutcome: "next_fallback",
435-}),
436-expect.objectContaining({
437-event: "model_fallback_decision",
438-decision: "candidate_succeeded",
439-candidateProvider: "anthropic",
440-candidateModel: "claude-haiku-3-5",
441-isPrimary: false,
442-requestedModelMatched: false,
443-fallbackStepType: "fallback_step",
444-fallbackStepFromModel: "openai/gpt-4.1-mini",
445-fallbackStepToModel: "anthropic/claude-haiku-3-5",
446-fallbackStepFromFailureReason: "rate_limit",
447-fallbackStepChainPosition: 2,
448-fallbackStepFinalOutcome: "succeeded",
449-}),
450-]),
451-);
452-expect(onFallbackStep).toHaveBeenCalledWith(
453-expect.objectContaining({
454-fallbackStepType: "fallback_step",
455-fallbackStepFromModel: "openai/gpt-4.1-mini",
456-fallbackStepToModel: "anthropic/claude-haiku-3-5",
457-fallbackStepFromFailureReason: "rate_limit",
458-fallbackStepChainPosition: 1,
459-fallbackStepFinalOutcome: "next_fallback",
460-}),
461-);
462-expect(onFallbackStep).toHaveBeenCalledWith(
463-expect.objectContaining({
464-fallbackStepType: "fallback_step",
465-fallbackStepFromModel: "openai/gpt-4.1-mini",
466-fallbackStepToModel: "anthropic/claude-haiku-3-5",
467-fallbackStepFromFailureReason: "rate_limit",
468-fallbackStepChainPosition: 2,
469-fallbackStepFinalOutcome: "succeeded",
470-}),
418+.map((record) => requireRecord(record.attributes, "decision payload"));
419+420+expectRecordWithFields(decisionPayloads, {
421+event: "model_fallback_decision",
422+decision: "probe_cooldown_candidate",
423+candidateProvider: "openai",
424+candidateModel: "gpt-4.1-mini",
425+allowTransientCooldownProbe: true,
426+});
427+expectRecordWithFields(decisionPayloads, {
428+event: "model_fallback_decision",
429+decision: "candidate_succeeded",
430+candidateProvider: "openai",
431+candidateModel: "gpt-4.1-mini",
432+isPrimary: true,
433+requestedModelMatched: true,
434+});
435+expectRecordWithFields(decisionPayloads, {
436+event: "model_fallback_decision",
437+decision: "candidate_failed",
438+candidateProvider: "openai",
439+candidateModel: "gpt-4.1-mini",
440+isPrimary: true,
441+requestedModelMatched: true,
442+nextCandidateProvider: "anthropic",
443+nextCandidateModel: "claude-haiku-3-5",
444+fallbackStepType: "fallback_step",
445+fallbackStepFromModel: "openai/gpt-4.1-mini",
446+fallbackStepToModel: "anthropic/claude-haiku-3-5",
447+fallbackStepFromFailureReason: "rate_limit",
448+fallbackStepChainPosition: 1,
449+fallbackStepFinalOutcome: "next_fallback",
450+});
451+expectRecordWithFields(decisionPayloads, {
452+event: "model_fallback_decision",
453+decision: "candidate_succeeded",
454+candidateProvider: "anthropic",
455+candidateModel: "claude-haiku-3-5",
456+isPrimary: false,
457+requestedModelMatched: false,
458+fallbackStepType: "fallback_step",
459+fallbackStepFromModel: "openai/gpt-4.1-mini",
460+fallbackStepToModel: "anthropic/claude-haiku-3-5",
461+fallbackStepFromFailureReason: "rate_limit",
462+fallbackStepChainPosition: 2,
463+fallbackStepFinalOutcome: "succeeded",
464+});
465+466+const fallbackSteps = onFallbackStep.mock.calls.map(([step]) =>
467+requireRecord(step, "fallback step"),
471468);
469+expectRecordWithFields(fallbackSteps, {
470+fallbackStepType: "fallback_step",
471+fallbackStepFromModel: "openai/gpt-4.1-mini",
472+fallbackStepToModel: "anthropic/claude-haiku-3-5",
473+fallbackStepFromFailureReason: "rate_limit",
474+fallbackStepChainPosition: 1,
475+fallbackStepFinalOutcome: "next_fallback",
476+});
477+expectRecordWithFields(fallbackSteps, {
478+fallbackStepType: "fallback_step",
479+fallbackStepFromModel: "openai/gpt-4.1-mini",
480+fallbackStepToModel: "anthropic/claude-haiku-3-5",
481+fallbackStepFromFailureReason: "rate_limit",
482+fallbackStepChainPosition: 2,
483+fallbackStepFinalOutcome: "succeeded",
484+});
472485});
473486474487it.each([
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。