Skip to content

scripts and links: five reports from the 2026-08-26 batch (#664, #663, #671, #670, #677) - #683

Merged
oyvindberg merged 13 commits into
masterfrom
script-facing-gaps
Sep 2, 2026
Merged

scripts and links: five reports from the 2026-08-26 batch (#664, #663, #671, #670, #677)#683
oyvindberg merged 13 commits into
masterfrom
script-facing-gaps

Conversation

@oyvindberg

@oyvindberg oyvindberg commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Five of the seven open reports from the 2026-08-26 batch, plus a cleanup the
last of them dragged in. Six commits, one concern each.

Branched off #682 while it was open, since #664 lives in the block that PR
rewrites. #682 has since merged, so this applies cleanly to master.

Closes #664. Closes #663. Closes #671. Closes #670. Closes #677.

#664 — a Scala.js test link ignored the project's jsKind

Two places decided how to emit the program, and they disagreed. The main link
read --module-kind, then the project's own jsKind. The test link declared
ScalaJsLinkConfig.Debug and took its CommonJSModule along with the debug
semantics it actually wanted — so a build saying jsKind: esmodule had its
tests linked as CommonJS, with no flag able to change it and nothing reporting
the substitution.

Both paths now derive the kind through one function, and runScalaJsTestSuite
is handed the kind that was linked rather than deriving a second opinion
from a constant. That part matters more than it looks: the test adapter picks
its Input from this value, and a NoModule program loaded as a module — or the
reverse — fails before any test runs. A second opinion is the one thing it must
never be.

Debug semantics stay. Nobody wants their tests run through the optimizer, and
the output directory is named after the mode, so nothing moved.

The suite passed either way

This is why the regression test asserts on the emitted JavaScript rather than on
the run's verdict. With the fix reverted, the ESModule case reports:

did not contain "export {" ... the test link did not emit an ES module,
so `jsKind: esmodule` never reached it

while the suite it ran still reported PASSED example.ModuleKindSuite: 1 passed, 0 failed. The link and the adapter agreed with each other and disagreed with
the build, so every behavioural signal was green. A test checking which config
object was passed around would have passed throughout too — the constant was
consistent, consistently wrong.

The fixture carries an @JSExportTopLevel, because a Scala.js program with no
exported members looks nearly identical under CommonJS and under ESModule. The
issue said as much; without an export there is nothing to tell them apart by.

Both directions are covered — esmodule must emit export, commonjs must
emit exports.greet — since a test asserting only the ESModule case would leave
"does it still honour the declaration when the declaration is the old constant"
unchecked.

#671commands.compile could not say whether it did anything

No new machinery was needed. The server already reports a CompileReason per
project, UpToDate already reached the client, and BuildState dropped it on
the floor under a comment saying it was purely informational.

It is accumulated now, and compile returns the BuildSummary the run
produced. The verdict is unchanged — a failed compile still throws — so a caller
who only wants "did it work" goes on ignoring the value.

BuildSummary.noOp is BSP's CompileReport.noOp computed from that: true when
every project that compiled was already up to date.

A run in which nothing compiled at all is deliberately not a no-op. There
was no compile to be a no-op about, and answering true would tell a deploy
script it may skip on the strength of a run that never looked. That is the case
the unit tests spend a third of their assertions on.

#663 — no link a script could call

Three lines, once #671 had opened the return path. Takes the same LinkOptions
the command line does.

#670 — a script's own exit code got the full-command dump

bleep <script> went through cli.apply, which throws on any non-zero exit with
the whole command line in the message. For git or gpg that is right: something
went wrong, and the reader needs the command to diagnose it. For a script that
exits 1 to report a problem it has already described, it means the report is
followed by a java path, two --add-opens flags and a hundred-jar classpath on
one line — and the reader stops looking for the two readable lines above it. The
code the script chose was replaced by bleep's flat 1 as well.

cli.exitCode hands the code back; cli.apply is that plus the throw, so every
existing caller keeps the diagnosis it had. bleep run and bleep <script>
JVM, node and native alike — use the former and return
BleepException.SubprocessExit, which renders as one line and carries the code
through to bleep's own exit status via ExitCode.FromSubprocess.

Covered through JvmRunner.Forked, which the ITs otherwise never touch: the
harness swaps in InProcess for every other test, and a System.exit there
would take the test JVM down with it.

#677 — the link would not say where it put things

The report asks for an artifact model: the graph should track a link output the
way it tracks compiled classes, and a downstream task should trigger the link.
Most of that premise is already true. bleep test builds a LinkTask and
DiscoverTask depends on it; bleep run calls linkProject before runJs;
LinkResult is a first-class task result the DAG stores and reads. The one verb
left is package, which is not a bleep command but a script — and #663 above
just handed that script commands.link.

What was actually missing is smaller and sharper: a script could trigger a link
and could not find out what it produced. It had to rebuild

link-output/<debug|release>/js/main.js

by hand — a layout bleep owns and has already renamed once, when --release
grew minification and the directory had to follow the mode rather than the
optimizer.

The authoritative answer already existed and already crossed the wire.
BuildEvent.LinkSucceeded carries generatedFiles, with a comment on the field
saying it is taken from the linker "rather than found by looking in the output
directory afterwards, which is guesswork that goes stale the moment a platform
changes its layout". bleep declined to guess for its own use and then left
scripts nothing else. BuildState matched the field as _, the same way it was
dropping CompileReason before #671.

It is BuildSummary.linkedOutputs now, and LinkedOutput.mainArtifact gives
the ordering convention a name instead of leaving callers to know that
files.head is special.

What this deliberately does not build: the model field, the DAG edge and the
artifact model the report asks for. Once the address is available, everything
else in #677 is four lines of script, and a script can do all of it. The one
thing mechanics would still buy is watch — edit a frontend source, have a server
pick up new JS — because bleep <script> -w watches the script project's
transitive deps and a JVM script project cannot legitimately depend on a JS one.
That is thin, and not worth a model concept until someone wants it.

test reports what ran

Same three lines as compile and link. Worth having for a reason that is not
the obvious one: a failing test throws, so "it returned" already meant nothing
failed. What a caller could not learn is whether anything ran, which is the
failure mode that reads as green in CI — the same hole testProjectsWithoutSuites
closes inside bleep, now visible to a script.

Its limit is stated rather than papered over: failures and cancelledSuites
describe a run that threw before the caller could read them, so the Java
TestReport carries counts only. Inspecting failures rather than propagating
them needs a non-throwing entry point, which is a separate decision.

Publish takes one version input, and dynver moved inside

Found while auditing the rest of Commands for return values worth forwarding.
Publish.Options carried versionOverride: Option[String] beside
versionFallback: Option[() => String] — a two-field encoding of a one-of-two
choice, with "exactly one must be set" enforced at runtime and, on the Java
side, thrown from a constructor. The thunk existed so the command line could
skip shelling out to git when --version was given; that laziness belongs to
resolving a version, not to the shape of the input, and the Java bridge was
collapsing it on the spot anyway.

It is now PublishVersion: Specified(value) or Dynver. Three commands that
spelled the same idea three ways — PublishLocal a required String,
PublishSonatype a bare versionOverride, Publish both fields — take one
type, and bleep publish-local --version becomes optional like every other
publish path.

Two things fell out of moving the resolution in:

  • dynverSonatypeSnapshots = true, and the paragraph explaining why it must be
    true, was written twice — once in Main, once in PublishSonatype. Once now.
  • --assert-release was asking "is this a snapshot" two ways. Publish
    inspected the string for + and -SNAPSHOT; PublishSonatype asked dynver.
    One answer now, and it is dynver's.

Behaviour is preserved, including the wart: --assert-release does not check a
version the caller spelled out. That used to read versionOverride.isEmpty
what it did, not why. It is a case in a match now with the reason written down,
and worth revisiting on its own rather than while moving it.

Also here

The Java scripting API gets the same two additions. Leaving it out would keep
"bleep's Commands exposes no link" literally true for Java scripts. The Java
CompileReport is deliberately narrower than the summary it comes from —
BuildSummary changes shape whenever the build display does, and a published
interface should not.

docs/concepts/bleep-scripts.mdx shows all of it — the noOp() check a script
actually writes, reading a linked path off the LinkReport instead of
rebuilding it, and .versionFromGit(). The CI/CD guide and the Maven Central
tutorial both now say what --assert-release applies to, and the CLI reference
is regenerated for the now-optional publish-local --version.

Also in here: the Kotlin/JS half of #664, and what it uncovered

#664 is the Scala.js side. The Kotlin lane had the same defect and no report:
model.KotlinJs.moduleKind was in the build model, decodable from bleep.yaml,
and had no reader anywhere in the server — the link read --module-kind and
otherwise used a hardcoded CommonJS.

Wiring it up immediately failed, which is why nobody could have been using it:
Kotlin names ES output <module>.mjs, the output lookup only knew .js, so it
found nothing — and reported that as "Kotlin/JS linking failed" with an empty
diagnostic list, for a compiler that had exited 0 with nothing to say. Both
fixed. A linker that succeeds while producing no module now says so and lists
what it did write.

Kotlin/JS test links stay UMD and now warn instead of silently substituting:
bleep runs those tests by require-ing the output from a generated CommonJS
script, which an ES module cannot satisfy. A warning and not a failure — those
builds' tests pass today.

model.KotlinJs had nine fields and no reader, so the rest were settled either
way rather than left ambiguous. Five threaded through (moduleName,
sourceMap, sourceMapPrefix, sourceMapEmbedSources, generateDts) — nearly
free, since bleep already set every one of them on the compiler arguments
unconditionally and the only question was which constant to hardcode. Three
deleted at every level they appeared: target was never passed to kotlinc at
all
and is a Gradle runner concept bleep does not implement, outputMode is
bleep's per-phase decision, and developmentMode is the same switch as dce.

Threading moduleName exposed one more: the linker identified the project's own
KLIB by matching file names against config.moduleName. Fine while that was
always the project name, wrong the moment a build could change it — the KLIB on
disk is named after the project, so nothing matched and the link failed. One
string doing two jobs; the lookup takes the project's name explicitly now.

Also in here: link and test share one output directory

The compile/link path handed LinkExecutor a base of targetDir/link-output,
the test path handed it targetDir. One project linked by both landed in two
trees, each with an up-to-date check blind to the other. This is #673's shape
one level up: that fix stopped every suite linking its own copy, this stops
the two commands doing the same. TaskDag.KotlinJsConfig.outputDir is gone
with it — no reader, and the two callers filled it with different values, which
is what made the paths look like they disagreed.

Also in here: a link matrix, and the test harness that runs it

bleep test has had a matrix since #682. bleep link had per-piece tests and
nothing that took a declaration, linked it, and ran the result — which is where
three defects sat at once, all found in the last few days.

13 cases across four targets: Scala.js (jsKind none/commonjs/esmodule, plus
release), Kotlin/JS (default, umd, plain, es, amd, plus one per threaded field),
Scala Native and Kotlin/Native (debug, release). Each links, asserts on the
artifact, and runs it and checks what it printed. The AMD case is shape-only
and says so, rather than looking like coverage it does not have.

Two harness problems surfaced while building it, both of which had been quietly
shaping every integration test:

  • The harness's config never reached the server. handleTest re-read the
    developer's own config.yaml, so parallelism = 1, testRunnerHeap = 512m
    and kspRunnerMaxMemory = 384m — set deliberately, with a comment about the
    explosion of test JVMs they prevent — were all ignored in favour of whatever
    the person running the suite happened to have. The in-process server takes a
    configOverride now; it is None for the real daemon, whose per-request
    re-read is deliberate.
  • Every connection built its own machine governor, sized for all cores and
    most of RAM. A suite running integration tests concurrently therefore had one
    governor per connection, each admitting forks as though nothing else were
    running — and a governor bounds forks across clients, which is only true if
    there is one of it. The harness shares one now, deliberately small.

SuiteIdleTimeoutIT pins both, and the idle-timeout semantics they depend on: a
suite that prints while finishing no test is idle by design and is killed.

Also in here: a Windows delete bug

PublishVersionTest builds a real git repository and deletes it, which failed
only on the Windows runner: git marks every object under .git/objects
read-only, and Windows will not unlink a read-only file at all, where POSIX only
consults the containing directory. Fixed in FileUtils.deleteDirectory rather
than in the test, because bleep clean uses it on Windows too and any read-only
file under a target directory would have refused identically.

Testing

Full suite on a frozen tree, matrix included: 1554 passed, 0 failed, 1 timed
out, 1 skipped
, 229 suites, 755s. The three munit 1.0.0-on-Scala-Native
failures and the load-sensitive ScalaNativeAdvancedTestIntegrationTest that
earlier runs showed are gone.

The one timeout is KotlinNativeTestFrameworkIT, and it is contention rather
than a slow test. Alone on an idle machine that suite runs in 33s; under the
full 13x-parallel run it measured 145s, 169s, and then breached the 120s idle
bound — a 4-5x stretch. The thread dump agrees: cpu=1389ms against
elapsed=213s, parked in runInProcess waiting on one Kotlin/Native inner
build (compile, LLVM link, run). It is a single test, so there is no
intermediate test event between TestStarted and TestFinished to reset the
timer, and stdout deliberately does not reset it — output is not progress. So a
33-second fixture dies because the machine is oversubscribed while it waits.
That is a scheduling problem, not a fixture-size one, and it is not addressed
here. Note the local run included the matrix sweeps, which CI excludes, so CI
carries substantially less of this load.

New: ScalaJsTestModuleKindIT (2), ScriptCommandsIT (3), ScriptExitCodeIT
(2), PublishVersionTest (5, against a real tagged git repo — including that a
dirty tree fails --assert-release), KotlinJsModuleKindIT (2),
TestLinkOutputDirIT, FileUtilsDeleteTest, the LinkMatrixIT family, and 5
cases in BuildStateReducerTest for noOp and linkedOutputs. Each fix was
confirmed by reverting it and watching the new test fail.

Not in here

kotlin.native — ten fields in the model, none of them read, and target
hardcoded to "host" against nineteen the model enumerates. The same
delete-or-support question kotlin.js just answered, deferred because target
there means real cross-compilation rather than plumbing: it needs a toolchain
per target, and a cross-compiled binary cannot be run on the host, so the link
matrix's "run it and check what it printed" stops applying.

--assert-release still does not check a version the caller spelled out. That
is preserved on purpose — it used to read versionOverride.isEmpty, which said
what it did and not why, and is now a case in a match with the reason written
down. Worth deciding on its own rather than while moving it.

Still open from the batch: #669 (sourcegen publishing a stale git-derived
constant) and #666 (bleep scalafix).

🤖 Generated with Claude Code

oyvindberg and others added 6 commits August 30, 2026 22:46
The main link path read `--module-kind`, then the project's `jsKind`. The test
path declared `ScalaJsLinkConfig.Debug` and took its `CommonJSModule` along with
the debug semantics it actually wanted, so a build saying `jsKind: esmodule` had
its tests linked as CommonJS, with no flag able to change it and nothing saying
so.

Debug semantics for a test link stay: nobody wants their tests run through the
optimizer, and the output directory is named after the mode, so this changes
where nothing lands. The module kind is not a semantics choice — it is what the
build declared.

Both paths now derive it through one function, and `runScalaJsTestSuite` is
handed the kind that was linked rather than deriving a second opinion from a
constant. The adapter picks its `Input` from that value, and a NoModule program
loaded as a module — or the reverse — fails before any test runs, so the one
thing it must never be is a guess.

Asserted on the emitted JavaScript, because the suite passed either way: the
constant was consistent, consistently wrong, and a test that checked which
config was passed around would have gone on passing throughout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`bleep <script>` ran everything through `cli.apply`, which throws on any
non-zero exit with the whole command line in the message. For git or gpg that is
right — something went wrong and the reader needs the command to diagnose it.
For a script that exits 1 to report a problem it has already described, it means
the report is followed by the java path, two `--add-opens` flags and a
hundred-jar classpath on one line, and the reader stops looking for the two
readable lines above it. The code the script chose was replaced by bleep's flat
1 as well.

`cli.exitCode` hands the code back instead of throwing; `cli.apply` is that plus
the throw, so every existing caller keeps the diagnosis it had. `bleep run` and
`bleep <script>` — JVM, node and native alike — use the former and return
`BleepException.SubprocessExit`, which renders as one line and carries the code
through to bleep's own exit status.

Covered through `JvmRunner.Forked`, which the ITs otherwise never touch: the
harness swaps in `InProcess`, and a `System.exit` there would take the test JVM
down with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hing (#663, #671)

Two gaps in the surface a build script reaches, both reported against a real
build.

`Commands` declared no `link`, so a script packaging linked JavaScript into a
jar had to shell out to the `bleep` command line and pay the start-up cost a
second time. It takes the same `LinkOptions` the command line does.

`compile` returned `Unit`, so a caller could not tell a compile that rebuilt
everything from one that found nothing to do. bleep already knows: the server
reports a `CompileReason` per project and `UpToDate` is the one that means the
compiler ran and found nothing. That reason reached the client and was dropped
on the floor with a comment saying it was purely informational. It is now
accumulated, and `compile` hands back the `BuildSummary` the run produced —
verdict unchanged, so a caller who only wants "did it work" can go on ignoring
it.

`BuildSummary.noOp` is BSP's `CompileReport.noOp` computed from that. A run in
which nothing compiled at all is deliberately not a no-op: there was no compile
to be a no-op about, and answering true would let a deploy skip on the strength
of a run that never looked.

Mirrored on the Java scripting API, which is the other half of the same surface
— with a `CompileReport` narrower than the summary it comes from, since
`BuildSummary` changes shape whenever the build display does and a published
interface should not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`commands.link` said whether it worked and nothing else, so a script that wants
the linked JavaScript — to copy it into a jar, to serve it — had to rebuild

    link-output/<debug|release>/js/main.js

by hand. That is a layout bleep owns and has already renamed once, when
`--release` grew minification and the directory had to follow the mode rather
than the optimizer.

The authoritative answer already existed and already crossed the wire.
`BuildEvent.LinkSucceeded` carries `generatedFiles`, with a comment on the field
saying it is taken from the linker "rather than found by looking in the output
directory afterwards, which is guesswork that goes stale the moment a platform
changes its layout". bleep declined to guess for its own use and then left
scripts with nothing else to do. `BuildState` dropped the field on the floor,
the same way it was dropping `CompileReason` before #671.

It is accumulated now as `BuildSummary.linkedOutputs`, and `link` returns the
summary the way `compile` does. `LinkedOutput.mainArtifact` names the ordering
convention — main artifact first, then source map and chunks — instead of
leaving callers to know that `files.head` is special.

This is what #677 actually needed. The rest of that report is already true:
`bleep test` and `bleep run` both link first, `LinkResult` is a first-class task
result the DAG tracks, and packaging is a script, which since #663 can drive the
link itself. What was missing was never the trigger — it was the address.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same three lines as `compile` and `link`: the run already builds a
`BuildSummary` and `Commands.test` threw it away.

The counts are the useful part, and not for the reason one might expect. A
failing test throws, so "it returned" already meant "nothing failed" — what a
caller could not learn is whether anything *ran*, which is the failure mode that
reads as green in CI. That is the same hole `testProjectsWithoutSuites` exists
to close inside bleep, now visible to a script.

Deliberately not solved here: the failure detail. `failures` and
`cancelledSuites` describe a run that threw before the caller could read them,
so a script that wants to inspect failures rather than propagate them needs a
non-throwing entry point. The Java `TestReport` carries counts only, and says
so, rather than exposing lists that are always empty by the time you hold them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…side bleep

`Publish.Options` carried `versionOverride: Option[String]` beside
`versionFallback: Option[() => String]` — a two-field encoding of a one-of-two
choice, with "exactly one must be set" enforced at runtime, and on the Java side
thrown from a constructor. The thunk existed so the command line could skip
shelling out to git when `--version` was given; that laziness belongs to
resolving a version, not to the shape of the input, and the Java bridge was
collapsing it on the spot anyway.

It is now `PublishVersion`: `Specified(value)` or `Dynver`. Three commands that
spelled the same idea three ways — `PublishLocal` a required `String`,
`PublishSonatype` a bare `versionOverride`, `Publish` both fields — take one
type now, and `bleep publish-local --version` becomes optional like every other
publish path.

Resolution moved with it. The command line used to build a `DynVerPlugin` and
pass it in as a lambda, while `PublishSonatype` built its own inside bleep-core;
one function builds it now, so `dynverSonatypeSnapshots = true` — and the
paragraph explaining why it must be true — is stated once instead of twice.
`PublishSonatype` still holds a dynver object, because `CiReleasePlugin` reads
git state itself and wants more than a version string, but it gets it from that
same function.

That also settles a disagreement nobody had noticed. `--assert-release` asked
"is this a snapshot" in two places and answered it two ways: `Publish` inspected
the string for `+` and `-SNAPSHOT`, `PublishSonatype` asked dynver. One answer
now, and it is dynver's.

Behaviour preserved, including the wart: `--assert-release` does not check a
version the caller spelled out. That used to read `versionOverride.isEmpty`,
which said what it did and not why; it is a case in a match now, with the reason
written down. Worth revisiting on its own, not while moving it.

Docs: the scripts page gains `.versionFromGit()` and what `--assert-release`
applies to, the CI/CD guide says the flag has nothing to check when a version is
passed, the Maven Central tutorial says the same at the flag, and the CLI
reference is regenerated for the now-optional `publish-local --version`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@oyvindberg oyvindberg changed the title scripts and links: four reports from the 2026-08-26 batch (#664, #663, #671, #670) scripts and links: five reports from the 2026-08-26 batch (#664, #663, #671, #670, #677) Sep 1, 2026
oyvindberg and others added 7 commits September 1, 2026 21:59
…t share one output directory

Two loose ends noted while fixing #664 and #673, both of which turned out to be
hiding a third thing.

**Kotlin/JS ignored `kotlin.js.moduleKind`.** The main link read the
`--module-kind` flag and otherwise used a hardcoded CommonJS, so a build
declaring `es` got CommonJS and nothing said otherwise — the Kotlin half of
#664. The field is now read, with the same precedence Scala.js uses: flag, then
project, then the default.

`model.KotlinJs` had no reader anywhere in the server before this. All nine of
its fields were dead, so only `moduleKind` is added to `schema.json`; listing
the rest would advertise settings that still do nothing.

**Kotlin/JS test links stay UMD, and now say so.** Unlike Scala.js, where the
adapter picks its `Input` per module kind, bleep runs Kotlin/JS tests by
generating a CommonJS script that installs a QUnit mock as a global and then
`require`s the output. ES modules make that throw; plain and AMD do not export
what the runner reads. So the declaration cannot be honoured there — and a
project that declares one now gets a warning rather than a silent substitution.
A warning and not a failure: those builds' tests pass today, and breaking them
to report a limitation of the runner is a poor trade.

**Honouring `es` uncovered why nobody could have used it.** Kotlin names ES
output `<module>.mjs`; the output lookup only knew `.js`, so it found nothing —
and the caller reported that as "Kotlin/JS linking failed" with an empty
diagnostic list, for a compiler that had exited 0 with nothing to complain
about. Both fixed: `.mjs` is recognised, and a linker that succeeds while
producing no module now says that, and lists what it did write.

**link and test wrote to different directories.** The compile/link path passed
`LinkExecutor` a base of `targetDir/link-output`; the test path passed
`targetDir`. One project linked by both landed in two trees, each with an
up-to-date check blind to the other. `bleep run` reads the first, which is why
nothing noticed. This is #673's shape one level up: that fix stopped every
*suite* linking its own copy, this stops the two *commands* doing the same.

`TaskDag.KotlinJsConfig.outputDir` is gone with it. It had no reader —
`LinkExecutor` computes the directory for every platform — and the two callers
filled it with two different values, which is what made the paths look like they
disagreed when neither was being consulted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`bleep test` has had a matrix since #682: every framework on every target, run
end to end, asserted on the JUnit XML. `bleep link` had per-piece tests — a
release link minifies, a test link honours `jsKind` — and nothing that took a
declaration, linked it, and ran the result.

That gap is where three defects sat at once, all found in the last few days: a
`--release` Scala.js link never called `withMinify`, a Kotlin/JS `es` link
reported "linking failed" because the output lookup only knew `.js`, and a test
link and a main link wrote into two different directories.

13 cases across four targets:

| target | cases |
|---|---|
| Scala.js | jsKind none / commonjs / esmodule, plus release |
| Kotlin/JS | default, umd, plain, es, amd |
| Scala Native | debug, release |
| Kotlin/Native | debug, release |

Each links, asserts on the artifact, and then **runs it and checks what it
printed**. Running is the part the per-piece tests could not do: a linked
program that is the wrong module kind still exists and still has plausible
bytes, and only node or the loader says otherwise.

Two details worth stating rather than leaving to be rediscovered:

- Every case asserts the path bleep *reported* — `BuildSummary.linkedOutputs`,
  new in this branch — rather than one the test reconstructed. A link that
  writes somewhere unexpected fails here instead of being quietly found by a
  directory walk.
- The AMD case is shape-only and says so. An AMD module defines itself into a
  loader node does not have, so "we did not run it" is recorded in the output
  rather than looking like coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`model.KotlinJs` had nine fields and, until this branch, no reader at all. Now
that one of them is wired, the rest had to be settled either way.

**Threaded through — five.** `moduleName`, `sourceMap`, `sourceMapPrefix`,
`sourceMapEmbedSources`, `generateDts`. This turned out to be nearly free, and
for a reason worth writing down: bleep sets every one of these on the compiler
arguments unconditionally, so the question was never whether to support them but
which constant to hardcode. They now come from the build in the same order the
module kind does — flag, then project, then bleep's default. `sourceMapPrefix`
stays an `Option`, since it is the only one where "unset" is a real state the
compiler is never told about.

**Deleted — three, everywhere they appeared.**

- `target` (browser/nodejs/both) was never passed to kotlinc at all: there is no
  `setField` for it in either the compile or the link path, so it was dead on
  bleep's own `KotlinJsCompilerConfig` too, not just in the build model. It is
  not a compiler flag — in Gradle it selects a runner and a toolchain, which
  bleep does not have. Gone from the model, the schema's vocabulary, the
  compiler config, and its enum. If a browser runner ever lands it comes back
  with something behind it.
- `outputMode` was equally unread at both levels; the paths set `irProduceJs` /
  `irProduceKlibFile` directly, which is bleep deciding per phase, correctly.
- `developmentMode` is the same switch as `dce`, driven by `--optimize` and
  release mode. Two names for one knob is an invitation for them to disagree.

**And `moduleName` did not work when first wired.** The linker identifies the
project's own KLIB by matching file names against `config.moduleName` — fine
while that was always the project name, wrong the moment a build could change
it: the KLIB on disk is named after the project, so nothing matched, `includes`
was never set, and the link failed. One string was doing two jobs, and only one
of them is the user's to choose; the lookup now takes the project's name
explicitly.

Six link-matrix cases, one per field, each asserting on what the compiler
produced — a `.d.ts` appears, the artifact is `custom_name.js`, the map carries
`sourcesContent`, its sources are prefixed — rather than on the configuration it
was handed. Schema and the cross-building docs list the six that do something.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things the in-process BSP server used to invent for itself, both of which
undid what its caller was trying to say.

**The config.** `handleTest` and `handleCompile` re-read the developer's own
`config.yaml` per request. That is right for the real daemon — `parallelism`,
the heaps and the idle timeout are machine settings, and picking up an edit
without a restart is the point — but there was no way for a caller that already
has a config to be heard. So bleep's own integration tests were carefully
setting `parallelism = 1`, `testRunnerHeap = 512m` and `kspRunnerMaxMemory =
384m`, with a long comment about the explosion of test JVMs those prevent, and
every one of them was ignored in favour of whatever the person running the suite
happened to have in their file. `configOverride` is `None` for the daemon, so
production behaviour is unchanged.

**The governor.** Each connection built `MachineResources.forThisMachine` —
every core, most of the RAM — so a suite running integration tests concurrently
had one governor per connection, each admitting forks as though nothing else
were running. A governor bounds forks *across* clients, which is only true if
there is one of it. The harness now makes one and shares it, deliberately small
at 2 cores and 2GB: this JVM is itself one of several the outer `bleep test`
forked, and those outer forks are governed by a daemon that cannot see anything
started in here.

**And `close` waits.** Closing the pipes is what tells `run()` to return, but
nothing waited for it, so a test could finish and the next one start while the
previous server was still cancelling requests and killing child processes. The
wait is bounded — a server that will not come down must not hang the suite
trying to leave.

`SuiteIdleTimeoutIT` pins both this and the idle-timeout semantics it depends
on: a suite that prints while finishing no test is idle *by design*, and is
killed. Showing that needs a one-minute timeout, which needs the harness's
config to actually arrive — with the override removed the fixture runs to
completion against the developer's own three-minute setting and the test fails
saying so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ndows)

`PublishVersionTest` builds a real git repository in a temp directory and
deletes it afterwards. That passed everywhere except the Windows CI runner,
where all five of its cases failed in cleanup with

    java.nio.file.AccessDeniedException:
      ...\bleep-publish-version-...\.git\objects\07\0774631921...

Windows will not unlink a read-only file at all; POSIX only consults the
containing directory's permissions, so the same tree deletes cleanly there. git
marks every object under `.git/objects` read-only, so a `.git` directory is the
easiest way to meet this — but nothing about it is specific to tests.
`FileUtils.deleteDirectory` is what `bleep clean` uses on Windows, so any
read-only file under a target directory would have refused the same way.

Fixed where the difference is, not in the test that tripped over it: a delete
that hits `AccessDeniedException` clears the attribute and tries once more. The
retry is narrow on purpose — that exception only, once, and the second attempt
is allowed to throw, because a tree that still cannot be emptied is a real
failure rather than something to walk past.

`FileUtilsDeleteTest` asserts the outcome rather than the mechanism, since the
mechanism differs by platform: on Windows it exercises the clear-and-retry, on
POSIX the plain path, and on either one a regression fails it on the platform
that has the problem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fallout from giving the in-process server a `configOverride`, and the good kind:
this test had a workaround for the defect that fix removed, and the workaround
is now the thing that is wrong.

`configuredDefaultMb` read the developer's own `config.yaml`, with a comment
saying it deliberately did not read `IntegrationTestHarness.testConfig` because
"the in-process server does not consult" it. That was accurate — and it meant
the assertions were written against whatever heap the person running the suite
had configured, which is exactly the problem: on this machine the harness asked
for 512m and the fork got 4g, and the test agreed with the fork because it was
reading the same file the server was.

Now that the server uses the config it is handed, the test reads it too. The
assertions stop depending on a file outside the repository, which is what the
old comment was trying to achieve by the only means then available.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`munit 1.0.0` on Scala Native failed the version sweep on all three Scala
versions, with one assertion: the constructor failure reached the report without
stack frames. That is true, and it is not a defect. munit 1.0.0 lets the process
die where 1.3.4 reports through the adapter, so the same fixture yields a bleep
process error there and a named cause with frames here.

The expectations it was measured against — `ctorFailureReport` and
`reportsCtorStackFrames` — are keyed on framework and platform. The comment on
the first already says the behaviour "cannot be keyed on the framework alone",
and the same reasoning extends one axis further: it cannot be keyed on the
platform alone either.

Rather than record every framework's reporting shape per version, the sweep
stops asserting it. Its stated job is discovery, runner selection and the fork
protocol — the ways an old release breaks a *user* — and holding a five-year-old
release to today's reporting detail only produces red that has to be explained
away, which is how a matrix stops being read. That file already warns about
exactly this for resolution errors.

What every version is still held to is unchanged and is the part that matters:
the suite whose constructor throws appears in the report, and something about it
is marked failed. A failure that disappears still fails here.

`pinnedVersion` is derived from `pinnedFor` rather than passed in, so it stays
right whichever matrix calls `checkFixture`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@oyvindberg
oyvindberg merged commit 13cb289 into master Sep 2, 2026
18 of 19 checks passed
@oyvindberg
oyvindberg deleted the script-facing-gaps branch September 2, 2026 06:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant