a simpler package manager for no-build websites
unpm test
51 folders · 2 files

README.md

Integration tests #

Run from the repository root, using only Go and its standard library:

go run ./test
go run ./test single-file pin-exact
go run ./test 'types-*'
go run ./test -list
go run ./test -binary ./unpm single-file

make integration runs the same runner; make integration CASES=single-file selects a case. make test runs both the existing unit tests and integration cases. CI runs both suites.

Case layout #

Each directory containing case.json is a case. Adding a case requires no Go code. For example:

test/single-file/
├── case.json
├── unpm.json
├── server/
│   └── mylib.js
├── expected/
│   ├── stdout.txt
│   ├── stderr.txt
│   └── vendor/
│       ├── {{host}}/
│       │   └── mylib.js
│       ├── importmap.js
│       ├── importmap.json
│       └── jsconfig.json
└── actual/                    # ignored; replaced on each run

The runner builds unpm once, copies unpm.json and the contents of an optional input/ directory to a fresh temporary working directory, serves server/ over local HTTP, and runs the binary there. Other files at the case root are not copied into the working directory.

For pinning, cleanup, check, and why, place the starting vendor tree in input/vendor/. Other starting files belong under input/ too: for example, input/custom.json becomes custom.json in the working directory. The usual unpm.json stays at the case root; defining it both there and in input/ is an error. Cases that do not need a configuration file can omit it.

The selected output paths and captured stdout/stderr are copied to actual/. The runner compares the entire expected/ and actual/ trees, including file contents, missing files, extra files, and directories. An absent expected output directory asserts that the command must not create it. Empty stdout/stderr files assert that the corresponding stream is empty. Files are compared byte for byte; JSON formatting, whitespace, and final newlines matter.

Cases use --silent to suppress progress and success messages while retaining plain-text warnings, errors, and why results. Arguments are passed exactly as written in case.json; the runner does not insert flags. Console output is compared directly, without stripping spinner frames or ANSI escape sequences.

Both successful and failed runs leave actual/ available for inspection. The temporary working directory and binary are removed when the runner exits. The runner never updates expected/. Edit expectations deliberately and review their Git diff, or inspect a failure with:

git diff --no-index test/single-file/expected test/single-file/actual

case.json #

A minimal case is:

{
  "args": ["vendor", "--silent"]
}

Supported settings:

Key Default Meaning
args Required Arguments passed directly to unpm, without a shell. [] tests invoking it without arguments.
exit_code 0 Expected process exit code.
outputs ["vendor"] Relative files or directories to copy into actual/ and compare. Use [] for console-only cases.
timeout "10s" Maximum command duration, as a Go duration. A timeout always fails the case.
responses {} HTTP response overrides, keyed by exact request path including its query string.

Unknown settings are errors. Output paths must be relative, non-overlapping, and inside the working directory. Include every output location whose presence or absence matters; for example, an --out assets/deps case can specify ["assets", "vendor"] to also catch accidental writes to the default directory.

HTTP fixtures #

Requests normally read the matching file under server/, ignoring the query string for the filename. Missing files return 404. JavaScript is served as text/javascript, .json and .map as application/json, and other files as text/plain. No directory listings or automatic directory redirects are used.

Each response override accepts status (default 200), headers, and file (a relative path under server/). For example:

{
  "args": ["vendor"],
  "responses": {
    "/pkg": {
      "status": 302,
      "headers": {"Location": "/pkg/index.js"}
    },
    "/pkg/index.js": {
      "headers": {"X-Typescript-Types": "./index.d.ts"}
    },
    "/shim": {
      "file": "shim.js",
      "headers": {"X-ESM-Path": "/pkg/index.js"}
    }
  }
}

An override applies only to its exact request URI. This allows a redirect from /file.js to /file.js?target=es2022 while serving the latter normally. A file override also lets a URL such as /pkg have a body while files beneath /pkg/ exist on disk. Non-200 responses have an empty body unless file is set.

Use local fixture URLs for downloads; the runner does not block external network access. Existing cases need no internet access.

Variable output #

The runner expands two literal placeholders in input filenames and contents, command arguments, server file contents, and response header values:

  • {{host}}: the local HTTP server's 127.0.0.1:port address. Write URLs as http://{{host}}/mylib.js and expected vendor paths as vendor/{{host}}/mylib.js.
  • {{work}}: the absolute temporary working directory, for cases testing absolute configuration paths.

Those exact values are replaced with placeholders in captured output paths and contents, so actual/ can be compared directly with expected/. Nothing else is normalized: diagnostics are not sorted, JSON is not reformatted, and terminal bytes are retained in console output and file contents.

The string-literal-import case records an existing scanner limitation: text that looks like an import inside a string triggers a download. Since required download failures are fatal, this case expects an error and no output directory.