diff --git a/.gitattributes b/.gitattributes index 147b6c96..205d162b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,6 @@ # anything nested. Together they exclude all 3 bundles from language stats. backend/mcp-bundles/* linguist-vendored backend/mcp-bundles/**/* linguist-vendored + +# Source-pin tests read files and match on \n; a Windows checkout with autocrlf turned every one red. LF everywhere, on every OS. +* text=auto eol=lf diff --git a/.github/workflows/suites-matrix.yml b/.github/workflows/suites-matrix.yml index 7050001d..4a42050a 100644 --- a/.github/workflows/suites-matrix.yml +++ b/.github/workflows/suites-matrix.yml @@ -11,7 +11,8 @@ on: concurrency: group: suites-matrix-${{ github.ref }} - cancel-in-progress: true + # A run in flight keeps its verdicts; a push mid-run queues behind it instead of cancelling the Mac legs. + cancel-in-progress: false permissions: contents: read @@ -27,6 +28,8 @@ jobs: env: OSW_NEVER_KILL_ROUTER: '1' OSW_DISABLE_AUTO_RESUME: '1' + # The packaged app spawns its backend with PYTHONUTF8=1 (electron/main.js); the suite runs the way the product runs, or Windows' cp1252 default fails collection on the first source read. + PYTHONUTF8: '1' CSC_IDENTITY_AUTO_DISCOVERY: 'false' steps: - uses: actions/checkout@v4 diff --git a/electron/affiliateTracking.test.js b/electron/affiliateTracking.test.js index cd809311..fd22d96c 100644 --- a/electron/affiliateTracking.test.js +++ b/electron/affiliateTracking.test.js @@ -630,9 +630,13 @@ test("poll loop respects max attempts and gives up", async () => { isPackaged: true, }); - // Wait long enough for all attempts to fail. 20ms × 30 = 600ms. - await delay(900); - const state = readJson(path.join(userDataDir, "install.json")); + // Wait for the loop to record its last attempt, not for a clock: a fixed 900ms saw 29 of 30 on a loaded Mac. + let state = {}; + for (let i = 0; i < 200; i++) { + state = readJson(path.join(userDataDir, "install.json")); + if (state.attempts === 30) break; + await delay(50); + } assert.equal(state.ref, null, "no ref after exhausted polls"); assert.equal(state.attempts, 30, "all attempts recorded"); }); diff --git a/electron/backendSpawnUtf8.test.js b/electron/backendSpawnUtf8.test.js new file mode 100644 index 00000000..7cf66261 --- /dev/null +++ b/electron/backendSpawnUtf8.test.js @@ -0,0 +1,13 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +// 46 reads in backend/apps open text files without naming an encoding; on Windows the default is +// cp1252 and the first non-Latin byte raises. The packaged spawn declares UTF-8, and the CI suites +// run under the same declaration (suites-matrix.yml), so the two must not drift apart. +test('every backend spawn in main.js declares PYTHONUTF8=1', () => { + const main = fs.readFileSync(path.join(__dirname, 'main.js'), 'utf8'); + const spawns = main.match(/PYTHONUTF8: '1'/g) || []; + assert.ok(spawns.length >= 2, `expected the dev and packaged spawn envs to carry PYTHONUTF8, found ${spawns.length}`); +}); diff --git a/frontend/scripts/run-tests.mjs b/frontend/scripts/run-tests.mjs index 559d7d62..e067d684 100644 --- a/frontend/scripts/run-tests.mjs +++ b/frontend/scripts/run-tests.mjs @@ -10,6 +10,7 @@ import { globSync } from 'node:fs'; import { mkdirSync, rmSync } from 'node:fs'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; +import { pathToFileURL } from 'node:url'; import { fileURLToPath } from 'node:url'; const root = path.dirname(fileURLToPath(import.meta.url)) + '/..'; @@ -47,7 +48,8 @@ try { }); const built = globSync('**/*.mjs', { cwd: outDir }).concat(globSync('**/*.js', { cwd: outDir })); const setup = path.join(root, 'scripts/test-globals.mjs'); - const res = spawnSync(process.execPath, ['--import', setup, '--test', ...built.map((f) => path.join(outDir, f))], + // --import takes a URL: a bare Windows path (D:\...) is read as protocol 'd:' and every test file fails before it starts. + const res = spawnSync(process.execPath, ['--import', pathToFileURL(setup).href, '--test', ...built.map((f) => path.join(outDir, f))], { stdio: 'inherit', cwd: root }); status = res.status ?? 1; } finally {