fix(hooks): isolate slug-generator auth failures · openclaw/openclaw@318cae1
openperf
·
2026-05-31
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import { expect, vi } from "vitest"; |
| 2 | +import type { ProviderRuntimeModel } from "../plugins/provider-runtime-model.types.js"; |
2 | 3 | import type { AgentMessage } from "./runtime/index.js"; |
3 | 4 | import type { SessionManager } from "./sessions/index.js"; |
4 | 5 | import type { TranscriptPolicy } from "./transcript-policy.js"; |
@@ -12,6 +13,7 @@ export type SanitizeSessionHistoryFn = (params: {
|
12 | 13 | sessionManager: SessionManager; |
13 | 14 | sessionId: string; |
14 | 15 | modelId?: string; |
| 16 | +model?: ProviderRuntimeModel; |
15 | 17 | policy?: TranscriptPolicy; |
16 | 18 | preserveLatestAssistantThinking?: boolean; |
17 | 19 | }) => Promise<AgentMessage[]>; |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -3524,7 +3524,7 @@ function shouldPreserveOpenRouterReasoningReplay(model: OpenAIModeModel): boolea
|
3524 | 3524 | } |
3525 | 3525 | |
3526 | 3526 | function shouldTrustReasoningContentReplayMetadata(model: OpenAIModeModel): boolean { |
3527 | | -if (model.reasoning !== true || isGemma4ModelId(model.id)) { |
| 3527 | +if (!model.reasoning || isGemma4ModelId(model.id)) { |
3528 | 3528 | return false; |
3529 | 3529 | } |
3530 | 3530 | const provider = model.provider.trim().toLowerCase(); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -54,6 +54,16 @@ describe("generateSlugViaLLM", () => {
|
54 | 54 | expect(options.cleanupBundleMcpOnRunEnd).toBe(true); |
55 | 55 | }); |
56 | 56 | |
| 57 | +it("marks the run lane-local so internal-helper failures do not poison shared profile health (#71709)", async () => { |
| 58 | +await generateSlugViaLLM({ |
| 59 | +sessionContent: "hello", |
| 60 | +cfg: {} as OpenClawConfig, |
| 61 | +}); |
| 62 | + |
| 63 | +expect(runEmbeddedAgentMock).toHaveBeenCalledOnce(); |
| 64 | +expect(requireFirstRunOptions().authProfileFailurePolicy).toBe("local"); |
| 65 | +}); |
| 66 | + |
57 | 67 | it("honors configured agent timeoutSeconds for slow local providers", async () => { |
58 | 68 | await generateSlugViaLLM({ |
59 | 69 | sessionContent: "hello", |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -73,6 +73,9 @@ Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design",
|
73 | 73 | timeoutMs, |
74 | 74 | runId: `slug-gen-${Date.now()}`, |
75 | 75 | cleanupBundleMcpOnRunEnd: true, |
| 76 | +// Internal helper run: route failures lane-local so an upstream 400/billing |
| 77 | +// here cannot poison the shared profile (#71709). |
| 78 | +authProfileFailurePolicy: "local", |
76 | 79 | }); |
77 | 80 | |
78 | 81 | // Extract text from payloads |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -19,17 +19,43 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
19 | 19 | return Boolean(value) && typeof value === "object" && !Array.isArray(value); |
20 | 20 | } |
21 | 21 | |
| 22 | +function parseDeviceAuthEntry(role: string, value: unknown): DeviceAuthEntry | null { |
| 23 | +if ( |
| 24 | +!isRecord(value) || |
| 25 | +typeof value.token !== "string" || |
| 26 | +!Array.isArray(value.scopes) || |
| 27 | +!value.scopes.every((scope) => typeof scope === "string") || |
| 28 | +typeof value.updatedAtMs !== "number" || |
| 29 | +!Number.isFinite(value.updatedAtMs) |
| 30 | +) { |
| 31 | +return null; |
| 32 | +} |
| 33 | +return { |
| 34 | +token: value.token, |
| 35 | + role, |
| 36 | +scopes: value.scopes, |
| 37 | +updatedAtMs: value.updatedAtMs, |
| 38 | +}; |
| 39 | +} |
| 40 | + |
22 | 41 | function parseDeviceAuthStore(value: unknown): DeviceAuthStore | null { |
23 | 42 | if (!isRecord(value) || value.version !== 1 || typeof value.deviceId !== "string") { |
24 | 43 | return null; |
25 | 44 | } |
26 | 45 | if (!isRecord(value.tokens)) { |
27 | 46 | return null; |
28 | 47 | } |
| 48 | +const tokens: Record<string, DeviceAuthEntry> = {}; |
| 49 | +for (const [role, rawEntry] of Object.entries(value.tokens)) { |
| 50 | +const entry = parseDeviceAuthEntry(role, rawEntry); |
| 51 | +if (entry) { |
| 52 | +tokens[role] = entry; |
| 53 | +} |
| 54 | +} |
29 | 55 | return { |
30 | 56 | version: 1, |
31 | 57 | deviceId: value.deviceId, |
32 | | -tokens: value.tokens, |
| 58 | + tokens, |
33 | 59 | }; |
34 | 60 | } |
35 | 61 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -168,6 +168,7 @@ describe("applyMediaUnderstanding – echo transcript", () => {
|
168 | 168 | `No API key resolved for provider "${provider}" (auth mode: ${auth?.mode}).`, |
169 | 169 | ); |
170 | 170 | }, |
| 171 | +isProviderAuthError: vi.fn(() => false), |
171 | 172 | resolveAwsSdkEnvVarName: vi.fn(() => undefined), |
172 | 173 | resolveEnvApiKey: vi.fn(() => null), |
173 | 174 | resolveModelAuthMode: vi.fn(() => "api-key"), |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -279,6 +279,7 @@ describe("applyMediaUnderstanding", () => {
|
279 | 279 | `No API key resolved for provider "${provider}" (auth mode: ${auth?.mode}).`, |
280 | 280 | ); |
281 | 281 | }, |
| 282 | +isProviderAuthError: vi.fn(() => false), |
282 | 283 | })); |
283 | 284 | vi.doMock("../media/fetch.js", () => ({ |
284 | 285 | readRemoteMediaBuffer: readRemoteMediaBufferMock, |
@@ -365,7 +366,6 @@ describe("applyMediaUnderstanding", () => {
|
365 | 366 | cfg: createGroqAudioConfig(), |
366 | 367 | providers: createGroqProviders(), |
367 | 368 | }); |
368 | | - |
369 | 369 | expect(result.appliedAudio).toBe(true); |
370 | 370 | expectTranscriptApplied({ |
371 | 371 | ctx, |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import { vi } from "vitest"; |
2 | 2 | |
3 | 3 | export function createAvailableModelAuthMockModule() { |
| 4 | +class ProviderAuthError extends Error { |
| 5 | +constructor( |
| 6 | +readonly code: "missing-api-key" | "missing-provider-auth", |
| 7 | +readonly provider: string, |
| 8 | +message: string, |
| 9 | +) { |
| 10 | +super(message); |
| 11 | +this.name = "ProviderAuthError"; |
| 12 | +} |
| 13 | +} |
| 14 | + |
4 | 15 | return { |
5 | 16 | hasAvailableAuthForProvider: vi.fn(() => true), |
6 | 17 | resolveApiKeyForProvider: vi.fn(async () => ({ |
@@ -9,6 +20,11 @@ export function createAvailableModelAuthMockModule() {
|
9 | 20 | mode: "api-key", |
10 | 21 | })), |
11 | 22 | requireApiKey: vi.fn((auth: { apiKey?: string }) => auth.apiKey ?? "test-key"), |
| 23 | + ProviderAuthError, |
| 24 | +isProviderAuthError: vi.fn( |
| 25 | +(err: unknown, code?: "missing-api-key" | "missing-provider-auth") => |
| 26 | +err instanceof ProviderAuthError && (!code || err.code === code), |
| 27 | +), |
12 | 28 | }; |
13 | 29 | } |
14 | 30 | |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。