Tee-Object writes UTF-16, so grepping your own log returns zero matches twice over
encoding · case
Symptom
Section titled “Symptom”You tee a test run to a log so you can grep it afterwards. The console output is perfect. Every subsequent filter returns zero matches — not an error, not a partial result, just nothing. So you conclude the suite printed nothing and start debugging the wrong thing.
"not ok 1 - boom" | Tee-Object -FilePath $env:TEMP\t.txt | Out-Nullconst raw = readFileSync(process.env.TEMP + "\\t.txt");raw.slice(0, 8).toString("hex"); // fffe6e006f007400 <- UTF-16LE + BOMraw.toString("utf8").split(/\r?\n/) .filter((l) => /^not ok/.test(l)).length; // 0raw.toString("utf16le").split(/\r?\n/) .filter((l) => /^not ok/.test(l)).length; // 0 <- still zero!Two layers, and fixing only the first still gives you zero:
Tee-Object -FilePathinherits the 5.1 default encoding: UTF-16LE with BOM. Reading as UTF-8 yieldsn\0o\0t\0— the anchor^not oknever matches.- Decode as
utf16leand you still get zero, because the BOM survives asU+FEFFat the head of the first line, so^no longer sits againstn.
The zero-match result is identical for “the file is empty”, “the pattern is wrong”, and “the encoding is wrong”, which is what makes this expensive.
Workaround
Section titled “Workaround”npm test 2>&1 | Out-File -Encoding utf8 run.log # 5.1: UTF-8 with BOMnpm test 2>&1 | Set-Content -Encoding utf8NoBOM run.log # 7+: cleanWhen a POSIX consumer must read it on 5.1, drop to .NET:
[System.IO.File]::WriteAllText($path, $text). On the reader side, strip a
leading U+FEFF before anchoring — decoding correctly is not enough.
oss-outfile-bom covers Out-File and >. This is the same default reached
through a different cmdlet, and it deserves its own entry because Tee-Object is
specifically the thing you add in order to grep later — so the failure lands
exactly where you were about to look. The BOM-survives-utf16le-decode half is not
covered by that case either.
Real-world hit
Section titled “Real-world hit”Hit while auditing a Windows CI failure in lidge-jun/codexclaw: the suite summary lines were filtered out of a tee’d log, making a 1901-test run look like it produced no output at all.