Skip to content

statechart: tests fail when TMPDIR goes through a symlink (the macOS default) or where LD_PRELOAD is unavailable #923

Open

Description

@oesni

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 TMPDIR stay 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 names LD_PRELOAD.

I will open a PR with this fix.

Environment

  • macOS 26.5 (arm64), Node 22.23, TLC 1.7.4
  • Linux: node:22-bookworm container (arm64), Node 22.23, OpenJDK 17, TLC 1.7.4

Activity

  1. deanrie commented on Oct 26, 2026

    @deanrie

    Hey @oesni, one data point from a macOS 26 machine with the default TMPDIR (/var/folders/.../T/, which resolves to /private/var/...): on main 99b609a the suite shows 150 tests, 58 pass, 0 fail, 92 skipped, because needsTlc skips every test you list when TLC is not installed. So there is a third setup in your table, "no TLC", where the symlink and the LD_PRELOAD cases never run at all, and a contributor without TLC sees green on exactly the machine that would otherwise fail. Your reading of rel() matches the code: path.relative(cwd, work) with cwd resolved and work built from the unresolved TMPDIR starts 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 names LD_PRELOAD is 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 under tlc/, so whichever of the two lands second will need a rebase. If yours comes first I will rebase #785 onto it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions