Skip to content

Install end to end against a real Jellyfin server - #138

Merged
iderex merged 3 commits into
mainfrom
install-server-and-refuse-a-half-supplied-job
Aug 23, 2026
Merged

Install end to end against a real Jellyfin server#138
iderex merged 3 commits into
mainfrom
install-server-and-refuse-a-half-supplied-job

Conversation

@iderex

@iderex iderex commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Closes #54.

The needs-jellyfin requirement was carried by nothing, so asking for it refused
rather than running, and the job of that name supplied neither of the two things
the requirement is named for. Both are here, and the run below is against a real
server rather than against this repository's idea of one.

The run

gh workflow run harness.yml --repo Flowfin/hub \
  --ref install-server-and-refuse-a-half-supplied-job -f requirement=needs-jellyfin

Run 2026-08-23 at 4b2e224, which is this branch's head. It came back green, and
what it printed is the evidence for every clause of the done-condition:

server under test: http://127.0.0.1:8096
published catalogue under test: https://flowfin.dev/manifest.json
the server answered after 0s, version 10.11.11, not yet set up
the startup wizard is complete and this run holds the administrator account
plugin under test: Requests (0f9c9107-b31b-459e-81fa-6d35dac25e79)
1 checksum(s) rewritten, every archive url left alone
serving a catalogue whose checksum belongs to no archive at http://127.0.0.1:39643/manifest.json
the server now carries one repository: http://127.0.0.1:39643/manifest.json
the server lists Requests with 1 version(s)
the server refused the request itself, 500: Error processing request.
after 2m0s the server has not installed Requests, which is the refusal this half is for
serving the published catalogue unchanged at http://127.0.0.1:36669/manifest.json
the server now carries one repository: http://127.0.0.1:36669/manifest.json
the server lists Requests with 1 version(s)
the server installed Requests after 0s, one field away from the manifest it refused
the server no longer holds the plugin, so the half below watches a real install
the server now carries one repository: https://flowfin.dev/manifest.json
the server lists Requests with 1 version(s)
the server installed Requests from the published address after 0s
--- PASS: TestAServerInstallsFromThePublishedCatalogueAndRefusesAMismatchedChecksum (123.63s)

Three halves, and the order is the argument

The mismatched checksum runs first, so nothing has installed yet and a plugin
that is absent afterwards is absent because the install was refused. It runs
against a catalogue the check serves itself, because
decisions/manifest-address.md treats the published address as a promise that
cannot be withdrawn, and a knowingly broken manifest published there is served
to every operator polling it.

The same catalogue with its own checksum runs second, over the same server, the
same address and the same archive URL. That is the one-change neighbour, and
without it the refusal above would sit equally well with a server that could not
use an address of that shape at all. It uninstalls afterwards and refuses to
carry on until the server says the plugin is gone, so the half after it watches
an install arrive rather than finding one already there.

The published address runs last, on a server the half before it left clean.

What the first run against a server corrected

The check as first written read only the plugin list, on the assumption that the
server accepts an install request and refuses later while it hashes what it
downloaded. It answers the request itself instead, with a 500, so that first run
failed on a refusal that had already happened. Both spellings are read now and
the plugin list is still what decides.

The gate never depends on any of it

go run . harness | grep 'carried by'
  carried by internal/address/answered_test.go, internal/freshness/published_test.go, internal/links/elsewhere_test.go, internal/sources/world_test.go
  carried by internal/keyboard/browser_test.go
  carried by internal/pairing/server_test.go

Taking all six out of the tracked tree, in a throwaway clone of this branch at
4b2e224 that was pushed nowhere:

go clean -testcache
git rm -q internal/address/answered_test.go internal/freshness/published_test.go \
          internal/links/elsewhere_test.go internal/sources/world_test.go \
          internal/keyboard/browser_test.go internal/pairing/server_test.go
git commit -q -s -m "scratch: remove every harness-tagged file"
go run . gate
gate examined 14 of 14 legs.
  build                            passed
  test                             passed
  format                           passed
  editorconfig                     passed
  tests-reach-nothing              passed
  no-hardcoded-names               passed
  site-fetches-nothing-outside     passed
  site-links-resolve               passed
  coverage                         passed
  site-declares-its-language       passed
  install-address-is-answered      passed
  colour-stands-off-every-surface  passed
  page-matches-the-token-file      passed
  pr-hygiene                       passed

go run . gate | grep -c 'no check carries it yet'
3

The count of three is what says the removal reached the whole harness rather
than part of it.

The job supplies both, and something reads that now

The job brings the server up and hands the address in, which is the shape
needs-network already uses. Unset is a refusal in the check rather than a skip,
so a job that started a server and forgot to pass the address fails as a
refusing check rather than as a green run over nothing.

Nothing read a job definition that delivers half of what it names.
Requirement.Supplies names what the runner puts in the environment and
SuppliedIn reads which names each job sets. Watched refusing, on the
one-character mistake somebody makes:

-      JELLYFIN_ADDRESS: http://127.0.0.1:8096
+      JELLYFIN_ADRESS: http://127.0.0.1:8096

go test ./internal/harness -run TestEveryJobSuppliesWhatItsRequirementSaysIsHandedIn -count=1
--- FAIL: TestEveryJobSuppliesWhatItsRequirementSaysIsHandedIn (0.01s)
    harness_test.go:160: requirement needs-jellyfin is handed JELLYFIN_ADDRESS, and the job of that name in .github/workflows/harness.yml sets [JELLYFIN_ADRESS]
