CLI ZIP extraction regression verification Revision: 202b06fc5b3436e3e549e6d913d1ecca2ee2b8d2 Scope: tests/helpers/zip.ts and tests/unit/utils/zip.test.ts only; production source unchanged. Runtime: Node 24.15.0 with npm 11.19.1; minimum Node 22.13.0 also exercised. COMMAND: CI=true npm run test:unit EXIT: 0 ✓ tests/unit/utils/exit-codes.test.ts (2 tests) 0ms ✓ tests/unit/utils/rules-proxy.test.ts (1 test) 0ms Test Files 78 passed (78) Tests 665 passed (665) Start at 04:17:40 Duration 25.81s (transform 850ms, setup 0ms, collect 2.48s, tests 22.94s, environment 0ms, prepare 96ms) COMMAND: Node 22.13.0: node node_modules/vitest/vitest.mjs run tests/unit/utils/zip.test.ts EXIT: 0 RUN v3.2.7 /tmp/netliloop-work/run306/cli ✓ tests/unit/utils/zip.test.ts (18 tests) 728ms ✓ extracts complete binary files and empty directories (deflate: false) 387ms ✓ extracts complete binary files and empty directories (deflate: true) 312ms Test Files 1 passed (1) Tests 18 passed (18) Start at 04:17:40 Duration 1.26s (transform 136ms, setup 0ms, collect 212ms, tests 728ms, environment 0ms, prepare 97ms) COMMAND: npm run typecheck EXIT: 0 > netlify-cli@27.10.2 typecheck > node ./node_modules/typescript-native/bin/tsc COMMAND: npm run lint EXIT: 0 > netlify-cli@27.10.2 lint > eslint --cache COMMAND: npm run format:check EXIT: 0 > netlify-cli@27.10.2 format:check > oxfmt --check Checking formatting... All matched files use the correct format. Finished in 1508ms on 650 files using 4 threads. MUTATION CONTROLS 13 intentional product regressions each pass the original 647 tests; each fails the final added suite. Across these controls, every one of the 18 new tests has been seen failing. Source was restored after each control run. allow-directory-symlink-writes Original suite exit 0: Test Files 77 passed (77); Tests 647 passed (647) Final new suite exit 1: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯; FAIL tests/unit/utils/zip.test.ts > rejects writes through an existing directory symlink outside the target; FAIL tests/unit/utils/zip.test.ts > rejects writes through an archive directory symlink outside the target; Test Files 1 failed (1); Tests 2 failed | 16 passed (18) follow-file-symlinks Original suite exit 0: Test Files 77 passed (77); Tests 647 passed (647) Final new suite exit 1: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯; FAIL tests/unit/utils/zip.test.ts > replaces an existing file symlink without overwriting its target; FAIL tests/unit/utils/zip.test.ts > replaces an archive file symlink without overwriting its target; Test Files 1 failed (1); Tests 2 failed | 16 passed (18) swallow-archive-errors Original suite exit 0: Test Files 77 passed (77); Tests 647 passed (647) Final new suite exit 1: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 7 ⎯⎯⎯⎯⎯⎯⎯; FAIL tests/unit/utils/zip.test.ts > rejects an archive entry outside the target: relative; FAIL tests/unit/utils/zip.test.ts > rejects an archive entry outside the target: absolute; FAIL tests/unit/utils/zip.test.ts > rejects an archive entry outside the target: nested; FAIL tests/unit/utils/zip.test.ts > closes the archive after a truncated stream and rejects without extracting later entries; FAIL tests/unit/utils/zip.test.ts > closes the archive after a write failure and rejects without extracting later entries; FAIL tests/unit/utils/zip.test.ts > rejects writes through an existing directory symlink outside the target; FAIL tests/unit/utils/zip.test.ts > rejects writes through an archive directory symlink outside the target; Test Files 1 failed (1); Tests 7 failed | 11 passed (18) swallow-stream-write-errors Original suite exit 0: Test Files 77 passed (77); Tests 647 passed (647) Final new suite exit 1: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯; FAIL tests/unit/utils/zip.test.ts > closes the archive after a truncated stream and rejects without extracting later entries; FAIL tests/unit/utils/zip.test.ts > closes the archive after a write failure and rejects without extracting later entries; Test Files 1 failed (1); Tests 2 failed | 16 passed (18) drop-executable-mode Original suite exit 0: Test Files 77 passed (77); Tests 647 passed (647) Final new suite exit 1: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯; FAIL tests/unit/utils/zip.test.ts > preserves executable permissions without special mode bits; Test Files 1 failed (1); Tests 1 failed | 17 passed (18) extract-symlinks-as-files Original suite exit 0: Test Files 77 passed (77); Tests 647 passed (647) Final new suite exit 1: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯; FAIL tests/unit/utils/zip.test.ts > extracts relative symlinks to files within the archive; FAIL tests/unit/utils/zip.test.ts > rejects writes through an archive directory symlink outside the target; Test Files 1 failed (1); Tests 2 failed | 16 passed (18) append-on-reextraction Original suite exit 0: Test Files 77 passed (77); Tests 647 passed (647) Final new suite exit 1: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯; FAIL tests/unit/utils/zip.test.ts > overwrites a previous extraction without leaving trailing bytes; Test Files 1 failed (1); Tests 1 failed | 17 passed (18) extract-macos-metadata Original suite exit 0: Test Files 77 passed (77); Tests 647 passed (647) Final new suite exit 1: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯; FAIL tests/unit/utils/zip.test.ts > skips macOS metadata and continues extracting later entries; Test Files 1 failed (1); Tests 1 failed | 17 passed (18) truncate-extracted-bytes Original suite exit 0: Test Files 77 passed (77); Tests 647 passed (647) Final new suite exit 1: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 8 ⎯⎯⎯⎯⎯⎯⎯; FAIL tests/unit/utils/zip.test.ts > extracts complete binary files and empty directories (deflate: false); FAIL tests/unit/utils/zip.test.ts > extracts complete binary files and empty directories (deflate: true); FAIL tests/unit/utils/zip.test.ts > skips macOS metadata and continues extracting later entries; FAIL tests/unit/utils/zip.test.ts > overwrites a previous extraction without leaving trailing bytes; FAIL tests/unit/utils/zip.test.ts > extracts relative symlinks to files within the archive; FAIL tests/unit/utils/zip.test.ts > accepts a destination directory reached through a symlink; FAIL tests/unit/utils/zip.test.ts > replaces an existing file symlink without overwriting its target; FAIL tests/unit/utils/zip.test.ts > replaces an archive file symlink without overwriting its target; Test Files 1 failed (1); Tests 8 failed | 10 passed (18) reject-empty-archives Original suite exit 0: Test Files 77 passed (77); Tests 647 passed (647) Final new suite exit 1: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯; FAIL tests/unit/utils/zip.test.ts > accepts an empty archive; Test Files 1 failed (1); Tests 1 failed | 17 passed (18) forget-canonical-destination Original suite exit 0: Test Files 77 passed (77); Tests 647 passed (647) Final new suite exit 1: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯; FAIL tests/unit/utils/zip.test.ts > accepts a destination directory reached through a symlink; Test Files 1 failed (1); Tests 1 failed | 17 passed (18) swallow-open-errors Original suite exit 0: Test Files 77 passed (77); Tests 647 passed (647) Final new suite exit 1: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯; FAIL tests/unit/utils/zip.test.ts > rejects an invalid archive; Test Files 1 failed (1); Tests 1 failed | 17 passed (18) leak-archive-on-failure Original suite exit 0: Test Files 77 passed (77); Tests 647 passed (647) Final new suite exit 1: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯; FAIL tests/unit/utils/zip.test.ts > closes the archive after a truncated stream and rejects without extracting later entries; FAIL tests/unit/utils/zip.test.ts > closes the archive after a write failure and rejects without extracting later entries; Test Files 1 failed (1); Tests 2 failed | 16 passed (18) INDEPENDENT ARCHIVE-TO-HTTP RECEIPT COMMAND: python /tmp/netliloop-work/run306/site-command.py cli /tmp/netliloop-work/run306/node22/node-v22.13.0-linux-x64/bin/node --import tsx /tmp/netliloop-work/run306/verify-independent-zips.mts /tmp/netliloop-work/run306/cli-tester /opt/buildhome/.local/share/mise/installs/python/3.14.3/lib/python3.14/zipfile/__init__.py:1721: UserWarning: Duplicate name: 'handler.txt' return self._open_to_write(zinfo, force_zip64=force_zip64) {"fixtureRoot": "/tmp/netlify-zip-proof-l4NRku", "archives": 6, "producer": "Python zipfile"} ⬥ Extracted function modern from /tmp/netlify-zip-proof-l4NRku/functions/modern.zip. ⬥ Extracted function stored from /tmp/netlify-zip-proof-l4NRku/functions/stored.zip. ⬥ Extracted function deflated from /tmp/netlify-zip-proof-l4NRku/functions/deflated.zip. { "outcome": "PASS", "node": "v22.13.0", "platform": "linux", "checks": [ "real ZISI listFunctions discovered and real FunctionsRegistry extracted three Python ZIP archives", "stored/deflated content, empty paths, relative symlinks, metadata skip and special-mode stripping verified on real filesystem", "real loopback HTTP POST -> createHandler -> registry URL lookup -> lambda-local invocation returned exact Unicode text, binary digest, symlink contents, query/body and headers", "new ___netlify-entry-point.mjs filename selected and invoked; missing function returns HTTP 404", "Python-produced traversal and escaping directory symlink rejected; duplicate file symlink replaced without changing external sentinel" ], "seams": [ "Function manifests supplied as local fixture metadata", "Local geolocation state uses Map; production offline/mock geolocation mode selected", "Express body-parser/server shell composed around exported production createHandler; private getFunctionsServer factory not invoked" ], "mocks": [], "limitations": [ "Node V1 runtime used, including .mjs entrypoint selection; V2 worker runtime not exercised", "CLI command parser, full netlify serve startup, deploy and external APIs not exercised", "POSIX filesystem only" ], "temporaryFixturesRemoved": true } EXIT: 0 INDEPENDENT FIXTURE VALIDATION COMMAND: python /tmp/netliloop-work/run306/site-command.py cli /tmp/netliloop-work/run306/node22/node-v22.13.0-linux-x64/bin/node --import tsx /tmp/netliloop-work/run306/verify-zip-fixture-helper.mts /tmp/netliloop-work/run306/cli-tester /tmp/netliloop-work/run306/helper-validation.zip EXIT: 0 COMMAND: python3 /tmp/netliloop-work/run306/validate-zip-fixture-helper.py /tmp/netliloop-work/run306/helper-validation.zip PASS: Python zipfile independently validated helper archive CRCs, binary bytes, UTF-8, deflate, directory, duplicate ordering, UNIX executable mode and symlink mode. EXIT: 0 REVIEW Adversarial review: fixed absolute-path sentinel alignment and added observation of actual archive close events. A deliberate omitted close passes 647 original tests and fails both new failure-path cases. Rejected expansion into product behavior: recursive mkdir outside the target precedes canonical validation. This is pre-existing and was reproduced; the new tests claim file-write protection, not absence of outside directory creation. CONFIRMED pre-existing behavior: extraction rejects the file write but creates outside/new before checking containment; no file is written. LIMITS Linux/POSIX execution here; seven permission/symlink cases intentionally skip on Windows. Windows/macOS CI pending at PR creation. HTTP proof uses V1 runtime, a fixture manifest, production offline/mock geolocation mode and an Express shell around exported createHandler. V2 workers and complete CLI startup remain unverified. No module mocks in the HTTP proof; unit close-event spies preserve real behavior. LINEAR Application token endpoint returned HTTP 401 twice. No mutation used the personal API key. Scheduled tracking must be created in FRB (Frameworks & Build), whose FRB-2334 and recent CLI backlog establish ownership. Proposed issue: Guard CLI function ZIP extraction against regressions. Label: netliloop-pr. Risk: low, because only tests change. Related existing FRB-2340 concerns primitives extraction and is not claimed as fixed by these tests. FILE SHA256 18ae6b275bf4b866fba4f76283ee14d5b66c99496179688c4229654ba4bad21e src/utils/zip.ts b4131cf74106659a16363320d74800c3d170b02e7310d1256e7a202c0ae18167 tests/helpers/zip.ts d04f0e8f986914df33d3b5f609f86053703bf0c339a0e40c477e0baa5817848b tests/unit/utils/zip.test.ts ADDITIONAL NODE 24 INTEGRATION RECEIPT COMMAND: CI=true NETLIFY_TELEMETRY_DISABLED=1 node --import tsx verify-independent-zips.mts /path/to/cli EXIT: 0 /opt/buildhome/.local/share/mise/installs/python/3.14.3/lib/python3.14/zipfile/__init__.py:1721: UserWarning: Duplicate name: 'handler.txt' return self._open_to_write(zinfo, force_zip64=force_zip64) {"fixtureRoot": "/tmp/netlify-zip-proof-tF2DUH", "archives": 6, "producer": "Python zipfile"} ⬥ Extracted function modern from /tmp/netlify-zip-proof-tF2DUH/functions/modern.zip. ⬥ Extracted function stored from /tmp/netlify-zip-proof-tF2DUH/functions/stored.zip. ⬥ Extracted function deflated from /tmp/netlify-zip-proof-tF2DUH/functions/deflated.zip. { "outcome": "PASS", "node": "v24.15.0", "platform": "linux", "checks": [ "real ZISI listFunctions discovered and real FunctionsRegistry extracted three Python ZIP archives", "stored/deflated content, empty paths, relative symlinks, metadata skip and special-mode stripping verified on real filesystem", "real loopback HTTP POST -> createHandler -> registry URL lookup -> lambda-local invocation returned exact Unicode text, binary digest, symlink contents, query/body and headers", "new ___netlify-entry-point.mjs filename selected and invoked; missing function returns HTTP 404", "Python-produced traversal and escaping directory symlink rejected; duplicate file symlink replaced without changing external sentinel" ], "seams": [ "Function manifests supplied as local fixture metadata", "Local geolocation state uses Map; production offline/mock geolocation mode selected", "Express body-parser/server shell composed around exported production createHandler; private getFunctionsServer factory not invoked" ], "mocks": [], "limitations": [ "Node V1 runtime used, including .mjs entrypoint selection; V2 worker runtime not exercised", "CLI command parser, full netlify serve startup, deploy and external APIs not exercised", "POSIX filesystem only" ], "temporaryFixturesRemoved": true } PROOF REVIEW Evidence reviewer accepted the test-only Linux/V1 scope and required the Node 24 integration receipt plus fixture-validation script links. Both were added. Optional suggestion also implemented: mutation harness now requires exactly exit 1 and a named failed test, rejecting runner crashes; all thirteen final mutations satisfy this stricter check.