Repository navigation
statechart: tests fail when TMPDIR goes through a symlink (the macOS default) or where LD_PRELOAD is unavailable #923
Description
node --test pstack/skills/statechart/scripts/test/statechart.test.mjs on main (99b609a) fails some tests outside a plain Linux setup. CI runs only on ubuntu-latest with the default /tmp, where all 150 pass, so it does not catch this. PR #889 mentions the same thing: "13 tests there expect a /tmp temp path and fail on both."
There are two separate causes, both in the test suite. Neither is a bug in the statechart scripts.
| Setup | Failures (of 150) |
|---|---|
Linux, default /tmp |
0 |
Linux, TMPDIR set to a symlink to a real folder |
13 |
macOS, default TMPDIR |
18 |
macOS, TMPDIR set to its resolved path |
4 |
1. The suite assumes tmpdir() contains no symlink (13 tests on Linux, 14 on macOS)
The tests make their work folder with mkdtempSync(join(tmpdir(), "statechart-")), run the script with that folder as its cwd, and compare paths the script prints with paths built from tmpdir(). When TMPDIR or one of its parents is a symlink, the two disagree:
- the script's
process.cwd()returns the resolved path, such as/real/statechart-X - paths that come from
TMPDIRstay unresolved, such as the TLC output folder/link/statechart-X/tmp/statechart-Y
For example, in "an error after TLC runs keeps TLC's output, and the message names its folder", rel() in statechart.mjs computes path.relative(cwd, work). Because the prefixes differ, the result starts with .., so rel() prints the absolute path. The message says ... are in /link/.../tmp/statechart-Y., the test's regex expects are in tmp/statechart-..., the match is null, and destructuring it throws TypeError: object null is not iterable. The --against audit, output-folder, TLC standard-module and graph-dump tests fail the same way.
This does not depend on the OS. In a node:22-bookworm container with TMPDIR pointing at a symlink, 13 tests fail. On macOS the default TMPDIR is /var/folders/.../T/, and /var is a symlink to /private/var, so everyone running the suite there hits it. macOS fails the same 13 tests plus "statechart.mjs finds TLC with the real mise when an untrusted mise.toml is next to the spec and above TMPDIR", which needs mise and was skipped in the container. Running on macOS with TMPDIR="$(cd "$TMPDIR" && pwd -P)" removes all 14.
Resolving the temp root in the scripts instead (fs.realpathSync(os.tmpdir()) at the four mkdtempSync calls, or resolving inside rel()) does not fix it. Some tests then fail the other way, because the script prints the resolved path while the test expects one under the symlink. The scripts' fallback to an absolute path is fine. In real use the temp folder is usually outside the cwd, so the message already shows an absolute path there.
2. The rename tests need LD_PRELOAD (4 tests on macOS)
"a run that cannot rename ..." and "a run whose rename fails after it moves the output folder aside ..." compile a small shared library and load it with LD_PRELOAD so that rename fails. LD_PRELOAD is a feature of the Linux dynamic loader. The tests skip only when the library does not compile, so on a system where it compiles but LD_PRELOAD is not honored, such as macOS, they run anyway. There the run exits 0 where the tests expect 2, which fits rename succeeding. These 4 still fail on macOS with a resolved TMPDIR, and they pass on Linux.
Suggested fix (tests only)
- In
statechart.test.mjs, resolve the temp root once (const tmpdir = () => realpathSync(osTmpdir())) so expected paths match what the script sees. - Build the rename preload only when
process.platform === "linux", and skip those tests elsewhere with a reason that namesLD_PRELOAD.
I will open a PR with this fix.
Environment
- macOS 26.5 (arm64), Node 22.23, TLC 1.7.4
- Linux:
node:22-bookwormcontainer (arm64), Node 22.23, OpenJDK 17, TLC 1.7.4
Activity
deanrie commentedon Oct 26, 2026
Hey @oesni, one data point from a macOS 26 machine with the default
TMPDIR(/var/folders/.../T/, which resolves to/private/var/...): onmain99b609a the suite shows 150 tests, 58 pass, 0 fail, 92 skipped, becauseneedsTlcskips every test you list when TLC is not installed. So there is a third setup in your table, "no TLC", where the symlink and theLD_PRELOADcases never run at all, and a contributor without TLC sees green on exactly the machine that would otherwise fail. Your reading ofrel()matches the code:path.relative(cwd, work)withcwdresolved andworkbuilt from the unresolvedTMPDIRstarts with.., so the absolute fallback prints.Agreed that both fixes belong in the test file and not in the scripts. For the rename tests, gating on
process.platform === "linux"with a skip reason that namesLD_PRELOADis the right shape; the current reason, "cannot compile a rename preload", is what hides the macOS case, since the library does compile there.One coordination note: #785 (open) also edits
statechart.test.mjs, adding fixtures for foreign files undertlc/, so whichever of the two lands second will need a rebase. If yours comes first I will rebase #785 onto it.