FAIL	flowfin.dev/hub/internal/harness	2.171s

Reverted afterwards. What it cannot see is written where it is written: a job
that sets the address and starts nothing at it passes this reader, because a
step bringing a server up has no shape a reader of that file could recognise.
That half is caught by the check refusing on an environment it cannot use, which
is why the check refuses rather than skipping.

The means

Go and the test runner that arrives with it, which is what decisions/means.md
already settled for every check here, and no new dependency: the check talks to
the server over net/http and the standard library's JSON, and go.mod is
unchanged. A container library would have been a dependency added in the same
week as the browser driver, for a server this job can start with one line
instead. The server image is pinned by digest beside its version, the way every
action in that file is pinned by commit beside its tag.

What is not covered

The negative half needs the server to reach a listener on the machine running
the check, which is why the job puts the server on this machine's own network. A
server that cannot route back fails as the catalogue never filling in, and the
failure says so.

The check refuses a server that has already been set up, because it takes the
administrator account on the one it talks to. CONTRIBUTING.md says how to
bring a fresh one up here rather than leaving somebody to find that out by
watching the check refuse.

Nothing in this ran with elevation, and no step asks for any. The server is an
ordinary container on a runner that is destroyed afterwards, and the check's own
listener takes a port the operating system chooses, which is above 1024 by
construction.

Who has read this

Nobody but me. There is no second reader on this change, and the run above and
the commands beside it stand in place of one rather than a claim that somebody
checked them.

An earlier branch for this issue swept an untracked directory into a commit that
was no part of this work. Nothing was rewritten: the corrected history is this
branch, under a new name, and the other one is abandoned rather than forced over.

iderex added 3 commits August 23, 2026 13:14
The needs-jellyfin requirement was carried by nothing, so asking for it
refused rather than running, and the job of that name supplied neither of
the two things the requirement is named for. Both are here.

internal/pairing/server_test.go carries the tag. It sets a fresh server up,
adds a repository address, waits for the catalogue to fill in, installs the
plugin and reads the server's own plugin list back. The mismatched-checksum
half runs first, against a catalogue the check serves itself on a port the
operating system chooses, because decisions/manifest-address.md treats the
published address as a promise that cannot be withdrawn and a knowingly
broken manifest there is served to every operator polling it. Only the
positive half points at the published address, and it runs second so the
refusal above cannot be read off a server that had already installed.

The failure this prevents is the one nothing on this side can see. A
manifest that passes every leg of the gate can still render as an empty
repository or a refused install because of a field the server reads
differently, and the install step is where decisions/artifact-checksum-pairing.md
is finally decided against real bytes rather than against a fixture.

The job brings the server up and hands the address in, which is the shape
needs-network already uses for MANIFEST_ADDRESS. Unset is a refusal in the
check rather than a skip, so a job that started a server and forgot to pass
the address fails as a refusing check rather than as a green run over
nothing. The image is pinned by digest beside its version, the way every
action in that file is pinned by commit beside its tag.

Nothing here reaches the gate. The tag keeps the file out of every leg, and
the gate's own report goes on saying the harness did not run.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
The first run against a server answered 500 to the install request itself
rather than accepting it and refusing while it hashed the archive, so the
half reading only the plugin list failed on a refusal that had already
happened. Both spellings are the refusal now and the plugin list is still
what decides.

A refusal on its own says nothing about which field caused it, so the same
catalogue with its own checksum is served from the same address and installs.
That is the one-change neighbour: same server, same archive, one field. It
uninstalls afterwards, and refuses to carry on until the server says the
plugin is gone, because the half after it has to watch an install arrive
rather than find one already there.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
…54)

A job here reports under a requirement's own name and carries a one-line
step running the entry point, and until now that was everything anything
read. A job that also has to hand the check an address could set it, or not,
and every reader passed either one. The check it starts then refuses for want
of an environment nobody set, which reads as a broken check rather than as a
job missing a line.

Requirement.Supplies names what the runner puts in the environment, and
SuppliedIn reads which names each job sets, in both spellings this file uses:
a mapping on the job and a mapping on a step. needs-network already had the
shape and is now declared with it, so the guard reads two jobs rather than
the one it was written for.

Watched refusing, on the one-character mistake somebody makes:

    -      JELLYFIN_ADDRESS: http://127.0.0.1:8096
    +      JELLYFIN_ADRESS: http://127.0.0.1:8096

    go test ./internal/harness -run TestEveryJobSuppliesWhatItsRequirementSaysIsHandedIn -count=1
    --- FAIL: TestEveryJobSuppliesWhatItsRequirementSaysIsHandedIn (0.01s)
        harness_test.go:160: requirement needs-jellyfin is handed JELLYFIN_ADDRESS, and the job of that name in .github/workflows/harness.yml sets [JELLYFIN_ADRESS]
    FAIL

Reverted afterwards. What it cannot see is stated where it is written: a job
that sets the address and starts nothing at it passes this, and what catches
that half is the check refusing on an environment it cannot use.

CONTRIBUTING.md says how to run the requirement here, because a contributor
on a laptop cannot have a test bring a server up for them and should not have
to discover that by watching it refuse.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex
iderex merged commit e6a4cde into main Aug 23, 2026
39 checks passed
@iderex
iderex deleted the install-server-and-refuse-a-half-supplied-job branch August 23, 2026 11:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Install end to end against a real Jellyfin server, in the environment-bound harness

1 participant