Close protocol compatibility review gaps

Review feedback found narrow compatibility regressions in the web-search shim, Responses tool detection, service tier accounting, and update-center Docker candidate actions. The fix keeps synthetic search routed through the dedicated search model, preserves Responses text compatibility, distinguishes absent service_tier from filtered service_tier, and evaluates recent Docker tags per candidate.

Constraint: Review fixes must stay on dev first, then flow into the main-based release PR branch.

Rejected: Leave Responses web_search simulation on the request model | it has the same search-route selection risk as Anthropic simulation.

Confidence: high

Scope-risk: narrow

Tested: npx vitest run --root . src/server/proxy-core/responsesPreflight.test.ts src/server/proxy-core/serviceTierPolicy.test.ts src/server/routes/proxy/chat.stream.test.ts src/web/pages/settings/UpdateCenterSection.test.tsx

Tested: npm run typecheck:server

Tested: npm run typecheck:web

Tested: npm run repo:drift-check

Co-authored-by: OmX <omx@oh-my-codex.dev>
This commit is contained in:
cita-777
2026-05-08 10:42:49 +08:00
parent 2491a9af5a
commit 8309ac073a
9 changed files with 163 additions and 19 deletions
@@ -500,6 +500,110 @@ describe('UpdateCenterSection', () => {
}
});
it('disables only the recent Docker candidate that already matches the running helper image', async () => {
apiMock.getUpdateCenterStatus.mockResolvedValueOnce({
currentVersion: '1.2.3',
config: {
enabled: true,
helperBaseUrl: 'http://metapi-deploy-helper.ai.svc.cluster.local:9850',
namespace: 'ai',
releaseName: 'metapi',
chartRef: 'oci://ghcr.io/cita-777/charts/metapi',
imageRepository: '1467078763/metapi',
githubReleasesEnabled: true,
dockerHubTagsEnabled: true,
defaultDeploySource: 'docker-hub-tag',
},
githubRelease: {
normalizedVersion: '1.3.0',
displayVersion: '1.3.0',
},
dockerHubTag: {
normalizedVersion: 'latest',
tagName: 'latest',
digest: 'sha256:efb2ee6553866bd3268dcc54c02fa5f9789728c51ed4af63328aaba6da67df35',
displayVersion: 'latest @ sha256:efb2ee655386',
},
dockerHubRecentTags: [
{
normalizedVersion: 'dev',
tagName: 'dev',
digest: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
displayVersion: 'dev @ sha256:aaaaaaaaaaaa',
publishedAt: '2026-03-30T11:54:35.591877Z',
},
{
normalizedVersion: 'dev-20260417-next',
tagName: 'dev-20260417-next',
digest: 'sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
displayVersion: 'dev-20260417-next @ sha256:bbbbbbbbbbbb',
publishedAt: '2026-03-30T10:54:35.591877Z',
},
],
helper: {
ok: true,
healthy: true,
revision: '17',
imageTag: 'dev',
imageDigest: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
history: [],
},
runtime: {
lastCheckedAt: '2026-03-30 20:30:00',
lastCheckError: null,
lastResolvedSource: 'docker-hub-tag',
lastResolvedDisplayVersion: 'dev',
lastResolvedCandidateKey: 'docker-hub-tag:dev@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
lastNotifiedCandidateKey: null,
lastNotifiedAt: null,
},
runningTask: null,
lastFinishedTask: null,
});
let root!: ReactTestRenderer;
try {
await act(async () => {
root = create(
<MemoryRouter>
<ToastProvider>
<UpdateCenterSection />
</ToastProvider>
</MemoryRouter>,
);
});
await flushMicrotasks();
const runningCandidateButton = root.root.find((node) => (
node.type === 'button'
&& typeof node.props.onClick === 'function'
&& collectText(node) === '部署 dev'
));
const nextCandidateButton = root.root.find((node) => (
node.type === 'button'
&& typeof node.props.onClick === 'function'
&& collectText(node) === '部署 dev-20260417-next'
));
expect(runningCandidateButton.props.disabled).toBe(true);
expect(runningCandidateButton.props.title).toContain('当前已运行该镜像');
expect(nextCandidateButton.props.disabled).toBe(false);
await act(async () => {
await nextCandidateButton.props.onClick();
});
await flushMicrotasks();
expect(apiMock.deployUpdateCenter).toHaveBeenCalledWith({
source: 'docker-hub-tag',
targetTag: 'dev-20260417-next',
targetDigest: 'sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
});
} finally {
root?.unmount();
}
});
it('keeps rollback history compact and opens the full revision list in a centered modal', async () => {
let root!: ReactTestRenderer;
try {
+12 -6
View File
@@ -445,10 +445,6 @@ export default function UpdateCenterSection() {
const manualDockerTag = String(manualDockerTarget.tag || '').trim();
const manualDockerDigest = String(manualDockerTarget.digest || '').trim();
const recentDockerCandidates = normalizeRecentDockerCandidates(status?.dockerHubRecentTags);
const canDeployDockerByTag = !deploying
&& config.enabled
&& config.dockerHubTagsEnabled
&& helperHealthy;
const canDeployManualDocker = !deploying
&& config.enabled
&& config.dockerHubTagsEnabled
@@ -803,6 +799,15 @@ export default function UpdateCenterSection() {
const candidateTag = String(candidate.tagName || candidate.normalizedVersion || '').trim();
const candidateDigest = String(candidate.digest || '').trim();
const candidateLabel = candidate.displayVersion || candidate.normalizedVersion || candidateTag;
const candidateDeployState = describeDockerDeployState({
enabled: config.enabled && config.dockerHubTagsEnabled,
helperHealthy,
helperError: status?.helper?.error,
currentVersion: status?.currentVersion,
helper: status?.helper,
candidate,
});
const canDeployCandidate = !deploying && candidateDeployState.canDeploy;
return (
<div
key={`${candidateTag}:${candidateDigest || 'no-digest'}`}
@@ -825,9 +830,10 @@ export default function UpdateCenterSection() {
type="button"
className="btn btn-ghost"
style={{ border: '1px solid var(--color-border)' }}
disabled={!canDeployDockerByTag || !candidateTag}
disabled={!canDeployCandidate || !candidateTag}
title={candidateDeployState.reason}
onClick={() => {
if (!canDeployDockerByTag || !candidateTag) return;
if (!canDeployCandidate || !candidateTag) return;
void runDeploy('docker-hub-tag', {
tag: candidateTag,
digest: candidateDigest || null,