Skip to content

Commit 9ed7fa8

Browse files
Add and refine high-value Node unit tests for core guards
Expand Node unit coverage for config predicates, token-retention boundaries, port-error handling, and utility guard edge cases. Refine existing util tests so boundary intent is explicit and easier to review, while preserving current runtime behavior.
1 parent ab628e1 commit 9ed7fa8

9 files changed

+860
-5
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import assert from 'node:assert/strict'
2+
import { afterEach, test } from 'node:test'
3+
import {
4+
getNavigatorLanguage,
5+
isUsingBingWebModel,
6+
isUsingChatgptApiModel,
7+
isUsingCustomModel,
8+
isUsingMultiModeModel,
9+
isUsingOpenAiApiModel,
10+
} from '../../../src/config/index.mjs'
11+
12+
const originalNavigatorDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'navigator')
13+
14+
const restoreNavigator = () => {
15+
if (originalNavigatorDescriptor) {
16+
Object.defineProperty(globalThis, 'navigator', originalNavigatorDescriptor)
17+
} else {
18+
delete globalThis.navigator
19+
}
20+
}
21+
22+
const setNavigatorLanguage = (language) => {
23+
Object.defineProperty(globalThis, 'navigator', {
24+
value: { language },
25+
configurable: true,
26+
})
27+
}
28+
29+
afterEach(() => {
30+
restoreNavigator()
31+
})
32+
33+
test('getNavigatorLanguage returns zhHant for zh-TW style locales', () => {
34+
setNavigatorLanguage('zh-TW')
35+
assert.equal(getNavigatorLanguage(), 'zhHant')
36+
})
37+
38+
test('getNavigatorLanguage returns first two letters for non-zhHant locales', () => {
39+
setNavigatorLanguage('en-US')
40+
assert.equal(getNavigatorLanguage(), 'en')
41+
})
42+
43+
test('getNavigatorLanguage normalizes mixed-case zh-TW locale to zhHant', () => {
44+
setNavigatorLanguage('ZH-TW')
45+
assert.equal(getNavigatorLanguage(), 'zhHant')
46+
})
47+
48+
test('getNavigatorLanguage treats zh-Hant locale as zhHant', () => {
49+
setNavigatorLanguage('zh-Hant')
50+
assert.equal(getNavigatorLanguage(), 'zhHant')
51+
})
52+
53+
test('isUsingChatgptApiModel detects chatgpt API models and excludes custom model', () => {
54+
assert.equal(isUsingChatgptApiModel({ modelName: 'chatgptApi4oMini' }), true)
55+
assert.equal(isUsingChatgptApiModel({ modelName: 'customModel' }), false)
56+
})
57+
58+
test('isUsingOpenAiApiModel accepts both chat and completion API model groups', () => {
59+
assert.equal(isUsingOpenAiApiModel({ modelName: 'chatgptApi4oMini' }), true)
60+
assert.equal(isUsingOpenAiApiModel({ modelName: 'gptApiInstruct' }), true)
61+
})
62+
63+
test('isUsingOpenAiApiModel excludes custom model', () => {
64+
assert.equal(isUsingOpenAiApiModel({ modelName: 'customModel' }), false)
65+
})
66+
67+
test('isUsingCustomModel works with modelName and apiMode forms', () => {
68+
assert.equal(isUsingCustomModel({ modelName: 'customModel' }), true)
69+
70+
const apiMode = {
71+
groupName: 'customApiModelKeys',
72+
itemName: 'customModel',
73+
isCustom: true,
74+
customName: 'my-custom-model',
75+
customUrl: '',
76+
apiKey: '',
77+
active: true,
78+
}
79+
assert.equal(isUsingCustomModel({ apiMode }), true)
80+
})
81+
82+
test('isUsingMultiModeModel currently follows Bing web group behavior', () => {
83+
assert.equal(isUsingBingWebModel({ modelName: 'bingFree4' }), true)
84+
assert.equal(isUsingMultiModeModel({ modelName: 'bingFree4' }), true)
85+
assert.equal(isUsingBingWebModel({ modelName: 'chatgptFree35' }), false)
86+
assert.equal(isUsingMultiModeModel({ modelName: 'chatgptFree35' }), false)
87+
})
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import assert from 'node:assert/strict'
2+
import { beforeEach, test } from 'node:test'
3+
import { clearOldAccessToken, getUserConfig, setAccessToken } from '../../../src/config/index.mjs'
4+
5+
const THIRTY_DAYS_MS = 30 * 24 * 3600 * 1000
6+
7+
beforeEach(() => {
8+
globalThis.__TEST_BROWSER_SHIM__.clearStorage()
9+
})
10+
11+
test('getUserConfig migrates legacy chat.openai.com URL to chatgpt.com', async () => {
12+
globalThis.__TEST_BROWSER_SHIM__.replaceStorage({
13+
customChatGptWebApiUrl: 'https://chat.openai.com',
14+
})
15+
16+
const config = await getUserConfig()
17+
18+
assert.equal(config.customChatGptWebApiUrl, 'https://chatgpt.com')
19+
})
20+
21+
test('getUserConfig keeps modern chatgpt.com URL unchanged', async () => {
22+
globalThis.__TEST_BROWSER_SHIM__.replaceStorage({
23+
customChatGptWebApiUrl: 'https://chatgpt.com',
24+
})
25+
26+
const config = await getUserConfig()
27+
28+
assert.equal(config.customChatGptWebApiUrl, 'https://chatgpt.com')
29+
})
30+
31+
test('clearOldAccessToken clears expired token older than 30 days', async (t) => {
32+
const now = 1_700_000_000_000
33+
t.mock.method(Date, 'now', () => now)
34+
35+
globalThis.__TEST_BROWSER_SHIM__.replaceStorage({
36+
accessToken: 'stale-token',
37+
tokenSavedOn: now - THIRTY_DAYS_MS - 1_000,
38+
})
39+
40+
await clearOldAccessToken()
41+
42+
const storage = globalThis.__TEST_BROWSER_SHIM__.getStorage()
43+
assert.equal(storage.accessToken, '')
44+
// tokenSavedOn write behavior is covered in the dedicated setAccessToken test below.
45+
})
46+
47+
test('setAccessToken updates tokenSavedOn to Date.now', async (t) => {
48+
const now = 1_700_000_000_000
49+
t.mock.method(Date, 'now', () => now)
50+
51+
await setAccessToken('new-token')
52+
53+
const storage = globalThis.__TEST_BROWSER_SHIM__.getStorage()
54+
assert.equal(storage.accessToken, 'new-token')
55+
assert.equal(storage.tokenSavedOn, now)
56+
})
57+
58+
test('clearOldAccessToken keeps recent token within 30 days', async (t) => {
59+
const now = 1_700_000_000_000
60+
t.mock.method(Date, 'now', () => now)
61+
const recentSavedOn = now - THIRTY_DAYS_MS + 1_000
62+
63+
globalThis.__TEST_BROWSER_SHIM__.replaceStorage({
64+
accessToken: 'fresh-token',
65+
tokenSavedOn: recentSavedOn,
66+
})
67+
68+
await clearOldAccessToken()
69+
70+
const storage = globalThis.__TEST_BROWSER_SHIM__.getStorage()
71+
assert.equal(storage.accessToken, 'fresh-token')
72+
assert.equal(storage.tokenSavedOn, recentSavedOn)
73+
})
74+
75+
test('clearOldAccessToken keeps token when exactly 30 days old', async (t) => {
76+
const now = 1_700_000_000_000
77+
t.mock.method(Date, 'now', () => now)
78+
const savedOn = now - THIRTY_DAYS_MS
79+
80+
globalThis.__TEST_BROWSER_SHIM__.replaceStorage({
81+
accessToken: 'boundary-token',
82+
tokenSavedOn: savedOn,
83+
})
84+
85+
await clearOldAccessToken()
86+
87+
const storage = globalThis.__TEST_BROWSER_SHIM__.getStorage()
88+
assert.equal(storage.accessToken, 'boundary-token')
89+
assert.equal(storage.tokenSavedOn, savedOn)
90+
})

