diff --git a/scripts/test-ai-model-picker.cjs b/scripts/test-ai-model-picker.cjs index 49916f0..b099305 100644 --- a/scripts/test-ai-model-picker.cjs +++ b/scripts/test-ai-model-picker.cjs @@ -6,7 +6,7 @@ const ts = require('typescript'); const React = require('react'); const source = ts.createSourceFile('SettingsPage.tsx', fs.readFileSync(require.resolve('../src/pages/settings/SettingsPage.tsx'), 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); -const constants = source.statements.filter(node => ts.isVariableStatement(node) && ['AI_MODEL_DEFAULTS', 'AI_MODEL_OPTIONS', 'CUSTOM_AI_MODEL'].includes(node.declarationList.declarations[0].name.getText(source))); +const constants = source.statements.filter(node => ts.isVariableStatement(node) && ['AI_MODEL_DEFAULTS', 'AI_MODEL_OPTIONS', 'CUSTOM_AI_MODEL', 'AI_PROVIDER_LABELS'].includes(node.declarationList.declarations[0].name.getText(source))); function compile(code, globals = {}) { const exports = {}; vm.runInNewContext(ts.transpileModule(code, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX } }).outputText, { exports, require, ...globals }); @@ -19,11 +19,11 @@ function find(node, predicate) { return ts.forEachChild(node, child => find(child, predicate)); } const section = find(source, node => ts.isJsxElement(node) && node.openingElement.tagName.getText(source) === 'section' && node.openingElement.attributes.getText(source).includes('border-t border-[#e2e8f0] pt-5') && node.getText(source).includes('AI 공급자')); -function controls(provider, model, custom = false) { +function controls(provider, model, custom = false, configured = true) { const state = { aiProvider: provider, aiModel: model, aiCustomModel: custom }; const context = { aiSettingLoading: false, saving: false, aiEndpointUrl: 'https://example.test', aiApiKey: '', aiHasApiKey: false, aiConfiguredProvider: null, - AI_PROVIDER_LABELS: { OPENAI: 'OpenAI', ANTHROPIC: 'Claude', GEMINI: 'Gemini', QWEN: 'Qwen' }, + aiCopilotConfigured: configured, DEFAULT_QWEN_ENDPOINT: 'https://example.test', KeyRound: () => null, setAiProvider(value) { state.aiProvider = value; }, setAiModel(value) { state.aiModel = value; }, setAiCustomModel(value) { state.aiCustomModel = value; }, setAiApiKey() {}, setAiEndpointUrl() {}, @@ -37,7 +37,7 @@ function elements(root, predicate) { } for (const provider of Object.keys(AI_MODEL_OPTIONS)) { - test(`${provider} offers current models and a custom model field without changing defaults`, () => { + test(`${provider} offers current models and a custom model field`, () => { const h = controls(provider, AI_MODEL_DEFAULTS[provider]); let tree = h.render(); const select = elements(tree, node => node.type === 'select')[0]; @@ -60,6 +60,18 @@ for (const provider of Object.keys(AI_MODEL_OPTIONS)) { }); } +test('provider defaults select the highest-tier current models at the top of each list', () => { + const expected = { OPENAI: 'gpt-6-astra', ANTHROPIC: 'claude-fable-5-1', GEMINI: 'gemini-4-argon', QWEN: 'qwen3.8-max' }; + for (const [provider, model] of Object.entries(expected)) { + assert.equal(AI_MODEL_DEFAULTS[provider], model); + assert.equal(AI_MODEL_OPTIONS[provider][0].value, model); + const h = controls('QWEN', 'qwen3.7-plus'); + if (provider === 'QWEN') h.state.aiProvider = 'OPENAI'; + elements(h.render(), node => node.props.role === 'radio' && node.props.children === ({ OPENAI: 'OpenAI', ANTHROPIC: 'Claude', GEMINI: 'Gemini', QWEN: 'Qwen' })[provider])[0].props.onClick(); + assert.equal(h.state.aiModel, model); + } +}); + test('saved unknown model IDs remain visible in custom mode', () => { const model = 'provider/private-model:revision-10'; const h = controls('QWEN', model, true); @@ -72,15 +84,26 @@ test('saved unknown model IDs remain visible in custom mode', () => { test('the selected provider button preserves a custom draft', () => { const h = controls('OPENAI', 'custom-gpt', true); const providers = elements(h.render(), node => node.props.role === 'radio'); - providers[0].props.onClick(); + providers.find(node => node.props.children === 'OpenAI').props.onClick(); assert.equal(h.state.aiModel, 'custom-gpt'); assert.equal(h.state.aiCustomModel, true); - providers[1].props.onClick(); + providers.find(node => node.props.children === 'Claude').props.onClick(); assert.equal(h.state.aiProvider, 'ANTHROPIC'); assert.equal(h.state.aiModel, AI_MODEL_DEFAULTS.ANTHROPIC); assert.equal(h.state.aiCustomModel, false); }); +test('A-Copilot is first and exposes only fixed read-only fields without a model picker', () => { + const tree = controls('A_COPILOT', 'a-copilot').render(); + assert.equal(elements(tree, node => node.props.role === 'radio')[0].props.children, 'A-Copilot'); + assert.equal(elements(tree, node => node.type === 'select').length, 0); + const inputs = elements(tree, node => node.type === 'input'); + assert.deepEqual(inputs.map(node => node.props.value), ['A-Copilot', '서버에서 관리']); + assert.ok(inputs.every(node => node.props.readOnly && !node.props.onChange)); + assert.equal(AI_MODEL_DEFAULTS.A_COPILOT, 'a-copilot'); + assert.equal(elements(controls('A_COPILOT', 'a-copilot', false, false).render(), node => node.props.role === 'alert').length, 1); +}); + test('official current API IDs are present', () => { for (const [provider, ids] of Object.entries({ OPENAI: ['gpt-6.1-sol', 'gpt-6-astra', 'gpt-6-luna'], diff --git a/scripts/test-settings-navigation.cjs b/scripts/test-settings-navigation.cjs index 6899808..382cde0 100644 --- a/scripts/test-settings-navigation.cjs +++ b/scripts/test-settings-navigation.cjs @@ -24,7 +24,6 @@ const props = { session: { accessToken: 'test-token', user: { id: 'teacher' } }, selectedCourseId: 'course-a', courses: [{ id: 'course-a', title: 'A' }, { id: 'course-b', title: 'B' }], - onSelectCourse() {}, }; test('settings remounts the credential draft when course or session changes', () => { @@ -35,21 +34,20 @@ test('settings remounts the credential draft when course or session changes', () assert.notEqual(initial.key, form({ ...props, session: { ...props.session, accessToken: 'new-token' } }).key); }); -test('settings offers the global course selection and no credential form without a course', () => { - const selected = []; - const tree = SettingsPage({ ...props, onSelectCourse: id => selected.push(id) }); +test('settings is width-constrained and centered without a duplicate course selector', () => { + const tree = SettingsPage(props); + assert.match(tree.props.className, /mx-auto.*w-full.*max-w-\[920px\]/); const header = React.Children.toArray(tree.props.children)[0]; - const select = elements(header.props.action, node => node.type === 'select')[0]; - assert.equal(select.props.value, 'course-a'); - select.props.onChange({ target: { value: 'course-b' } }); - assert.deepEqual(selected, ['course-b']); + assert.equal(header.props.action, undefined); + assert.equal(elements(tree, node => node.type === 'select').length, 0); assert.equal(elements(SettingsPage({ ...props, selectedCourseId: '', courses: [] }), node => node.type === CourseAiSettings).length, 0); }); test('curriculum no longer loads AI settings or offers an agent tab', () => { const curriculum = read('pages/curriculum/CurriculumPage.tsx'); assert.doesNotMatch(curriculum, /getCourseAiSetting|updateCourseAiSetting|handleSaveAiSetting|setActiveTab\('ai'\)/); - assert.match(read('App.tsx'), //); + assert.match(read('App.tsx'), /