Skip to content

Commit 545e0ed

Browse files
committed
fix: edit model ui crash issue fix
1 parent 7bc3dbc commit 545e0ed

3 files changed

Lines changed: 96 additions & 23 deletions

File tree

backend/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/pages/ScanDetail.jsx

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -824,22 +824,24 @@ function ScanRunSettings({ scan, onSave, references, referencesLoading, referenc
824824
const [saving, setSaving] = useState(false);
825825
const [error, setError] = useState(null);
826826
const current = runSettingsDraft(scan);
827-
const activeDraft = draft || current;
827+
const activeDraft = mergeRunSettingsDraft(current, draft);
828828
const payload = draft ? runSettingsPayload(draft, current) : {};
829829
const dirty = Object.keys(payload).length > 0;
830-
const jobLimitValid =
831-
!activeDraft.job_limit.trim() ||
832-
(/^\d+$/.test(activeDraft.job_limit.trim()) &&
833-
Number(activeDraft.job_limit) >= 1 &&
834-
Number(activeDraft.job_limit) <= 1_000_000);
830+
const jobLimit = activeDraft.job_limit.trim();
831+
const jobLimitValid = !jobLimit || (/^\d+$/.test(jobLimit) && Number(jobLimit) >= 1 && Number(jobLimit) <= 1_000_000);
835832
const valid =
836833
jobLimitValid && !!references && modelConfigurationIsValid(activeDraft, references.providers, references.catalog);
837834
useUnsavedChangesPrompt(editing && (dirty || saving));
838835

839836
const open = () => {
840837
const currentDraft = runSettingsDraft(scan);
841838
setDraft(
842-
references ? modelConfigurationForCatalog(currentDraft, references.providers, references.catalog) : currentDraft
839+
references
840+
? mergeRunSettingsDraft(
841+
currentDraft,
842+
modelConfigurationForCatalog(currentDraft, references.providers, references.catalog)
843+
)
844+
: currentDraft
843845
);
844846
setError(null);
845847
setEditing(true);
@@ -950,11 +952,7 @@ function ScanRunSettings({ scan, onSave, references, referencesLoading, referenc
950952
<ModelConfiguration
951953
value={activeDraft}
952954
onChange={(nextDraft) =>
953-
setDraft((currentDraft) => ({
954-
...currentDraft,
955-
...nextDraft,
956-
job_limit: nextDraft.job_limit ?? currentDraft.job_limit,
957-
}))
955+
setDraft((currentDraft) => mergeRunSettingsDraft(currentDraft || current, nextDraft))
958956
}
959957
providers={references?.providers || []}
960958
catalog={references?.catalog || {}}
@@ -1043,7 +1041,7 @@ function ScanRunSettings({ scan, onSave, references, referencesLoading, referenc
10431041
);
10441042
}
10451043

1046-
function runSettingsDraft(scan) {
1044+
export function runSettingsDraft(scan = {}) {
10471045
return {
10481046
model: scan.model || '',
10491047
model_provider: scan.modelProvider || 'openrouter',
@@ -1053,14 +1051,42 @@ function runSettingsDraft(scan) {
10531051
};
10541052
}
10551053

1056-
function runSettingsPayload(draft, current) {
1054+
function runSettingsValue(value, fallback) {
1055+
if (value === undefined) return fallback;
1056+
if (value === null) return '';
1057+
return typeof value === 'string' ? value : String(value);
1058+
}
1059+
1060+
export function mergeRunSettingsDraft(current = {}, patch = {}) {
1061+
const base = {
1062+
model: runSettingsValue(current?.model, ''),
1063+
model_provider: runSettingsValue(current?.model_provider, 'openrouter'),
1064+
thinking_effort: runSettingsValue(current?.thinking_effort, 'medium'),
1065+
harness: runSettingsValue(current?.harness, 'codex'),
1066+
job_limit: runSettingsValue(current?.job_limit, ''),
1067+
};
1068+
return {
1069+
model: runSettingsValue(patch?.model, base.model),
1070+
model_provider: runSettingsValue(patch?.model_provider, base.model_provider),
1071+
thinking_effort: runSettingsValue(patch?.thinking_effort, base.thinking_effort),
1072+
harness: runSettingsValue(patch?.harness, base.harness),
1073+
job_limit: runSettingsValue(patch?.job_limit, base.job_limit),
1074+
};
1075+
}
1076+
1077+
export function runSettingsPayload(draft, current) {
1078+
const normalizedCurrent = mergeRunSettingsDraft({}, current);
1079+
const normalizedDraft = mergeRunSettingsDraft(normalizedCurrent, draft);
10571080
const payload = {};
1058-
const model = draft.model.trim();
1059-
if (model !== current.model) payload.model = model;
1060-
if (draft.model_provider !== current.model_provider) payload.model_provider = draft.model_provider;
1061-
if (draft.thinking_effort !== current.thinking_effort) payload.thinking_effort = draft.thinking_effort;
1062-
if (draft.harness !== current.harness) payload.harness = draft.harness;
1063-
if (draft.job_limit !== current.job_limit) payload.jobLimit = draft.job_limit.trim() ? Number(draft.job_limit) : null;
1081+
const model = normalizedDraft.model.trim();
1082+
const jobLimit = normalizedDraft.job_limit.trim();
1083+
if (model !== normalizedCurrent.model) payload.model = model;
1084+
if (normalizedDraft.model_provider !== normalizedCurrent.model_provider)
1085+
payload.model_provider = normalizedDraft.model_provider;
1086+
if (normalizedDraft.thinking_effort !== normalizedCurrent.thinking_effort)
1087+
payload.thinking_effort = normalizedDraft.thinking_effort;
1088+
if (normalizedDraft.harness !== normalizedCurrent.harness) payload.harness = normalizedDraft.harness;
1089+
if (normalizedDraft.job_limit !== normalizedCurrent.job_limit) payload.jobLimit = jobLimit ? Number(jobLimit) : null;
10641090
return payload;
10651091
}
10661092

frontend/src/pages/ScanDetail.test.jsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,54 @@ import { createElement } from 'react';
33
import { renderToStaticMarkup } from 'react-dom/server';
44
import { MemoryRouter } from 'react-router-dom';
55

6-
import { scanActions, ScanStatusPanel } from './ScanDetail.jsx';
6+
import {
7+
mergeRunSettingsDraft,
8+
runSettingsDraft,
9+
runSettingsPayload,
10+
scanActions,
11+
ScanStatusPanel,
12+
} from './ScanDetail.jsx';
13+
14+
describe('scan run settings', () => {
15+
const current = {
16+
model: 'gpt-5-codex',
17+
model_provider: 'codex',
18+
thinking_effort: 'medium',
19+
harness: 'codex',
20+
job_limit: '250',
21+
};
22+
23+
it('preserves the job limit when catalog normalization returns only model fields', () => {
24+
const catalogDraft = {
25+
model: 'gpt-5-codex',
26+
model_provider: 'codex',
27+
thinking_effort: 'medium',
28+
harness: 'codex',
29+
};
30+
31+
expect(mergeRunSettingsDraft(current, catalogDraft)).toEqual(current);
32+
expect(runSettingsPayload(catalogDraft, current)).toEqual({});
33+
});
34+
35+
it('normalizes older scan records into complete string-valued drafts', () => {
36+
expect(runSettingsDraft({ model: 'legacy-model' })).toEqual({
37+
model: 'legacy-model',
38+
model_provider: 'openrouter',
39+
thinking_effort: 'medium',
40+
harness: 'codex',
41+
job_limit: '',
42+
});
43+
});
44+
45+
it('treats fields missing from a partial draft as unchanged', () => {
46+
expect(runSettingsPayload({ model: ' replacement-model ' }, current)).toEqual({ model: 'replacement-model' });
47+
});
48+
49+
it('still supports setting and clearing a job limit', () => {
50+
expect(runSettingsPayload({ job_limit: ' 25 ' }, { ...current, job_limit: '' })).toEqual({ jobLimit: 25 });
51+
expect(runSettingsPayload({ job_limit: '' }, current)).toEqual({ jobLimit: null });
52+
});
53+
});
754

855
describe('scan lifecycle actions', () => {
956
it('offers stop controls without allowing active deletion', () => {

0 commit comments

Comments
 (0)