Feat/upgrade openclaw (#729)

This commit is contained in:
paisley
2026-04-01 14:22:47 +08:00
committed by GitHub
Unverified
parent bf5b089158
commit d34a88e629
24 changed files with 903 additions and 600 deletions

View File

@@ -2,15 +2,13 @@ import { describe, expect, it } from 'vitest';
import { GatewayRestartGovernor } from '@electron/gateway/restart-governor';
describe('GatewayRestartGovernor', () => {
it('suppresses restart during exponential cooldown window', () => {
const governor = new GatewayRestartGovernor({
baseCooldownMs: 1000,
maxCooldownMs: 8000,
maxRestartsPerWindow: 10,
windowMs: 60000,
stableResetMs: 60000,
circuitOpenMs: 60000,
});
it('allows first restart unconditionally', () => {
const governor = new GatewayRestartGovernor();
expect(governor.decide(1000).allow).toBe(true);
});
it('suppresses restart during cooldown window', () => {
const governor = new GatewayRestartGovernor({ cooldownMs: 1000 });
expect(governor.decide(1000).allow).toBe(true);
governor.recordExecuted(1000);
@@ -18,76 +16,26 @@ describe('GatewayRestartGovernor', () => {
const blocked = governor.decide(1500);
expect(blocked.allow).toBe(false);
expect(blocked.allow ? '' : blocked.reason).toBe('cooldown_active');
expect(blocked.allow ? 0 : blocked.retryAfterMs).toBeGreaterThan(0);
expect(blocked.allow ? 0 : blocked.retryAfterMs).toBe(500);
expect(governor.decide(3000).allow).toBe(true);
// After cooldown expires, restart is allowed again
expect(governor.decide(2001).allow).toBe(true);
});
it('opens circuit after restart budget is exceeded', () => {
const governor = new GatewayRestartGovernor({
maxRestartsPerWindow: 2,
windowMs: 60000,
baseCooldownMs: 0,
maxCooldownMs: 0,
stableResetMs: 120000,
circuitOpenMs: 30000,
});
it('allows unlimited restarts as long as cooldown is respected', () => {
const governor = new GatewayRestartGovernor({ cooldownMs: 100 });
expect(governor.decide(1000).allow).toBe(true);
governor.recordExecuted(1000);
expect(governor.decide(2000).allow).toBe(true);
governor.recordExecuted(2000);
const budgetBlocked = governor.decide(3000);
expect(budgetBlocked.allow).toBe(false);
expect(budgetBlocked.allow ? '' : budgetBlocked.reason).toBe('budget_exceeded');
const circuitBlocked = governor.decide(4000);
expect(circuitBlocked.allow).toBe(false);
expect(circuitBlocked.allow ? '' : circuitBlocked.reason).toBe('circuit_open');
expect(governor.decide(62001).allow).toBe(true);
// 10 restarts in a row, each respecting cooldown — all should be allowed
for (let i = 0; i < 10; i++) {
const t = 1000 + i * 200;
expect(governor.decide(t).allow).toBe(true);
governor.recordExecuted(t);
}
});
it('resets consecutive backoff after stable running period', () => {
const governor = new GatewayRestartGovernor({
baseCooldownMs: 1000,
maxCooldownMs: 8000,
maxRestartsPerWindow: 10,
windowMs: 600000,
stableResetMs: 5000,
circuitOpenMs: 60000,
});
governor.recordExecuted(0);
governor.recordExecuted(1000);
const blockedBeforeStable = governor.decide(2500);
expect(blockedBeforeStable.allow).toBe(false);
expect(blockedBeforeStable.allow ? '' : blockedBeforeStable.reason).toBe('cooldown_active');
governor.onRunning(3000);
const allowedAfterStable = governor.decide(9000);
expect(allowedAfterStable.allow).toBe(true);
});
it('resets time-based state when clock moves backwards', () => {
const governor = new GatewayRestartGovernor({
maxRestartsPerWindow: 2,
windowMs: 60000,
baseCooldownMs: 1000,
maxCooldownMs: 8000,
stableResetMs: 60000,
circuitOpenMs: 30000,
});
governor.recordExecuted(10_000);
governor.recordExecuted(11_000);
const blocked = governor.decide(11_500);
expect(blocked.allow).toBe(false);
// Simulate clock rewind and verify stale guard state does not lock out restarts.
const afterRewind = governor.decide(9_000);
expect(afterRewind.allow).toBe(true);
it('onRunning is a no-op but does not throw', () => {
const governor = new GatewayRestartGovernor();
expect(() => governor.onRunning(1000)).not.toThrow();
});
it('wraps counters safely at MAX_SAFE_INTEGER', () => {
@@ -103,4 +51,12 @@ describe('GatewayRestartGovernor', () => {
suppressedTotal: 0,
});
});
it('getObservability returns circuit_open_until as always 0', () => {
const governor = new GatewayRestartGovernor();
governor.recordExecuted(1000);
const obs = governor.getObservability();
expect(obs.circuit_open_until).toBe(0);
expect(obs.executed_total).toBe(1);
});
});

View File

@@ -61,14 +61,22 @@ describe('provider-model-sync', () => {
});
});
it('returns null for oauth and multi-instance providers', () => {
it('builds modelstudio payload and returns null for multi-instance providers', () => {
expect(
buildNonOAuthAgentProviderUpdate(
providerConfig({ type: 'qwen-portal', id: 'qwen-portal' }),
'qwen-portal',
'qwen-portal/coder-model',
providerConfig({ type: 'modelstudio', id: 'modelstudio' }),
'modelstudio',
'modelstudio/qwen3.5-plus',
),
).toBeNull();
).toEqual({
providerKey: 'modelstudio',
entry: {
baseUrl: 'https://coding.dashscope.aliyuncs.com/v1',
api: 'openai-completions',
apiKey: 'MODELSTUDIO_API_KEY',
models: [{ id: 'qwen3.5-plus', name: 'qwen3.5-plus' }],
},
});
expect(
buildNonOAuthAgentProviderUpdate(

View File

@@ -58,7 +58,7 @@ describe('provider metadata', () => {
it('keeps builtin provider sources in sync', () => {
expect(BUILTIN_PROVIDER_TYPES).toEqual(
expect.arrayContaining(['anthropic', 'openai', 'google', 'openrouter', 'ark', 'moonshot', 'siliconflow', 'minimax-portal', 'minimax-portal-cn', 'qwen-portal', 'ollama'])
expect.arrayContaining(['anthropic', 'openai', 'google', 'openrouter', 'ark', 'moonshot', 'siliconflow', 'minimax-portal', 'minimax-portal-cn', 'modelstudio', 'ollama'])
);
});
@@ -125,13 +125,13 @@ describe('provider metadata', () => {
const google = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'google');
const minimax = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'minimax-portal');
const minimaxCn = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'minimax-portal-cn');
const qwen = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'qwen-portal');
const qwen = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'modelstudio');
expect(openai).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'gpt-5.4' });
expect(google).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'gemini-3-pro-preview' });
expect(minimax).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'MiniMax-M2.7' });
expect(minimaxCn).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'MiniMax-M2.7' });
expect(qwen).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'coder-model' });
expect(qwen).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'qwen3.5-plus' });
expect(shouldShowProviderModelId(openai, false)).toBe(false);
expect(shouldShowProviderModelId(google, false)).toBe(false);
@@ -149,7 +149,7 @@ describe('provider metadata', () => {
expect(resolveProviderModelForSave(google, ' ', true)).toBe('gemini-3-pro-preview');
expect(resolveProviderModelForSave(minimax, ' ', true)).toBe('MiniMax-M2.7');
expect(resolveProviderModelForSave(minimaxCn, ' ', true)).toBe('MiniMax-M2.7');
expect(resolveProviderModelForSave(qwen, ' ', true)).toBe('coder-model');
expect(resolveProviderModelForSave(qwen, ' ', true)).toBe('qwen3.5-plus');
});
it('saves OpenRouter model overrides by default and SiliconFlow only in dev mode', () => {