Skip to content

Refuse a catalogue in which two entries carry one guid - #139

Merged
iderex merged 1 commit into
mainfrom
check/812-a-duplicate-guid-is-refused
Aug 24, 2026
Merged

Refuse a catalogue in which two entries carry one guid#139
iderex merged 1 commit into
mainfrom
check/812-a-duplicate-guid-is-refused

Conversation

@iderex

@iderex iderex commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Contributes to iderex/operations#812, whose remaining item is that the check for a duplicate guid "belongs in front of the manifest producer and has to make it red".

What was wrong

A guid is a plugin's identity to a Jellyfin server: it names the plugin in the configuration, in the catalogue and on disk. Nothing on the route from a descriptor to the published file asked whether two entries claimed one identity. The instance that produced the issue was measured across all twelve plugin manifests before any of the three had a release: discover, requests and invites all carried the plugin template's own guid.

What this adds

catalogue.JudgeShared reads the entries Build produced and refuses the run before publish.Place is reached, so a colliding stock leaves the install address answering with the file from the last run that finished. It sits beside JudgeDropped and Judge in Publish, in the same position and for the same reason: every refusal is decided before any byte is produced.

Every shared identifier is named at once, and each with the entries that carry it, so a stock in which two pairs collide is repaired in one pass rather than one run per pair.

Why this is a refusal rather than a note in the report

A generator that accumulates into a map keyed by guid ships one entry and drops the other, with nothing red and nothing said - the file is well formed, deterministic, and missing a plugin. manifest/determinism_test.go says in its own comment that a map keyed by guid is how a generator accumulates. This route builds a slice, so both entries survive as far as this rule, and this is where the run stops instead of publishing them. Either way the manifest is wrong before anybody uses it.

Watched refusing

Two mutations, each the mistake somebody actually makes, both reverted afterwards.

The call in Publish deleted:

go test ./internal/catalogue -count=1
--- FAIL: TestTwoEntriesUnderOneGuidAreRefusedRatherThanPlaced
    two entries carried one guid and the run exited zero:
    manifest.json: wrote new bytes, carrying 2 plugin(s) and 2 version(s).
FAIL

The carrier count off by one, len(names) < 2 to < 3:

--- FAIL: TestTwoEntriesUnderOneGuidAreRefusedRatherThanPlaced
--- FAIL: TestASharedGuidIsRefusedInBothDirections/one_identity_twice
    one identity twice was accepted
FAIL

The near-miss, and why it is the load-bearing test

TestTwoPluginsWithIdentitiesOfTheirOwnArePlaced publishes two plugins whose descriptors carry different guids and asserts both entries reach the file. It is there because published() hands every fixture plugin one package constant, so a rule refusing any run with two plugins in it would have passed every other test in this package. carrying() is the plant: it rewrites the guid in the descriptor body rather than on the release, because the guid never travels on the release - the route reads it out of the asset.

The means

Go, in the package that already holds the order the parts run in, judged by the suite that already exists. The rule is a comparison over entries the route has in hand, so it needs no new runtime, no dependency and no parallel apparatus; go.mod is byte-identical after this change.

The bound, stated rather than left to be discovered

The comparison is byte-for-byte on the identifier as written. A pair differing only in case, or only in braces, reaches the rule as two identifiers and is not refused. Reading otherwise would refuse pairs no server confuses, which is a judgement about what a server parses rather than about what this repository declares; internal/identity is where a guid is held to its shape. The bound is written at the rule and argued on iderex/operations#812.

What was run

go run . gate
gate examined 14 of 14 legs.
  build  test  format  editorconfig  tests-reach-nothing  no-hardcoded-names
  site-fetches-nothing-outside  site-links-resolve  coverage
  site-declares-its-language  install-address-is-answered
  colour-stands-off-every-surface  page-matches-the-token-file  pr-hygiene
                                                            all passed

The harness did not run and none of its three requirements was asked for; no leg above depends on any of them. Nothing here reaches the network: every fixture is invented and the only host in one is the domain reserved for documentation.

No second reader

This board has no second reader tonight. The two mutations above and their output stand in place of one.

…ions#812)

A guid is a plugin's identity to a Jellyfin server: it names the plugin in
the configuration, in the catalogue and on disk. Nothing between a descriptor
and the published file asked whether two entries claimed one identity, and
three plugin boards shipped the plugin template's own guid, measured across
all twelve manifests before any of them had a release.

JudgeShared reads the entries the route has built and refuses the run before
publish.Place is reached, so a colliding stock leaves the address answering
with the file from the last run that finished. Every shared identifier is
named at once and each with the entries that carry it, so a stock in which
two pairs collide is repaired in one pass.

WHAT MAKES THE REFUSAL WORTH MORE THAN THE SHAPE IT LOOKS LIKE. A generator
accumulating into a map keyed by guid ships one entry and drops the other
with nothing red and nothing said: the file is well formed, deterministic,
and missing a plugin. This route builds a slice, so both entries survive as
far as here and the run stops instead of publishing them.

Watched refusing, twice, on the mistakes somebody makes:

    the call in Publish deleted
    go test ./internal/catalogue -count=1
    --- FAIL: TestTwoEntriesUnderOneGuidAreRefusedRatherThanPlaced
        two entries carried one guid and the run exited zero
        manifest.json: wrote new bytes, carrying 2 plugin(s) and 2 version(s).

    the carrier count off by one, len(names) < 2 -> < 3
    --- FAIL: TestTwoEntriesUnderOneGuidAreRefusedRatherThanPlaced
    --- FAIL: TestASharedGuidIsRefusedInBothDirections/one_identity_twice
        one identity twice was accepted

Both reverted afterwards.

The near-miss is the test beside it: two plugins whose descriptors carry
identities of their own are placed, and both entries reach the file. It is
there because published() hands every fixture plugin one constant, so a rule
refusing any run with two plugins in it would have passed every other test in
the package. carrying() is the plant, and it rewrites the guid in the
descriptor rather than in the release, because the guid never travels on the
release.

The comparison is byte-for-byte on the identifier as written. A pair
differing only in case reaches the rule as two identifiers; that is a bound
rather than a decision, it is stated where the rule is, and where it is
argued is the issue above.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex
iderex merged commit 2a8f88a into main Aug 24, 2026
36 checks passed
@iderex
iderex deleted the check/812-a-duplicate-guid-is-refused branch August 24, 2026 12: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.

1 participant