fix: stabilize Windows CI integration tests - #10940
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces several test stability improvements, including Windows-specific process teardown using taskkill, increased timeouts, parallelized test execution, and skipping MOTD fetching in CI environments. Feedback on these changes highlights a potential race condition on Windows where the process teardown promise is resolved too early, implicit any type violations on the this parameter in test suites, and a redundant process.env.CI check before calling fetchMOTD().
| }); | ||
|
|
||
| describe("apps:list", () => { | ||
| describe("apps:list", function (this) { |
There was a problem hiding this comment.
In TypeScript, the special this parameter must have an explicit type annotation (e.g., this: Mocha.Suite). Leaving it as function (this) results in an implicit any type, which violates the repository style guide's rule against using any as an escape hatch.
describe("apps:list", function (this: Mocha.Suite) {References
- Never use
anyorunknownas an escape hatch. Define proper interfaces/types or use type guards. (link)
There was a problem hiding this comment.
Done! Added explicit type annotation.
| }); | ||
|
|
||
| describe("apps:sdkconfig", () => { | ||
| describe("apps:sdkconfig", function (this) { |
There was a problem hiding this comment.
In TypeScript, the special this parameter must have an explicit type annotation (e.g., this: Mocha.Suite). Leaving it as function (this) results in an implicit any type, which violates the repository style guide's rule against using any as an escape hatch.
describe("apps:sdkconfig", function (this: Mocha.Suite) {References
- Never use
anyorunknownas an escape hatch. Define proper interfaces/types or use type guards. (link)
There was a problem hiding this comment.
Done! Added explicit type annotation.
| if (!process.env.CI) { | ||
| fetchMOTD(); | ||
| } |
|
/joe-review |
…l, and fix debug log step
…emulator/triggers tests
…ean-install cleanup
…r in pnpm fixture
…--no-audit to npm ci
…ock during npm ci
| const lowerParent = resolvedParent.toLowerCase(); | ||
| const lowerChild = resolvedChild.toLowerCase(); | ||
| return ( | ||
| (lowerChild.startsWith(lowerParent + path.sep) || lowerChild.startsWith(lowerParent)) && |
There was a problem hiding this comment.
🔴 [Security] Zip Slip Vulnerability Bypass (Windows)
Rationale: The current logic (lowerChild.startsWith(lowerParent + path.sep) || lowerChild.startsWith(lowerParent)) will return true if the child path starts with the parent path, even if it is not a sub-directory but a sibling with a matching prefix (e.g., C:\foo-bar starts with C:\foo). This allows zip-slip traversal to sibling directories.
Suggested Fix:
Ensure lowerParent ends with a separator before checking startsWith:
const lowerParent = resolvedParent.toLowerCase();
const lowerChild = resolvedChild.toLowerCase();
const parentWithSep = lowerParent.endsWith(path.sep) ? lowerParent : lowerParent + path.sep;
return lowerChild.startsWith(parentWithSep) && lowerChild !== lowerParent;There was a problem hiding this comment.
Ok, that seems valid, will fix.
| p.once("exit", () => resolve()); | ||
| }); | ||
|
|
||
| const timeoutPromise = new Promise<void>((resolve) => setTimeout(resolve, 2000)); |
There was a problem hiding this comment.
🟡 Nit: Uncleared timeout
Rationale: The setTimeout is not cleared if the process exits before the 2-second timeout, which can keep the event loop active.
Suggested Fix:
let timeoutId: NodeJS.Timeout;
const timeoutPromise = new Promise<void>((resolve) => {
timeoutId = setTimeout(resolve, 2000);
});
try {
execSync(`taskkill /pid ${p.pid} /T /F`);
} catch {
// ignore if process already exited
}
return Promise.race([exitPromise, timeoutPromise]).then(() => {
clearTimeout(timeoutId);
this.process = undefined;
});| - node-version: "24.18" | ||
| script: "npm run test:functions-discover" | ||
| steps: | ||
| - name: Disable Windows Defender for workspace |
There was a problem hiding this comment.
Windows Defender sounds like it could be important. Should you add a comment on why this is necessary / good to do?
There was a problem hiding this comment.
Reasonable - will add a brief explanation here
| echo "${DATE}" > "public/${TARGET_FILE}" | ||
| echo "Initialized temp directory." | ||
|
|
||
| function kill_port() { |
There was a problem hiding this comment.
(Also from AI)
These commands match both local and foreign ports. If the test runner (or any other process in the CI VM) has an active client connection to the emulator port, the client process's ephemeral port info will also match the grep or lsof query.
For example, if the emulator is on port 8085 and the test runner connects to it from local port 54321:
netstat will show a line with 127.0.0.1:54321 -> 127.0.0.1:8085 (ESTABLISHED).
Because :8085 appears in the foreign address, grep ":8085 " will match this line and extract the PID of the client (the test runner).
The script will then execute taskkill on the test runner itself, causing the CI job to crash or fail mysteriously.
Recommended Fix:
Restrict the search to only processes listening on the target port.
For POSIX (lsof): Filter by TCP:LISTEN state:
# Before
local pids=$(lsof -t -i:"$PORT_NUM" 2>/dev/null || true)
# After (Safer)
local pids=$(lsof -t -sTCP:LISTEN -i:"$PORT_NUM" 2>/dev/null || true)
For Windows (netstat): Use awk to ensure the port matches the Local Address (2nd column) and the state is LISTENING.
# Before
local pids=$(netstat -ano | grep ":$PORT_NUM " | awk '{print $5}' | sort -u || true)
# After (Safer)
local pids=$(netstat -ano | awk -v port=":$PORT_NUM" '$2 ~ port"$" && $4 == "LISTENING" {print $5}' | sort -u || true)
…, and port filtering
… prevent cross-OS test collision
Description
Stabilizes Windows CI integration tests, addresses review feedback, and improves runner reliability.
Key Changes
src/unzip.ts,src/unzip.spec.ts):parentWithSeptrailing separator check inisChildDirto prevent prefix-matching sibling directories (e.g./tmp/appvs/tmp/app-secret) from bypassing directory traversal protection.win32to prevent casing discrepancies betweenprocess.cwd()and archive entries from triggering false-positive Zip Slip errors.isChildDir.scripts/integration-helpers/cli.ts):CLIProcess.stop()on Windows, executetaskkill /pid ${p.pid} /T /Fand await the processexitevent with a 2-second timeout race fallback.clearTimeoutwhen process exit resolves first.scripts/hosting-tests/run.sh,scripts/triggers-end-to-end-tests/run.sh):LISTENINGstate (lsof -sTCP:LISTENon POSIX and$2 ~ port"$" && $4 == "LISTENING"on Windows) to prevent killing client test runner processes with active connections to emulator ports.${GITHUB_RUN_NUMBER}-${RUNNER_OS}-${RANDOM}) to prevent cross-OS test collisions in matrix CI runs.src/fetchMOTD.ts):fetchMOTD()early whenprocess.env.CIis set to avoid unnecessary network calls and potential timeouts during CI test execution.scripts/hosting-tests/run.sh,scripts/storage-deploy-tests/run.sh):firebase target:apply <service> <target-name> <resource> --project <project>commands instead of writing.firebasercdirectly..github/workflows/node-test.yml):Set-MpPreference -ExclusionPath @("${{ github.workspace }}", "$env:LocalAppData\npm-cache", "$env:AppData\npm")) with explanatory documentation.npm@9.5downgrade step on Node 24 Windows runners to eliminate Arborist deadlock duringnpm ci.--prefer-offline --no-auditacross fixture dependency installations in test runners.scripts/client-integration-tests/tests.ts,run.sh):this: Mocha.Suiteannotations.this.timeout(15 * 1000)andthis.retries(2)toapps:listandapps:sdkconfigsuites.apps.listvariants in parallel usingPromise.all..firebasercdeletion indeployHostingafter hook with try/catch.scripts/client-integration-tests/run.sh.CI Validation Runs
All 6 Windows integration test suites (
client-integration,emulator,hosting,triggers-end-to-end:inspect,functions-discover,storage-deploy) and all 12 Linux suites have been validated with 100% GREEN (53/53 passing checks):b667bc42dclient-integration(2m43s),emulator(4m29s),hosting(7m11s),triggers:inspect(8m26s),functions-discover(6m45s),storage-deploy(6m38s)fa6cdcd78client-integration(3m50s),emulator(4m26s),hosting(7m9s),triggers:inspect(8m6s),functions-discover(8m21s),storage-deploy(11m54s)b10a8181dclient-integration(2m39s),emulator(4m41s),hosting(6m55s),triggers:inspect(9m8s),functions-discover(7m24s),storage-deploy(6m53s)