|
9 | 9 | createConfigStore, |
10 | 10 | DEFAULT_ORIGIN, |
11 | 11 | isSafeInternalPath, |
| 12 | + isSimCloudOrigin, |
12 | 13 | partitionForOrigin, |
13 | 14 | validateOriginInput, |
14 | 15 | } from '@/main/config' |
@@ -145,6 +146,26 @@ describe('createConfigStore', () => { |
145 | 146 | expect(reloaded.getOrigin()).toBe('https://self-hosted.example') |
146 | 147 | }) |
147 | 148 |
|
| 149 | + // setOrigin writes the whole settings file synchronously on the main thread, |
| 150 | + // and re-confirming the URL already in the field is the common case in the |
| 151 | + // server picker. |
| 152 | + it('does not rewrite settings when setOrigin is given the stored origin', () => { |
| 153 | + const filePath = tempSettingsPath() |
| 154 | + const store = createConfigStore(filePath, {}) |
| 155 | + store.setOrigin('https://self-hosted.example') |
| 156 | + // A sentinel only this test could have written. A rewrite serializes the |
| 157 | + // in-memory settings over it, so its survival proves no write happened — |
| 158 | + // unlike an mtime comparison, which two writes a fraction of a millisecond |
| 159 | + // apart can pass by accident. |
| 160 | + writeFileSync(filePath, `${readFileSync(filePath, 'utf8')}\n// sentinel\n`) |
| 161 | + |
| 162 | + expect(store.setOrigin('https://self-hosted.example')).toEqual({ |
| 163 | + ok: true, |
| 164 | + origin: 'https://self-hosted.example', |
| 165 | + }) |
| 166 | + expect(readFileSync(filePath, 'utf8')).toContain('// sentinel') |
| 167 | + }) |
| 168 | + |
148 | 169 | it('canonicalizes the apex production origin on setOrigin, not just on load', () => { |
149 | 170 | // Entering https://sim.ai mid-session must not persist the apex: the |
150 | 171 | // running session would use the wrong cookie partition and misclassify |
@@ -223,6 +244,25 @@ describe('createConfigStore', () => { |
223 | 244 | }) |
224 | 245 | }) |
225 | 246 |
|
| 247 | +describe('isSimCloudOrigin', () => { |
| 248 | + it('recognizes Sim-operated origins and nothing else', () => { |
| 249 | + for (const origin of ['https://sim.ai', 'https://www.sim.ai', 'https://www.staging.sim.ai']) { |
| 250 | + expect(isSimCloudOrigin(origin)).toBe(true) |
| 251 | + } |
| 252 | + // A lookalike host must not pass — the suffix check is on the parsed |
| 253 | + // hostname, never a prefix or substring of the raw string. |
| 254 | + for (const origin of [ |
| 255 | + 'https://sim.example.com', |
| 256 | + 'https://sim.ai.evil.example', |
| 257 | + 'https://notsim.ai', |
| 258 | + 'http://localhost:3000', |
| 259 | + 'not a url', |
| 260 | + ]) { |
| 261 | + expect(isSimCloudOrigin(origin)).toBe(false) |
| 262 | + } |
| 263 | + }) |
| 264 | +}) |
| 265 | + |
226 | 266 | describe('channelForOrigin', () => { |
227 | 267 | it('maps each environment origin to its channel', () => { |
228 | 268 | expect(channelForOrigin('https://sim.ai')).toBe('prod') |
|
0 commit comments