tests/unit/services/apis/openai-api-compat.test.mjs

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,210 @@ test('generateAnswersWithChatgptApiCompat sends expected request and aggregates
7979
assert.deepEqual(session.conversationRecords.at(-1), { question: 'CurrentQ', answer: 'Hello' })
8080
})
8181

82+
test('generateAnswersWithChatgptApiCompat uses max_completion_tokens for OpenAI gpt-5 models', async (t) => {
83+
t.mock.method(console, 'debug', () => {})
84+
setStorage({
85+
maxConversationContextLength: 3,
86+
maxResponseTokenLength: 321,
87+
temperature: 0.2,
88+
})
89+
90+
const session = {
91+
modelName: 'chatgptApi-gpt-5.1-chat-latest',
92+
conversationRecords: [],
93+
isRetry: false,
94+
}
95+
const port = createFakePort()
96+
97+
let capturedInit
98+
t.mock.method(globalThis, 'fetch', async (_input, init) => {
99+
capturedInit = init
100+
return createMockSseResponse([
101+
'data: {"choices":[{"delta":{"content":"OK"},"finish_reason":"stop"}]}\n\n',
102+
])
103+
})
104+
105+
await generateAnswersWithChatgptApiCompat(
106+
'https://api.example.com/v1',
107+
port,
108+
'CurrentQ',
109+
session,
110+
'sk-test',
111+
{},
112+
'openai',
113+
)
114+
115+
const body = JSON.parse(capturedInit.body)
116+
assert.equal(body.max_completion_tokens, 321)
117+
assert.equal(Object.hasOwn(body, 'max_tokens'), false)
118+
})
119+
120+
test('generateAnswersWithChatgptApiCompat removes conflicting token key from extraBody', async (t) => {
121+
t.mock.method(console, 'debug', () => {})
122+
setStorage({
123+
maxConversationContextLength: 3,
124+
maxResponseTokenLength: 222,
125+
temperature: 0.2,
126+
})
127+
128+
const session = {
129+
modelName: 'chatgptApi4oMini',
130+
conversationRecords: [],
131+
isRetry: false,
132+
}
133+
const port = createFakePort()
134+
135+
let capturedInit
136+
t.mock.method(globalThis, 'fetch', async (_input, init) => {
137+
capturedInit = init
138+
return createMockSseResponse([
139+
'data: {"choices":[{"delta":{"content":"OK"},"finish_reason":"stop"}]}\n\n',
140+
])
141+
})
142+
143+
await generateAnswersWithChatgptApiCompat(
144+
'https://api.example.com/v1',
145+
port,
146+
'CurrentQ',
147+
session,
148+
'sk-test',
149+
{
150+
max_completion_tokens: 999,
151+
top_p: 0.9,
152+
},
153+
)
154+
155+
const body = JSON.parse(capturedInit.body)
156+
assert.equal(body.max_tokens, 222)
157+
assert.equal(Object.hasOwn(body, 'max_completion_tokens'), false)
158+
assert.equal(body.top_p, 0.9)
159+
})
160+
161+
test('generateAnswersWithChatgptApiCompat removes max_tokens from extraBody for OpenAI gpt-5 models', async (t) => {
162+
t.mock.method(console, 'debug', () => {})
163+
setStorage({
164+
maxConversationContextLength: 3,
165+
maxResponseTokenLength: 500,
166+
temperature: 0.2,
167+
})
168+
169+
const session = {
170+
modelName: 'chatgptApi-gpt-5.1-chat-latest',
171+
conversationRecords: [],
172+
isRetry: false,
173+
}
174+
const port = createFakePort()
175+
176+
let capturedInit
177+
t.mock.method(globalThis, 'fetch', async (_input, init) => {
178+
capturedInit = init
179+
return createMockSseResponse([
180+
'data: {"choices":[{"delta":{"content":"OK"},"finish_reason":"stop"}]}\n\n',
181+
])
182+
})
183+
184+
await generateAnswersWithChatgptApiCompat(
185+
'https://api.example.com/v1',
186+
port,
187+
'CurrentQ',
188+
session,
189+
'sk-test',
190+
{
191+
max_tokens: 999,
192+
top_p: 0.8,
193+
},
194+
'openai',
195+
)
196+
197+
const body = JSON.parse(capturedInit.body)
198+
assert.equal(body.max_completion_tokens, 500)
199+
assert.equal(Object.hasOwn(body, 'max_tokens'), false)
200+
assert.equal(body.top_p, 0.8)
201+
})
202+
203+
test('generateAnswersWithChatgptApiCompat allows max_tokens override for compat provider', async (t) => {
204+
t.mock.method(console, 'debug', () => {})
205+
setStorage({
206+
maxConversationContextLength: 3,
207+
maxResponseTokenLength: 400,
208+
temperature: 0.2,
209+
})
210+
211+
const session = {
212+
modelName: 'chatgptApi4oMini',
213+
conversationRecords: [],
214+
isRetry: false,
215+
}
216+
const port = createFakePort()
217+
218+
let capturedInit
219+
t.mock.method(globalThis, 'fetch', async (_input, init) => {
220+
capturedInit = init
221+
return createMockSseResponse([
222+
'data: {"choices":[{"delta":{"content":"OK"},"finish_reason":"stop"}]}\n\n',
223+
])
224+
})
225+
226+
await generateAnswersWithChatgptApiCompat(
227+
'https://api.example.com/v1',
228+
port,
229+
'CurrentQ',
230+
session,
231+
'sk-test',
232+
{
233+
max_tokens: 333,
234+
top_p: 0.75,
235+
},
236+
)
237+
238+
const body = JSON.parse(capturedInit.body)
239+
assert.equal(body.max_tokens, 333)
240+
assert.equal(Object.hasOwn(body, 'max_completion_tokens'), false)
241+
assert.equal(body.top_p, 0.75)
242+
})
243+
244+
test('generateAnswersWithChatgptApiCompat allows max_completion_tokens override for OpenAI gpt-5 models', async (t) => {
245+
t.mock.method(console, 'debug', () => {})
246+
setStorage({
247+
maxConversationContextLength: 3,
248+
maxResponseTokenLength: 400,
249+
temperature: 0.2,
250+
})
251+
252+
const session = {
253+
modelName: 'chatgptApi-gpt-5.1-chat-latest',
254+
conversationRecords: [],
255+
isRetry: false,
256+
}
257+
const port = createFakePort()
258+
259+
let capturedInit
260+
t.mock.method(globalThis, 'fetch', async (_input, init) => {
261+
capturedInit = init
262+
return createMockSseResponse([
263+
'data: {"choices":[{"delta":{"content":"OK"},"finish_reason":"stop"}]}\n\n',
264+
])
265+
})
266+
267+
await generateAnswersWithChatgptApiCompat(
268+
'https://api.example.com/v1',
269+
port,
270+
'CurrentQ',
271+
session,
272+
'sk-test',
273+
{
274+
max_completion_tokens: 333,
275+
top_p: 0.65,
276+
},
277+
'openai',
278+
)
279+
280+
const body = JSON.parse(capturedInit.body)
281+
assert.equal(body.max_completion_tokens, 333)
282+
assert.equal(Object.hasOwn(body, 'max_tokens'), false)
283+
assert.equal(body.top_p, 0.65)
284+
})
285+
82286
test('generateAnswersWithChatgptApiCompat throws on non-ok response with JSON error body', async (t) => {
83287
t.mock.method(console, 'debug', () => {})
84288
setStorage({

0 commit comments

Comments
 (0)